1
0
Fork 0
golang-github-pocketbase-dbx/expression.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

421 lines
11 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 (
"fmt"
"sort"
"strings"
)
// Expression represents a DB expression that can be embedded in a SQL statement.
type Expression interface {
// Build converts an expression into a SQL fragment.
// If the expression contains binding parameters, they will be added to the given Params.
Build(*DB, Params) string
}
// HashExp represents a hash expression.
//
// A hash expression is a map whose keys are DB column names which need to be filtered according
// to the corresponding values. For example, HashExp{"level": 2, "dept": 10} will generate
// the SQL: "level"=2 AND "dept"=10.
//
// HashExp also handles nil values and slice values. For example, HashExp{"level": []interface{}{1, 2}, "dept": nil}
// will generate: "level" IN (1, 2) AND "dept" IS NULL.
type HashExp map[string]interface{}
// NewExp generates an expression with the specified SQL fragment and the optional binding parameters.
func NewExp(e string, params ...Params) Expression {
if len(params) > 0 {
return &Exp{e, params[0]}
}
return &Exp{e, nil}
}
// Not generates a NOT expression which prefixes "NOT" to the specified expression.
func Not(e Expression) Expression {
return &NotExp{e}
}
// And generates an AND expression which concatenates the given expressions with "AND".
func And(exps ...Expression) Expression {
return &AndOrExp{exps, "AND"}
}
// Or generates an OR expression which concatenates the given expressions with "OR".
func Or(exps ...Expression) Expression {
return &AndOrExp{exps, "OR"}
}
// In generates an IN expression for the specified column and the list of allowed values.
// If values is empty, a SQL "0=1" will be generated which represents a false expression.
func In(col string, values ...interface{}) Expression {
return &InExp{col, values, false}
}
// NotIn generates an NOT IN expression for the specified column and the list of disallowed values.
// If values is empty, an empty string will be returned indicating a true expression.
func NotIn(col string, values ...interface{}) Expression {
return &InExp{col, values, true}
}
// DefaultLikeEscape specifies the default special character escaping for LIKE expressions
// The strings at 2i positions are the special characters to be escaped while those at 2i+1 positions
// are the corresponding escaped versions.
var DefaultLikeEscape = []string{"\\", "\\\\", "%", "\\%", "_", "\\_"}
// Like generates a LIKE expression for the specified column and the possible strings that the column should be like.
// If multiple values are present, the column should be like *all* of them. For example, Like("name", "key", "word")
// will generate a SQL expression: "name" LIKE "%key%" AND "name" LIKE "%word%".
//
// By default, each value will be surrounded by "%" to enable partial matching. If a value contains special characters
// such as "%", "\", "_", they will also be properly escaped.
//
// You may call Escape() and/or Match() to change the default behavior. For example, Like("name", "key").Match(false, true)
// generates "name" LIKE "key%".
func Like(col string, values ...string) *LikeExp {
return &LikeExp{
left: true,
right: true,
col: col,
values: values,
escape: DefaultLikeEscape,
Like: "LIKE",
}
}
// NotLike generates a NOT LIKE expression.
// For example, NotLike("name", "key", "word") will generate a SQL expression:
// "name" NOT LIKE "%key%" AND "name" NOT LIKE "%word%". Please see Like() for more details.
func NotLike(col string, values ...string) *LikeExp {
return &LikeExp{
left: true,
right: true,
col: col,
values: values,
escape: DefaultLikeEscape,
Like: "NOT LIKE",
}
}
// OrLike generates an OR LIKE expression.
// This is similar to Like() except that the column should be like one of the possible values.
// For example, OrLike("name", "key", "word") will generate a SQL expression:
// "name" LIKE "%key%" OR "name" LIKE "%word%". Please see Like() for more details.
func OrLike(col string, values ...string) *LikeExp {
return &LikeExp{
or: true,
left: true,
right: true,
col: col,
values: values,
escape: DefaultLikeEscape,
Like: "LIKE",
}
}
// OrNotLike generates an OR NOT LIKE expression.
// For example, OrNotLike("name", "key", "word") will generate a SQL expression:
// "name" NOT LIKE "%key%" OR "name" NOT LIKE "%word%". Please see Like() for more details.
func OrNotLike(col string, values ...string) *LikeExp {
return &LikeExp{
or: true,
left: true,
right: true,
col: col,
values: values,
escape: DefaultLikeEscape,
Like: "NOT LIKE",
}
}
// Exists generates an EXISTS expression by prefixing "EXISTS" to the given expression.
func Exists(exp Expression) Expression {
return &ExistsExp{exp, false}
}
// NotExists generates an EXISTS expression by prefixing "NOT EXISTS" to the given expression.
func NotExists(exp Expression) Expression {
return &ExistsExp{exp, true}
}
// Between generates a BETWEEN expression.
// For example, Between("age", 10, 30) generates: "age" BETWEEN 10 AND 30
func Between(col string, from, to interface{}) Expression {
return &BetweenExp{col, from, to, false}
}
// NotBetween generates a NOT BETWEEN expression.
// For example, NotBetween("age", 10, 30) generates: "age" NOT BETWEEN 10 AND 30
func NotBetween(col string, from, to interface{}) Expression {
return &BetweenExp{col, from, to, true}
}
// Exp represents an expression with a SQL fragment and a list of optional binding parameters.
type Exp struct {
e string
params Params
}
// Build converts an expression into a SQL fragment.
func (e *Exp) Build(db *DB, params Params) string {
if len(e.params) == 0 {
return e.e
}
for k, v := range e.params {
params[k] = v
}
return e.e
}
// Build converts an expression into a SQL fragment.
func (e HashExp) Build(db *DB, params Params) string {
if len(e) == 0 {
return ""
}
// ensure the hash exp generates the same SQL for different runs
names := []string{}
for name := range e {
names = append(names, name)
}
sort.Strings(names)
var parts []string
for _, name := range names {
value := e[name]
switch value.(type) {
case nil:
name = db.QuoteColumnName(name)
parts = append(parts, name+" IS NULL")
case Expression:
if sql := value.(Expression).Build(db, params); sql != "" {
parts = append(parts, "("+sql+")")
}
case []interface{}:
in := In(name, value.([]interface{})...)
if sql := in.Build(db, params); sql != "" {
parts = append(parts, sql)
}
default:
pn := fmt.Sprintf("p%v", len(params))
name = db.QuoteColumnName(name)
parts = append(parts, name+"={:"+pn+"}")
params[pn] = value
}
}
if len(parts) == 1 {
return parts[0]
}
return strings.Join(parts, " AND ")
}
// NotExp represents an expression that should prefix "NOT" to a specified expression.
type NotExp struct {
e Expression
}
// Build converts an expression into a SQL fragment.
func (e *NotExp) Build(db *DB, params Params) string {
if sql := e.e.Build(db, params); sql != "" {
return "NOT (" + sql + ")"
}
return ""
}
// AndOrExp represents an expression that concatenates multiple expressions using either "AND" or "OR".
type AndOrExp struct {
exps []Expression
op string
}
// Build converts an expression into a SQL fragment.
func (e *AndOrExp) Build(db *DB, params Params) string {
if len(e.exps) == 0 {
return ""
}
var parts []string
for _, a := range e.exps {
if a == nil {
continue
}
if sql := a.Build(db, params); sql != "" {
parts = append(parts, sql)
}
}
if len(parts) == 1 {
return parts[0]
}
return "(" + strings.Join(parts, ") "+e.op+" (") + ")"
}
// InExp represents an "IN" or "NOT IN" expression.
type InExp struct {
col string
values []interface{}
not bool
}
// Build converts an expression into a SQL fragment.
func (e *InExp) Build(db *DB, params Params) string {
if len(e.values) == 0 {
if e.not {
return ""
}
return "0=1"
}
var values []string
for _, value := range e.values {
switch value.(type) {
case nil:
values = append(values, "NULL")
case Expression:
sql := value.(Expression).Build(db, params)
values = append(values, sql)
default:
name := fmt.Sprintf("p%v", len(params))
params[name] = value
values = append(values, "{:"+name+"}")
}
}
col := db.QuoteColumnName(e.col)
if len(values) == 1 {
if e.not {
return col + "<>" + values[0]
}
return col + "=" + values[0]
}
in := "IN"
if e.not {
in = "NOT IN"
}
return fmt.Sprintf("%v %v (%v)", col, in, strings.Join(values, ", "))
}
// LikeExp represents a variant of LIKE expressions.
type LikeExp struct {
or bool
left, right bool
col string
values []string
escape []string
// Like stores the LIKE operator. It can be "LIKE", "NOT LIKE".
// It may also be customized as something like "ILIKE".
Like string
}
// Escape specifies how a LIKE expression should be escaped.
// Each string at position 2i represents a special character and the string at position 2i+1 is
// the corresponding escaped version.
func (e *LikeExp) Escape(chars ...string) *LikeExp {
e.escape = chars
return e
}
// Match specifies whether to do wildcard matching on the left and/or right of given strings.
func (e *LikeExp) Match(left, right bool) *LikeExp {
e.left, e.right = left, right
return e
}
// Build converts an expression into a SQL fragment.
func (e *LikeExp) Build(db *DB, params Params) string {
if len(e.values) == 0 {
return ""
}
if len(e.escape)%2 != 0 {
panic("LikeExp.Escape must be a slice of even number of strings")
}
var parts []string
col := db.QuoteColumnName(e.col)
for _, value := range e.values {
name := fmt.Sprintf("p%v", len(params))
for i := 0; i < len(e.escape); i += 2 {
value = strings.Replace(value, e.escape[i], e.escape[i+1], -1)
}
if e.left {
value = "%" + value
}
if e.right {
value += "%"
}
params[name] = value
parts = append(parts, fmt.Sprintf("%v %v {:%v}", col, e.Like, name))
}
if e.or {
return strings.Join(parts, " OR ")
}
return strings.Join(parts, " AND ")
}
// ExistsExp represents an EXISTS or NOT EXISTS expression.
type ExistsExp struct {
exp Expression
not bool
}
// Build converts an expression into a SQL fragment.
func (e *ExistsExp) Build(db *DB, params Params) string {
sql := e.exp.Build(db, params)
if sql == "" {
if e.not {
return ""
}
return "0=1"
}
if e.not {
return "NOT EXISTS (" + sql + ")"
}
return "EXISTS (" + sql + ")"
}
// BetweenExp represents a BETWEEN or a NOT BETWEEN expression.
type BetweenExp struct {
col string
from, to interface{}
not bool
}
// Build converts an expression into a SQL fragment.
func (e *BetweenExp) Build(db *DB, params Params) string {
between := "BETWEEN"
if e.not {
between = "NOT BETWEEN"
}
name1 := fmt.Sprintf("p%v", len(params))
name2 := fmt.Sprintf("p%v", len(params)+1)
params[name1] = e.from
params[name2] = e.to
col := db.QuoteColumnName(e.col)
return fmt.Sprintf("%v %v {:%v} AND {:%v}", col, between, name1, name2)
}
// Enclose surrounds the provided nonempty expression with parenthesis "()".
func Enclose(exp Expression) Expression {
return &EncloseExp{exp}
}
// EncloseExp represents a parenthesis enclosed expression.
type EncloseExp struct {
exp Expression
}
// Build converts an expression into a SQL fragment.
func (e *EncloseExp) Build(db *DB, params Params) string {
str := e.exp.Build(db, params)
if str == "" {
return ""
}
return "(" + str + ")"
}