| package squirrel |
| |
| import ( |
| "database/sql/driver" |
| "fmt" |
| "io" |
| "reflect" |
| "sort" |
| "strings" |
| ) |
| |
| const ( |
| // Portable true/false literals. |
| sqlTrue = "(1=1)" |
| sqlFalse = "(1=0)" |
| ) |
| |
| type expr struct { |
| sql string |
| args []interface{} |
| } |
| |
| // Expr builds an expression from a SQL fragment and arguments. |
| // |
| // Ex: |
| // Expr("FROM_UNIXTIME(?)", t) |
| func Expr(sql string, args ...interface{}) expr { |
| return expr{sql: sql, args: args} |
| } |
| |
| func (e expr) ToSql() (sql string, args []interface{}, err error) { |
| return e.sql, e.args, nil |
| } |
| |
| type concatExpr []interface{} |
| |
| func (ce concatExpr) ToSql() (sql string, args []interface{}, err error) { |
| for _, part := range ce { |
| switch p := part.(type) { |
| case string: |
| sql += p |
| case Sqlizer: |
| pSql, pArgs, err := p.ToSql() |
| if err != nil { |
| return "", nil, err |
| } |
| sql += pSql |
| args = append(args, pArgs...) |
| default: |
| return "", nil, fmt.Errorf("%#v is not a string or Sqlizer", part) |
| } |
| } |
| return |
| } |
| |
| // ConcatExpr builds an expression by concatenating strings and other expressions. |
| // |
| // Ex: |
| // name_expr := Expr("CONCAT(?, ' ', ?)", firstName, lastName) |
| // ConcatExpr("COALESCE(full_name,", name_expr, ")") |
| func ConcatExpr(parts ...interface{}) concatExpr { |
| return concatExpr(parts) |
| } |
| |
| type exprs []expr |
| |
| func (es exprs) AppendToSql(w io.Writer, sep string, args []interface{}) ([]interface{}, error) { |
| for i, e := range es { |
| if i > 0 { |
| _, err := io.WriteString(w, sep) |
| if err != nil { |
| return nil, err |
| } |
| } |
| _, err := io.WriteString(w, e.sql) |
| if err != nil { |
| return nil, err |
| } |
| args = append(args, e.args...) |
| } |
| return args, nil |
| } |
| |
| // aliasExpr helps to alias part of SQL query generated with underlying "expr" |
| type aliasExpr struct { |
| expr Sqlizer |
| alias string |
| } |
| |
| // Alias allows to define alias for column in SelectBuilder. Useful when column is |
| // defined as complex expression like IF or CASE |
| // Ex: |
| // .Column(Alias(caseStmt, "case_column")) |
| func Alias(expr Sqlizer, alias string) aliasExpr { |
| return aliasExpr{expr, alias} |
| } |
| |
| func (e aliasExpr) ToSql() (sql string, args []interface{}, err error) { |
| sql, args, err = e.expr.ToSql() |
| if err == nil { |
| sql = fmt.Sprintf("(%s) AS %s", sql, e.alias) |
| } |
| return |
| } |
| |
| // Eq is syntactic sugar for use with Where/Having/Set methods. |
| // Ex: |
| // .Where(Eq{"id": 1}) |
| type Eq map[string]interface{} |
| |
| func (eq Eq) toSQL(useNotOpr bool) (sql string, args []interface{}, err error) { |
| if len(eq) == 0 { |
| // Empty Sql{} evaluates to true. |
| sql = sqlTrue |
| return |
| } |
| |
| var ( |
| exprs []string |
| equalOpr = "=" |
| inOpr = "IN" |
| nullOpr = "IS" |
| inEmptyExpr = sqlFalse |
| ) |
| |
| if useNotOpr { |
| equalOpr = "<>" |
| inOpr = "NOT IN" |
| nullOpr = "IS NOT" |
| inEmptyExpr = sqlTrue |
| } |
| |
| sortedKeys := getSortedKeys(eq) |
| for _, key := range sortedKeys { |
| var expr string |
| val := eq[key] |
| |
| switch v := val.(type) { |
| case driver.Valuer: |
| if val, err = v.Value(); err != nil { |
| return |
| } |
| } |
| |
| r := reflect.ValueOf(val) |
| if r.Kind() == reflect.Ptr { |
| if r.IsNil() { |
| val = nil |
| } else { |
| val = r.Elem().Interface() |
| } |
| } |
| |
| if val == nil { |
| expr = fmt.Sprintf("%s %s NULL", key, nullOpr) |
| } else { |
| if isListType(val) { |
| valVal := reflect.ValueOf(val) |
| if valVal.Len() == 0 { |
| expr = inEmptyExpr |
| if args == nil { |
| args = []interface{}{} |
| } |
| } else { |
| for i := 0; i < valVal.Len(); i++ { |
| args = append(args, valVal.Index(i).Interface()) |
| } |
| expr = fmt.Sprintf("%s %s (%s)", key, inOpr, Placeholders(valVal.Len())) |
| } |
| } else { |
| expr = fmt.Sprintf("%s %s ?", key, equalOpr) |
| args = append(args, val) |
| } |
| } |
| exprs = append(exprs, expr) |
| } |
| sql = strings.Join(exprs, " AND ") |
| return |
| } |
| |
| func (eq Eq) ToSql() (sql string, args []interface{}, err error) { |
| return eq.toSQL(false) |
| } |
| |
| // NotEq is syntactic sugar for use with Where/Having/Set methods. |
| // Ex: |
| // .Where(NotEq{"id": 1}) == "id <> 1" |
| type NotEq Eq |
| |
| func (neq NotEq) ToSql() (sql string, args []interface{}, err error) { |
| return Eq(neq).toSQL(true) |
| } |
| |
| // Like is syntactic sugar for use with LIKE conditions. |
| // Ex: |
| // .Where(Like{"name": "%irrel"}) |
| type Like map[string]interface{} |
| |
| func (lk Like) toSql(opr string) (sql string, args []interface{}, err error) { |
| var exprs []string |
| for key, val := range lk { |
| expr := "" |
| |
| switch v := val.(type) { |
| case driver.Valuer: |
| if val, err = v.Value(); err != nil { |
| return |
| } |
| } |
| |
| if val == nil { |
| err = fmt.Errorf("cannot use null with like operators") |
| return |
| } else { |
| if isListType(val) { |
| err = fmt.Errorf("cannot use array or slice with like operators") |
| return |
| } else { |
| expr = fmt.Sprintf("%s %s ?", key, opr) |
| args = append(args, val) |
| } |
| } |
| exprs = append(exprs, expr) |
| } |
| sql = strings.Join(exprs, " AND ") |
| return |
| } |
| |
| func (lk Like) ToSql() (sql string, args []interface{}, err error) { |
| return lk.toSql("LIKE") |
| } |
| |
| // NotLike is syntactic sugar for use with LIKE conditions. |
| // Ex: |
| // .Where(NotLike{"name": "%irrel"}) |
| type NotLike Like |
| |
| func (nlk NotLike) ToSql() (sql string, args []interface{}, err error) { |
| return Like(nlk).toSql("NOT LIKE") |
| } |
| |
| // ILike is syntactic sugar for use with ILIKE conditions. |
| // Ex: |
| // .Where(ILike{"name": "sq%"}) |
| type ILike Like |
| |
| func (ilk ILike) ToSql() (sql string, args []interface{}, err error) { |
| return Like(ilk).toSql("ILIKE") |
| } |
| |
| // NotILike is syntactic sugar for use with ILIKE conditions. |
| // Ex: |
| // .Where(NotILike{"name": "sq%"}) |
| type NotILike Like |
| |
| func (nilk NotILike) ToSql() (sql string, args []interface{}, err error) { |
| return Like(nilk).toSql("NOT ILIKE") |
| } |
| |
| // Lt is syntactic sugar for use with Where/Having/Set methods. |
| // Ex: |
| // .Where(Lt{"id": 1}) |
| type Lt map[string]interface{} |
| |
| func (lt Lt) toSql(opposite, orEq bool) (sql string, args []interface{}, err error) { |
| var ( |
| exprs []string |
| opr = "<" |
| ) |
| |
| if opposite { |
| opr = ">" |
| } |
| |
| if orEq { |
| opr = fmt.Sprintf("%s%s", opr, "=") |
| } |
| |
| sortedKeys := getSortedKeys(lt) |
| for _, key := range sortedKeys { |
| var expr string |
| val := lt[key] |
| |
| switch v := val.(type) { |
| case driver.Valuer: |
| if val, err = v.Value(); err != nil { |
| return |
| } |
| } |
| |
| if val == nil { |
| err = fmt.Errorf("cannot use null with less than or greater than operators") |
| return |
| } |
| if isListType(val) { |
| err = fmt.Errorf("cannot use array or slice with less than or greater than operators") |
| return |
| } |
| expr = fmt.Sprintf("%s %s ?", key, opr) |
| args = append(args, val) |
| |
| exprs = append(exprs, expr) |
| } |
| sql = strings.Join(exprs, " AND ") |
| return |
| } |
| |
| func (lt Lt) ToSql() (sql string, args []interface{}, err error) { |
| return lt.toSql(false, false) |
| } |
| |
| // LtOrEq is syntactic sugar for use with Where/Having/Set methods. |
| // Ex: |
| // .Where(LtOrEq{"id": 1}) == "id <= 1" |
| type LtOrEq Lt |
| |
| func (ltOrEq LtOrEq) ToSql() (sql string, args []interface{}, err error) { |
| return Lt(ltOrEq).toSql(false, true) |
| } |
| |
| // Gt is syntactic sugar for use with Where/Having/Set methods. |
| // Ex: |
| // .Where(Gt{"id": 1}) == "id > 1" |
| type Gt Lt |
| |
| func (gt Gt) ToSql() (sql string, args []interface{}, err error) { |
| return Lt(gt).toSql(true, false) |
| } |
| |
| // GtOrEq is syntactic sugar for use with Where/Having/Set methods. |
| // Ex: |
| // .Where(GtOrEq{"id": 1}) == "id >= 1" |
| type GtOrEq Lt |
| |
| func (gtOrEq GtOrEq) ToSql() (sql string, args []interface{}, err error) { |
| return Lt(gtOrEq).toSql(true, true) |
| } |
| |
| type conj []Sqlizer |
| |
| func (c conj) join(sep, defaultExpr string) (sql string, args []interface{}, err error) { |
| if len(c) == 0 { |
| return defaultExpr, []interface{}{}, nil |
| } |
| var sqlParts []string |
| for _, sqlizer := range c { |
| partSQL, partArgs, err := sqlizer.ToSql() |
| if err != nil { |
| return "", nil, err |
| } |
| if partSQL != "" { |
| sqlParts = append(sqlParts, partSQL) |
| args = append(args, partArgs...) |
| } |
| } |
| if len(sqlParts) > 0 { |
| sql = fmt.Sprintf("(%s)", strings.Join(sqlParts, sep)) |
| } |
| return |
| } |
| |
| // And conjunction Sqlizers |
| type And conj |
| |
| func (a And) ToSql() (string, []interface{}, error) { |
| return conj(a).join(" AND ", sqlTrue) |
| } |
| |
| // Or conjunction Sqlizers |
| type Or conj |
| |
| func (o Or) ToSql() (string, []interface{}, error) { |
| return conj(o).join(" OR ", sqlFalse) |
| } |
| |
| func getSortedKeys(exp map[string]interface{}) []string { |
| sortedKeys := make([]string, 0, len(exp)) |
| for k := range exp { |
| sortedKeys = append(sortedKeys, k) |
| } |
| sort.Strings(sortedKeys) |
| return sortedKeys |
| } |
| |
| func isListType(val interface{}) bool { |
| if driver.IsValue(val) { |
| return false |
| } |
| valVal := reflect.ValueOf(val) |
| return valVal.Kind() == reflect.Array || valVal.Kind() == reflect.Slice |
| } |