421 lines
11 KiB
Go
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 + ")"
|
|
}
|