1
0
Fork 0
golang-github-pocketbase-dbx/query_builder.go
Daniel Baumann 02cacc5b45
Adding upstream version 1.11.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-22 10:43:26 +02:00

244 lines
6.1 KiB
Go

// Copyright 2016 Qiang Xue. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package dbx
import (
"bytes"
"fmt"
"regexp"
"strings"
)
// QueryBuilder builds different clauses for a SELECT SQL statement.
type QueryBuilder interface {
// BuildSelect generates a SELECT clause from the given selected column names.
BuildSelect(cols []string, distinct bool, option string) string
// BuildFrom generates a FROM clause from the given tables.
BuildFrom(tables []string) string
// BuildGroupBy generates a GROUP BY clause from the given group-by columns.
BuildGroupBy(cols []string) string
// BuildJoin generates a JOIN clause from the given join information.
BuildJoin([]JoinInfo, Params) string
// BuildWhere generates a WHERE clause from the given expression.
BuildWhere(Expression, Params) string
// BuildHaving generates a HAVING clause from the given expression.
BuildHaving(Expression, Params) string
// BuildOrderByAndLimit generates the ORDER BY and LIMIT clauses.
BuildOrderByAndLimit(string, []string, int64, int64) string
// BuildUnion generates a UNION clause from the given union information.
BuildUnion([]UnionInfo, Params) string
}
// BaseQueryBuilder provides a basic implementation of QueryBuilder.
type BaseQueryBuilder struct {
db *DB
}
var _ QueryBuilder = &BaseQueryBuilder{}
// NewBaseQueryBuilder creates a new BaseQueryBuilder instance.
func NewBaseQueryBuilder(db *DB) *BaseQueryBuilder {
return &BaseQueryBuilder{db}
}
// DB returns the DB instance associated with the query builder.
func (q *BaseQueryBuilder) DB() *DB {
return q.db
}
// the regexp for columns and tables.
var selectRegex = regexp.MustCompile(`(?i:\s+as\s+|\s+)([\w\-_\.]+)$`)
// BuildSelect generates a SELECT clause from the given selected column names.
func (q *BaseQueryBuilder) BuildSelect(cols []string, distinct bool, option string) string {
var s bytes.Buffer
s.WriteString("SELECT ")
if distinct {
s.WriteString("DISTINCT ")
}
if option != "" {
s.WriteString(option)
s.WriteString(" ")
}
if len(cols) == 0 {
s.WriteString("*")
return s.String()
}
for i, col := range cols {
if i > 0 {
s.WriteString(", ")
}
matches := selectRegex.FindStringSubmatch(col)
if len(matches) == 0 {
s.WriteString(q.db.QuoteColumnName(col))
} else {
col := col[:len(col)-len(matches[0])]
alias := matches[1]
s.WriteString(q.db.QuoteColumnName(col) + " AS " + q.db.QuoteSimpleColumnName(alias))
}
}
return s.String()
}
// BuildFrom generates a FROM clause from the given tables.
func (q *BaseQueryBuilder) BuildFrom(tables []string) string {
if len(tables) == 0 {
return ""
}
s := ""
for _, table := range tables {
table = q.quoteTableNameAndAlias(table)
if s == "" {
s = table
} else {
s += ", " + table
}
}
return "FROM " + s
}
// BuildJoin generates a JOIN clause from the given join information.
func (q *BaseQueryBuilder) BuildJoin(joins []JoinInfo, params Params) string {
if len(joins) == 0 {
return ""
}
parts := []string{}
for _, join := range joins {
sql := join.Join + " " + q.quoteTableNameAndAlias(join.Table)
on := ""
if join.On != nil {
on = join.On.Build(q.db, params)
}
if on != "" {
sql += " ON " + on
}
parts = append(parts, sql)
}
return strings.Join(parts, " ")
}
// BuildWhere generates a WHERE clause from the given expression.
func (q *BaseQueryBuilder) BuildWhere(e Expression, params Params) string {
if e != nil {
if c := e.Build(q.db, params); c != "" {
return "WHERE " + c
}
}
return ""
}
// BuildHaving generates a HAVING clause from the given expression.
func (q *BaseQueryBuilder) BuildHaving(e Expression, params Params) string {
if e != nil {
if c := e.Build(q.db, params); c != "" {
return "HAVING " + c
}
}
return ""
}
// BuildGroupBy generates a GROUP BY clause from the given group-by columns.
func (q *BaseQueryBuilder) BuildGroupBy(cols []string) string {
if len(cols) == 0 {
return ""
}
s := ""
for i, col := range cols {
if i == 0 {
s = q.db.QuoteColumnName(col)
} else {
s += ", " + q.db.QuoteColumnName(col)
}
}
return "GROUP BY " + s
}
// BuildOrderByAndLimit generates the ORDER BY and LIMIT clauses.
func (q *BaseQueryBuilder) BuildOrderByAndLimit(sql string, cols []string, limit int64, offset int64) string {
if orderBy := q.BuildOrderBy(cols); orderBy != "" {
sql += " " + orderBy
}
if limit := q.BuildLimit(limit, offset); limit != "" {
return sql + " " + limit
}
return sql
}
// BuildUnion generates a UNION clause from the given union information.
func (q *BaseQueryBuilder) BuildUnion(unions []UnionInfo, params Params) string {
if len(unions) == 0 {
return ""
}
sql := ""
for i, union := range unions {
if i > 0 {
sql += " "
}
for k, v := range union.Query.params {
params[k] = v
}
u := "UNION"
if union.All {
u = "UNION ALL"
}
sql += fmt.Sprintf("%v (%v)", u, union.Query.sql)
}
return sql
}
var orderRegex = regexp.MustCompile(`\s+((?i)ASC|DESC)$`)
// BuildOrderBy generates the ORDER BY clause.
func (q *BaseQueryBuilder) BuildOrderBy(cols []string) string {
if len(cols) == 0 {
return ""
}
s := ""
for i, col := range cols {
if i > 0 {
s += ", "
}
matches := orderRegex.FindStringSubmatch(col)
if len(matches) == 0 {
s += q.db.QuoteColumnName(col)
} else {
col := col[:len(col)-len(matches[0])]
dir := matches[1]
s += q.db.QuoteColumnName(col) + " " + dir
}
}
return "ORDER BY " + s
}
// BuildLimit generates the LIMIT clause.
func (q *BaseQueryBuilder) BuildLimit(limit int64, offset int64) string {
if limit < 0 && offset > 0 {
// most DBMS requires LIMIT when OFFSET is present
limit = 9223372036854775807 // 2^63 - 1
}
sql := ""
if limit >= 0 {
sql = fmt.Sprintf("LIMIT %v", limit)
}
if offset <= 0 {
return sql
}
if sql != "" {
sql += " "
}
return sql + fmt.Sprintf("OFFSET %v", offset)
}
func (q *BaseQueryBuilder) quoteTableNameAndAlias(table string) string {
matches := selectRegex.FindStringSubmatch(table)
if len(matches) == 0 {
return q.db.QuoteTableName(table)
}
table = table[:len(table)-len(matches[0])]
return q.db.QuoteTableName(table) + " " + q.db.QuoteSimpleTableName(matches[1])
}