// 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 + ")" }