feature: new builder WhereClause dedicated for WHERE

This commit is contained in:
Huan Du
2024-04-13 23:32:24 +08:00
parent 8fc213eb7b
commit 96e64e9b7d
16 changed files with 560 additions and 86 deletions
+37 -6
View File
@@ -10,6 +10,7 @@
- [Basic usage](#basic-usage)
- [Pre-defined SQL builders](#pre-defined-sql-builders)
- [Build `WHERE` clause](#build-where-clause)
- [Share `WHERE` clause among builders](#share-where-clause-among-builders)
- [Build SQL for different systems](#build-sql-for-different-systems)
- [Using `Struct` as a light weight ORM](#using-struct-as-a-light-weight-orm)
- [Nested SQL](#nested-sql)
@@ -138,12 +139,12 @@ fmt.Println(args)
There are many methods for building conditions.
- [Cond.Equal](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Equal)/[Cond.E](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.E): `field = value`.
- [Cond.NotEqual](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotEqual)/[Cond.NE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NE): `field <> value`.
- [Cond.GreaterThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterThan)/[Cond.G](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.G): `field > value`.
- [Cond.GreaterEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterEqualThan)/[Cond.GE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GE): `field >= value`.
- [Cond.LessThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessThan)/[Cond.L](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.L): `field < value`.
- [Cond.LessEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessEqualThan)/[Cond.LE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LE): `field <= value`.
- [Cond.Equal](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Equal)/[Cond.E](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.E)/[Cond.EQ](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.EQ): `field = value`.
- [Cond.NotEqual](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotEqual)/[Cond.NE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NE)/[Cond.NEQ](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NEQ): `field <> value`.
- [Cond.GreaterThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterThan)/[Cond.G](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.G)/[Cond.GT](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GT): `field > value`.
- [Cond.GreaterEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterEqualThan)/[Cond.GE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GE)/[Cond.GTE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GTE): `field >= value`.
- [Cond.LessThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessThan)/[Cond.L](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.L)/[Cond.LT](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LT): `field < value`.
- [Cond.LessEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessEqualThan)/[Cond.LE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LE)/[Cond.LTE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LTE): `field <= value`.
- [Cond.In](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.In): `field IN (value1, value2, ...)`.
- [Cond.NotIn](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotIn): `field NOT IN (value1, value2, ...)`.
- [Cond.Like](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Like): `field LIKE value`.
@@ -164,6 +165,36 @@ There are also some methods to combine conditions.
- [Cond.And](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.And): Combine conditions with `AND` operator.
- [Cond.Or](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Or): Combine conditions with `OR` operator.
### Share `WHERE` clause among builders
Due to the importance of the `WHERE` statement in SQL, we often need to continuously append conditions and even share some common `WHERE` conditions among different builders. Therefore, we abstract the `WHERE` statement into a `WhereClause` struct, which can be used to create reusable `WHERE` conditions.
Here is a sample to show how to copy `WHERE` clause from a `SelectBuilder` to an `UpdateBuilder`.
```go
// Build a SQL to select a user from database.
sb := Select("name", "level").From("users")
sb.Where(
sb.Equal("id", 1234),
)
fmt.Println(sb)
ub := Update("users")
ub.Set(
ub.Add("level", 10),
)
// Set the WHERE clause of UPDATE to the WHERE clause of SELECT.
ub.WhereClause = sb.WhereClause
fmt.Println(ub)
// Output:
// SELECT name, level FROM users WHERE id = ?
// UPDATE users SET level = level + ? WHERE id = ?
```
The `WhereClause` is not thread-safe. Read samples for [WhereClause](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#WhereClause) to learn how to use it correctly.
### Build SQL for different systems
SQL syntax and parameter marks vary in different systems. In this package, we introduce a concept called "flavor" to smooth out these difference.
+65 -17
View File
@@ -141,22 +141,7 @@ func (args *Args) CompileWithFlavor(format string, flavor Flavor, initialValue .
}
query = buf.String()
if len(args.sqlNamedArgs) > 0 {
// Stabilize the sequence to make it easier to write test cases.
ints := make([]int, 0, len(args.sqlNamedArgs))
for _, p := range args.sqlNamedArgs {
ints = append(ints, p)
}
sort.Ints(ints)
for _, i := range ints {
values = append(values, args.args[i])
}
}
values = args.mergeSQLNamedArgs(values)
return
}
@@ -253,8 +238,71 @@ func (args *Args) compileArg(buf *stringBuilder, flavor Flavor, values []interfa
panic(fmt.Errorf("Args.CompileWithFlavor: invalid flavor %v (%v)", flavor, int(flavor)))
}
values = append(values, arg)
namedValues := parseNamedArgs(values)
if n := len(namedValues); n == 0 {
values = append(values, arg)
} else {
index := len(values) - n
values = append(values[:index+1], namedValues...)
values[index] = arg
}
}
return values
}
func (args *Args) mergeSQLNamedArgs(values []interface{}) []interface{} {
if len(args.sqlNamedArgs) == 0 {
return values
}
namedValues := parseNamedArgs(values)
existingNames := make(map[string]struct{}, len(namedValues))
for _, v := range namedValues {
if a, ok := v.(sql.NamedArg); ok {
existingNames[a.Name] = struct{}{}
}
}
// Stabilize the sequence to make it easier to write test cases.
ints := make([]int, 0, len(args.sqlNamedArgs))
for n, p := range args.sqlNamedArgs {
if _, ok := existingNames[n]; ok {
continue
}
ints = append(ints, p)
}
sort.Ints(ints)
for _, i := range ints {
values = append(values, args.args[i])
}
return values
}
func parseNamedArgs(initialValue []interface{}) (namedValues []interface{}) {
if len(initialValue) == 0 {
return nil
}
// sql.NamedArgs must be placed at the end of the initial value.
i := len(initialValue)
for ; i > 0; i-- {
switch initialValue[i-1].(type) {
case sql.NamedArg:
continue
}
break
}
namedValues = initialValue[i:]
return
}
+14 -11
View File
@@ -3,15 +3,18 @@
package sqlbuilder
import (
"strings"
)
// Cond provides several helper methods to build conditions.
type Cond struct {
Args *Args
}
// NewCond returns a new Cond.
func NewCond() *Cond {
return &Cond{
Args: &Args{},
}
}
// Equal represents "field = value".
func (c *Cond) Equal(field string, value interface{}) string {
buf := newStringBuilder()
@@ -137,7 +140,7 @@ func (c *Cond) In(field string, value ...interface{}) string {
buf := newStringBuilder()
buf.WriteString(Escape(field))
buf.WriteString(" IN (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteStrings(vs, ", ")
buf.WriteString(")")
return buf.String()
}
@@ -153,7 +156,7 @@ func (c *Cond) NotIn(field string, value ...interface{}) string {
buf := newStringBuilder()
buf.WriteString(Escape(field))
buf.WriteString(" NOT IN (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteStrings(vs, ", ")
buf.WriteString(")")
return buf.String()
}
@@ -218,7 +221,7 @@ func (c *Cond) NotBetween(field string, lower, upper interface{}) string {
func (c *Cond) Or(orExpr ...string) string {
buf := newStringBuilder()
buf.WriteString("(")
buf.WriteString(strings.Join(orExpr, " OR "))
buf.WriteStrings(orExpr, " OR ")
buf.WriteString(")")
return buf.String()
}
@@ -227,7 +230,7 @@ func (c *Cond) Or(orExpr ...string) string {
func (c *Cond) And(andExpr ...string) string {
buf := newStringBuilder()
buf.WriteString("(")
buf.WriteString(strings.Join(andExpr, " AND "))
buf.WriteStrings(andExpr, " AND ")
buf.WriteString(")")
return buf.String()
}
@@ -263,7 +266,7 @@ func (c *Cond) Any(field, op string, value ...interface{}) string {
buf.WriteString(" ")
buf.WriteString(op)
buf.WriteString(" ANY (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteStrings(vs, ", ")
buf.WriteString(")")
return buf.String()
}
@@ -281,7 +284,7 @@ func (c *Cond) All(field, op string, value ...interface{}) string {
buf.WriteString(" ")
buf.WriteString(op)
buf.WriteString(" ALL (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteStrings(vs, ", ")
buf.WriteString(")")
return buf.String()
}
@@ -299,7 +302,7 @@ func (c *Cond) Some(field, op string, value ...interface{}) string {
buf.WriteString(" ")
buf.WriteString(op)
buf.WriteString(" SOME (")
buf.WriteString(strings.Join(vs, ", "))
buf.WriteStrings(vs, ", ")
buf.WriteString(")")
return buf.String()
}
+1 -1
View File
@@ -131,7 +131,7 @@ func (ctb *CreateTableBuilder) BuildWithFlavor(flavor Flavor, initialArg ...inte
defs = append(defs, strings.Join(def, " "))
}
buf.WriteString(strings.Join(defs, ", "))
buf.WriteStrings(defs, ", ")
buf.WriteRune(')')
ctb.injection.WriteTo(buf, createTableMarkerAfterDefine)
+30 -7
View File
@@ -5,7 +5,6 @@ package sqlbuilder
import (
"strconv"
"strings"
)
const (
@@ -23,7 +22,11 @@ func NewDeleteBuilder() *DeleteBuilder {
func newDeleteBuilder() *DeleteBuilder {
args := &Args{}
proxy := &whereClauseProxy{}
return &DeleteBuilder{
whereClauseProxy: proxy,
whereClauseExpr: args.Add(proxy),
Cond: Cond{
Args: args,
},
@@ -35,10 +38,13 @@ func newDeleteBuilder() *DeleteBuilder {
// DeleteBuilder is a builder to build DELETE.
type DeleteBuilder struct {
*WhereClause
Cond
whereClauseProxy *whereClauseProxy
whereClauseExpr string
table string
whereExprs []string
orderByCols []string
order string
limit int
@@ -65,11 +71,25 @@ func (db *DeleteBuilder) DeleteFrom(table string) *DeleteBuilder {
// Where sets expressions of WHERE in DELETE.
func (db *DeleteBuilder) Where(andExpr ...string) *DeleteBuilder {
db.whereExprs = append(db.whereExprs, andExpr...)
if db.WhereClause == nil {
db.WhereClause = NewWhereClause()
}
db.WhereClause.AddWhereExpr(db.args, andExpr...)
db.marker = deleteMarkerAfterWhere
return db
}
// AddWhereClause adds all clauses in the whereClause to SELECT.
func (db *DeleteBuilder) AddWhereClause(whereClause *WhereClause) *DeleteBuilder {
if db.WhereClause == nil {
db.WhereClause = NewWhereClause()
}
db.WhereClause.AddWhereClause(whereClause)
return db
}
// OrderBy sets columns of ORDER BY in DELETE.
func (db *DeleteBuilder) OrderBy(col ...string) *DeleteBuilder {
db.orderByCols = col
@@ -123,16 +143,19 @@ func (db *DeleteBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
db.injection.WriteTo(buf, deleteMarkerAfterDeleteFrom)
if len(db.whereExprs) > 0 {
buf.WriteLeadingString("WHERE ")
buf.WriteString(strings.Join(db.whereExprs, " AND "))
if db.WhereClause != nil {
db.whereClauseProxy.WhereClause = db.WhereClause
defer func() {
db.whereClauseProxy.WhereClause = nil
}()
buf.WriteLeadingString(db.whereClauseExpr)
db.injection.WriteTo(buf, deleteMarkerAfterWhere)
}
if len(db.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteString(strings.Join(db.orderByCols, ", "))
buf.WriteStrings(db.orderByCols, ", ")
if db.order != "" {
buf.WriteRune(' ')
+2 -6
View File
@@ -3,10 +3,6 @@
package sqlbuilder
import (
"strings"
)
// injection is a helper type to manage injected SQLs in all builders.
type injection struct {
markerSQLs map[injectionMarker][]string
@@ -36,6 +32,6 @@ func (injection *injection) WriteTo(buf *stringBuilder, marker injectionMarker)
return
}
s := strings.Join(sqls, " ")
buf.WriteLeadingString(s)
buf.WriteLeadingString("")
buf.WriteStrings(sqls, " ")
}
+4 -4
View File
@@ -147,7 +147,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
ib.injection.WriteTo(buf, insertMarkerAfterInsertInto)
if len(ib.cols) > 0 {
buf.WriteLeadingString("(")
buf.WriteString(strings.Join(ib.cols, ", "))
buf.WriteStrings(ib.cols, ", ")
buf.WriteString(")")
ib.injection.WriteTo(buf, insertMarkerAfterCols)
@@ -156,7 +156,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
buf.WriteLeadingString("VALUES ")
values := make([]string, 0, len(ib.values))
values = append(values, fmt.Sprintf("(%v)", strings.Join(v, ", ")))
buf.WriteString(strings.Join(values, ", "))
buf.WriteStrings(values, ", ")
}
buf.WriteString(" SELECT 1 from DUAL")
@@ -176,7 +176,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
if len(ib.cols) > 0 {
buf.WriteLeadingString("(")
buf.WriteString(strings.Join(ib.cols, ", "))
buf.WriteStrings(ib.cols, ", ")
buf.WriteString(")")
ib.injection.WriteTo(buf, insertMarkerAfterCols)
@@ -198,7 +198,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
values = append(values, fmt.Sprintf("(%v)", strings.Join(v, ", ")))
}
buf.WriteString(strings.Join(values, ", "))
buf.WriteStrings(values, ", ")
}
ib.injection.WriteTo(buf, insertMarkerAfterValues)
+2 -2
View File
@@ -101,9 +101,9 @@ func Tuple(values ...interface{}) interface{} {
// TupleNames joins names with tuple format.
// The names is not escaped. Use `EscapeAll` to escape them if necessary.
func TupleNames(names ...string) string {
buf := &strings.Builder{}
buf := newStringBuilder()
buf.WriteRune('(')
buf.WriteString(strings.Join(names, ", "))
buf.WriteStrings(names, ", ")
buf.WriteRune(')')
return buf.String()
+41 -15
View File
@@ -42,7 +42,11 @@ func NewSelectBuilder() *SelectBuilder {
func newSelectBuilder() *SelectBuilder {
args := &Args{}
proxy := &whereClauseProxy{}
return &SelectBuilder{
whereClauseProxy: proxy,
whereClauseExpr: args.Add(proxy),
Cond: Cond{
Args: args,
},
@@ -55,15 +59,18 @@ func newSelectBuilder() *SelectBuilder {
// SelectBuilder is a builder to build SELECT.
type SelectBuilder struct {
*WhereClause
Cond
whereClauseProxy *whereClauseProxy
whereClauseExpr string
distinct bool
tables []string
selectCols []string
joinOptions []JoinOption
joinTables []string
joinExprs [][]string
whereExprs []string
havingExprs []string
groupByCols []string
orderByCols []string
@@ -140,11 +147,25 @@ func (sb *SelectBuilder) JoinWithOption(option JoinOption, table string, onExpr
// Where sets expressions of WHERE in SELECT.
func (sb *SelectBuilder) Where(andExpr ...string) *SelectBuilder {
sb.whereExprs = append(sb.whereExprs, andExpr...)
if sb.WhereClause == nil {
sb.WhereClause = NewWhereClause()
}
sb.WhereClause.AddWhereExpr(sb.args, andExpr...)
sb.marker = selectMarkerAfterWhere
return sb
}
// AddWhereClause adds all clauses in the whereClause to SELECT.
func (sb *SelectBuilder) AddWhereClause(whereClause *WhereClause) *SelectBuilder {
if sb.WhereClause == nil {
sb.WhereClause = NewWhereClause()
}
sb.WhereClause.AddWhereClause(whereClause)
return sb
}
// Having sets expressions of HAVING in SELECT.
func (sb *SelectBuilder) Having(andExpr ...string) *SelectBuilder {
sb.havingExprs = append(sb.havingExprs, andExpr...)
@@ -273,15 +294,16 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
var selectCols = make([]string, 0, len(sb.selectCols))
for i := range sb.selectCols {
cols := strings.SplitN(sb.selectCols[i], ".", 2)
if len(cols) == 1 {
selectCols = append(selectCols, cols[0])
} else {
selectCols = append(selectCols, cols[1])
}
}
buf.WriteString(strings.Join(selectCols, ", "))
buf.WriteStrings(selectCols, ", ")
} else {
buf.WriteString(strings.Join(sb.selectCols, ", "))
buf.WriteStrings(sb.selectCols, ", ")
}
}
@@ -297,6 +319,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
var selectCols = make([]string, 0, len(sb.selectCols)+1)
selectCols = append(selectCols, "ROWNUM r")
for i := range sb.selectCols {
cols := strings.SplitN(sb.selectCols[i], ".", 2)
if len(cols) == 1 {
@@ -305,16 +328,16 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
selectCols = append(selectCols, cols[1])
}
}
buf.WriteString(strings.Join(selectCols, ", "))
buf.WriteStrings(selectCols, ", ")
buf.WriteLeadingString("FROM ( SELECT ")
buf.WriteString(strings.Join(sb.selectCols, ", "))
buf.WriteStrings(sb.selectCols, ", ")
}
}
if len(sb.tables) > 0 {
buf.WriteLeadingString("FROM ")
buf.WriteString(strings.Join(sb.tables, ", "))
buf.WriteStrings(sb.tables, ", ")
}
sb.injection.WriteTo(buf, selectMarkerAfterFrom)
@@ -329,7 +352,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
if exprs := sb.joinExprs[i]; len(exprs) > 0 {
buf.WriteString(" ON ")
buf.WriteString(strings.Join(sb.joinExprs[i], " AND "))
buf.WriteStrings(sb.joinExprs[i], " AND ")
}
}
@@ -337,20 +360,23 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
sb.injection.WriteTo(buf, selectMarkerAfterJoin)
}
if len(sb.whereExprs) > 0 {
buf.WriteLeadingString("WHERE ")
buf.WriteString(strings.Join(sb.whereExprs, " AND "))
if sb.WhereClause != nil {
sb.whereClauseProxy.WhereClause = sb.WhereClause
defer func() {
sb.whereClauseProxy.WhereClause = nil
}()
buf.WriteLeadingString(sb.whereClauseExpr)
sb.injection.WriteTo(buf, selectMarkerAfterWhere)
}
if len(sb.groupByCols) > 0 {
buf.WriteLeadingString("GROUP BY ")
buf.WriteString(strings.Join(sb.groupByCols, ", "))
buf.WriteStrings(sb.groupByCols, ", ")
if len(sb.havingExprs) > 0 {
buf.WriteString(" HAVING ")
buf.WriteString(strings.Join(sb.havingExprs, " AND "))
buf.WriteStrings(sb.havingExprs, " AND ")
}
sb.injection.WriteTo(buf, selectMarkerAfterGroupBy)
@@ -358,7 +384,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
if len(sb.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteString(strings.Join(sb.orderByCols, ", "))
buf.WriteStrings(sb.orderByCols, ", ")
if sb.order != "" {
buf.WriteRune(' ')
@@ -422,7 +448,7 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
if oraclePage {
buf.WriteString(" ) ")
if len(sb.tables) > 0 {
buf.WriteString(strings.Join(sb.tables, ", "))
buf.WriteStrings(sb.tables, ", ")
}
min := sb.offset
+10 -3
View File
@@ -61,19 +61,26 @@ func ExampleSelectBuilder_advancedUsage() {
sb := NewSelectBuilder()
innerSb := NewSelectBuilder()
// Named arguments are supported.
start := sql.Named("start", 1234567890)
end := sql.Named("end", 1234599999)
level := sql.Named("level", 20)
sb.Select("id", "name")
sb.From(
sb.BuilderAs(innerSb, "user"),
)
sb.Where(
sb.In("status", Flatten([]int{1, 2, 3})...),
sb.Between("created_at", sql.Named("start", 1234567890), sql.Named("end", 1234599999)),
sb.Between("created_at", start, end),
)
sb.OrderBy("modified_at").Desc()
innerSb.Select("*")
innerSb.From("banned")
innerSb.Where(
innerSb.GreaterThan("level", level),
innerSb.LessEqualThan("updated_at", end),
innerSb.NotIn("name", Flatten([]string{"Huan Du", "Charmy Liu"})...),
)
@@ -82,8 +89,8 @@ func ExampleSelectBuilder_advancedUsage() {
fmt.Println(args)
// Output:
// SELECT id, name FROM (SELECT * FROM banned WHERE name NOT IN (?, ?)) AS user WHERE status IN (?, ?, ?) AND created_at BETWEEN @start AND @end ORDER BY modified_at DESC
// [Huan Du Charmy Liu 1 2 3 {{} start 1234567890} {{} end 1234599999}]
// SELECT id, name FROM (SELECT * FROM banned WHERE level > @level AND updated_at <= @end AND name NOT IN (?, ?)) AS user WHERE status IN (?, ?, ?) AND created_at BETWEEN @start AND @end ORDER BY modified_at DESC
// [Huan Du Charmy Liu 1 2 3 {{} level 20} {{} end 1234599999} {{} start 1234567890}]
}
func ExampleSelectBuilder_join() {
+13
View File
@@ -34,6 +34,19 @@ func (sb *stringBuilder) WriteString(s string) {
sb.builder.WriteString(s)
}
func (sb *stringBuilder) WriteStrings(ss []string, sep string) {
if len(ss) == 0 {
return
}
sb.WriteString(ss[0])
for _, s := range ss[1:] {
sb.WriteString(sep)
sb.WriteString(s)
}
}
func (sb *stringBuilder) WriteRune(r rune) {
sb.builder.WriteRune(r)
}
+1 -2
View File
@@ -5,7 +5,6 @@ package sqlbuilder
import (
"strconv"
"strings"
)
const (
@@ -162,7 +161,7 @@ func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
if len(ub.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteString(strings.Join(ub.orderByCols, ", "))
buf.WriteStrings(ub.orderByCols, ", ")
if ub.order != "" {
buf.WriteRune(' ')
+32 -8
View File
@@ -6,7 +6,6 @@ package sqlbuilder
import (
"fmt"
"strconv"
"strings"
)
const (
@@ -25,7 +24,11 @@ func NewUpdateBuilder() *UpdateBuilder {
func newUpdateBuilder() *UpdateBuilder {
args := &Args{}
proxy := &whereClauseProxy{}
return &UpdateBuilder{
whereClauseProxy: proxy,
whereClauseExpr: args.Add(proxy),
Cond: Cond{
Args: args,
},
@@ -37,11 +40,14 @@ func newUpdateBuilder() *UpdateBuilder {
// UpdateBuilder is a builder to build UPDATE.
type UpdateBuilder struct {
*WhereClause
Cond
whereClauseProxy *whereClauseProxy
whereClauseExpr string
table string
assignments []string
whereExprs []string
orderByCols []string
order string
limit int
@@ -82,11 +88,25 @@ func (ub *UpdateBuilder) SetMore(assignment ...string) *UpdateBuilder {
// Where sets expressions of WHERE in UPDATE.
func (ub *UpdateBuilder) Where(andExpr ...string) *UpdateBuilder {
ub.whereExprs = append(ub.whereExprs, andExpr...)
if ub.WhereClause == nil {
ub.WhereClause = NewWhereClause()
}
ub.WhereClause.AddWhereExpr(ub.args, andExpr...)
ub.marker = updateMarkerAfterWhere
return ub
}
// AddWhereClause adds all clauses in the whereClause to SELECT.
func (ub *UpdateBuilder) AddWhereClause(whereClause *WhereClause) *UpdateBuilder {
if ub.WhereClause == nil {
ub.WhereClause = NewWhereClause()
}
ub.WhereClause.AddWhereClause(whereClause)
return ub
}
// Assign represents SET "field = value" in UPDATE.
func (ub *UpdateBuilder) Assign(field string, value interface{}) string {
return fmt.Sprintf("%s = %s", Escape(field), ub.args.Add(value))
@@ -188,20 +208,24 @@ func (ub *UpdateBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
if len(ub.assignments) > 0 {
buf.WriteLeadingString("SET ")
buf.WriteString(strings.Join(ub.assignments, ", "))
buf.WriteStrings(ub.assignments, ", ")
}
ub.injection.WriteTo(buf, updateMarkerAfterSet)
if len(ub.whereExprs) > 0 {
buf.WriteLeadingString("WHERE ")
buf.WriteString(strings.Join(ub.whereExprs, " AND "))
if ub.WhereClause != nil {
ub.whereClauseProxy.WhereClause = ub.WhereClause
defer func() {
ub.whereClauseProxy.WhereClause = nil
}()
buf.WriteLeadingString(ub.whereClauseExpr)
ub.injection.WriteTo(buf, updateMarkerAfterWhere)
}
if len(ub.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteString(strings.Join(ub.orderByCols, ", "))
buf.WriteStrings(ub.orderByCols, ", ")
if ub.order != "" {
buf.WriteLeadingString(ub.order)
+4 -4
View File
@@ -59,10 +59,10 @@ func TestUpdateAssignments(t *testing.T) {
cases := map[string]func(ub *UpdateBuilder) string{
"f = f + 1|[]": func(ub *UpdateBuilder) string { return ub.Incr("f") },
"f = f - 1|[]": func(ub *UpdateBuilder) string { return ub.Decr("f") },
"f = f + $0|[123]": func(ub *UpdateBuilder) string { return ub.Add("f", 123) },
"f = f - $0|[123]": func(ub *UpdateBuilder) string { return ub.Sub("f", 123) },
"f = f * $0|[123]": func(ub *UpdateBuilder) string { return ub.Mul("f", 123) },
"f = f / $0|[123]": func(ub *UpdateBuilder) string { return ub.Div("f", 123) },
"f = f + $1|[123]": func(ub *UpdateBuilder) string { return ub.Add("f", 123) },
"f = f - $1|[123]": func(ub *UpdateBuilder) string { return ub.Sub("f", 123) },
"f = f * $1|[123]": func(ub *UpdateBuilder) string { return ub.Mul("f", 123) },
"f = f / $1|[123]": func(ub *UpdateBuilder) string { return ub.Div("f", 123) },
}
for expected, f := range cases {
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
// WhereClause is a Builder for WHERE clause.
// All builders which support `WHERE` clause have an anonymous `WhereClause` field,
// in which the conditions are stored.
//
// WhereClause can be shared among multiple builders.
// However, it is not thread-safe.
type WhereClause struct {
flavor Flavor
clauses []clause
}
var _ Builder = new(WhereClause)
// NewWhereClause creates a new WhereClause.
func NewWhereClause() *WhereClause {
return &WhereClause{}
}
// CopyWhereClause creates a copy of the whereClause.
func CopyWhereClause(whereClause *WhereClause) *WhereClause {
clauses := make([]clause, len(whereClause.clauses))
copy(clauses, whereClause.clauses)
return &WhereClause{
flavor: whereClause.flavor,
clauses: clauses,
}
}
type clause struct {
args *Args
andExprs []string
}
func (c *clause) Build(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
buf.WriteStrings(c.andExprs, " AND ")
sql, args = c.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
return
}
// whereClauseProxy is a proxy for WhereClause.
// It's useful when the WhereClause in a build can be changed.
type whereClauseProxy struct {
*WhereClause
}
var _ Builder = new(whereClauseProxy)
// BuildWithFlavor builds a WHERE clause with the specified flavor and initial arguments.
func (wc *WhereClause) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
if len(wc.clauses) == 0 {
return "", nil
}
buf := newStringBuilder()
buf.WriteLeadingString("WHERE ")
sql, args = wc.clauses[0].Build(flavor, initialArg...)
buf.WriteString(sql)
for _, clause := range wc.clauses[1:] {
buf.WriteString(" AND ")
sql, args = clause.Build(flavor, args...)
buf.WriteString(sql)
}
return buf.String(), args
}
// Build returns compiled WHERE clause string and args.
func (wc *WhereClause) Build() (sql string, args []interface{}) {
return wc.BuildWithFlavor(wc.flavor)
}
// SetFlavor sets the flavor of compiled sql.
// When the WhereClause belongs to a builder, the flavor of the builder will be used when building SQL.
func (wc *WhereClause) SetFlavor(flavor Flavor) (old Flavor) {
old = wc.flavor
wc.flavor = flavor
return
}
// AddWhereExpr adds an AND expression to WHERE clause with the specified arguments.
func (wc *WhereClause) AddWhereExpr(args *Args, andExpr ...string) {
// Merge with last clause if possible.
if len(wc.clauses) > 0 {
lastClause := &wc.clauses[len(wc.clauses)-1]
if lastClause.args == args {
lastClause.andExprs = append(lastClause.andExprs, andExpr...)
return
}
}
wc.clauses = append(wc.clauses, clause{
args: args,
andExprs: andExpr,
})
}
// AddWhereClause adds all clauses in the whereClause to the wc.
func (wc *WhereClause) AddWhereClause(whereClause *WhereClause) {
if whereClause == nil {
return
}
wc.clauses = append(wc.clauses, whereClause.clauses...)
}
+190
View File
@@ -0,0 +1,190 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"fmt"
)
func ExampleWhereClause() {
// Build a SQL to select a user from database.
sb := Select("name", "level").From("users")
sb.Where(
sb.Equal("id", 1234),
)
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Query database with the sql and update this user's level...
ub := Update("users")
ub.Set(
ub.Add("level", 10),
)
// The WHERE clause of UPDATE should be the same as the WHERE clause of SELECT.
ub.WhereClause = sb.WhereClause
sql, args = ub.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT name, level FROM users WHERE id = ?
// [1234]
// UPDATE users SET level = level + ? WHERE id = ?
// [10 1234]
}
func ExampleWhereClause_sharedAmongBuilders() {
// A WhereClause can be shared among builders.
// However, as it's not thread-safe, don't use it in a concurrent environment.
sb1 := Select("level").From("users")
sb2 := Select("status").From("users")
// Share the same WhereClause between sb1 and sb2.
whereClause := NewWhereClause()
sb1.WhereClause = whereClause
sb2.WhereClause = whereClause
// The Where method in sb1 and sb2 will update the same WhereClause.
// When we call sb1.Where(), the WHERE clause in sb2 will also be updated.
sb1.Where(
sb1.Like("name", "Charmy%"),
)
// We can get a copy of the WhereClause.
// The copy is independent from the original.
sb3 := Select("name").From("users")
sb3.WhereClause = CopyWhereClause(whereClause)
// Adding more expressions to sb1 and sb2 will not affect sb3.
sb2.Where(
sb2.In("status", 1, 2, 3),
)
// Adding more expressions to sb3 will not affect sb1 and sb2.
sb3.Where(
sb3.GreaterEqualThan("level", 10),
)
sql1, args1 := sb1.Build()
sql2, args2 := sb2.Build()
sql3, args3 := sb3.Build()
fmt.Println(sql1)
fmt.Println(args1)
fmt.Println(sql2)
fmt.Println(args2)
fmt.Println(sql3)
fmt.Println(args3)
// Output:
// SELECT level FROM users WHERE name LIKE ? AND status IN (?, ?, ?)
// [Charmy% 1 2 3]
// SELECT status FROM users WHERE name LIKE ? AND status IN (?, ?, ?)
// [Charmy% 1 2 3]
// SELECT name FROM users WHERE name LIKE ? AND level >= ?
// [Charmy% 10]
}
func ExampleWhereClause_clearWhereClause() {
db := DeleteFrom("users")
db.Where(
db.GreaterThan("level", 10),
)
sql, args := db.Build()
fmt.Println(sql)
fmt.Println(args)
// Clear WHERE clause.
db.WhereClause = nil
sql, args = db.Build()
fmt.Println(sql)
fmt.Println(args)
db.Where(
db.Equal("id", 1234),
)
sql, args = db.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// DELETE FROM users WHERE level > ?
// [10]
// DELETE FROM users
// []
// DELETE FROM users WHERE id = ?
// [1234]
}
func ExampleWhereClause_AddWhereExpr() {
// WhereClause can be used as a standalone builder to build WHERE clause.
// It's recommended to use it with Cond.
whereClause := NewWhereClause()
cond := NewCond()
whereClause.AddWhereExpr(
cond.Args,
cond.In("name", "Charmy", "Huan"),
cond.LessEqualThan("level", 10),
)
// Set the flavor of the WhereClause to PostgreSQL.
whereClause.SetFlavor(PostgreSQL)
sql, args := whereClause.Build()
fmt.Println(sql)
fmt.Println(args)
// Use this WhereClause in another builder.
sb := MySQL.NewSelectBuilder()
sb.Select("name", "level").From("users")
sb.WhereClause = whereClause
// The flavor of sb overrides the flavor of the WhereClause.
sql, args = sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// WHERE name IN ($1, $2) AND level <= $3
// [Charmy Huan 10]
// SELECT name, level FROM users WHERE name IN (?, ?) AND level <= ?
// [Charmy Huan 10]
}
func ExampleWhereClause_AddWhereClause() {
sb := Select("level").From("users")
sb.Where(
sb.Equal("id", 1234),
)
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
ub := Update("users")
ub.Set(
ub.Add("level", 10),
)
// Copy the WHERE clause of sb into ub and add more expressions.
ub.AddWhereClause(sb.WhereClause).Where(
ub.Equal("deleted", 0),
)
sql, args = ub.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT level FROM users WHERE id = ?
// [1234]
// UPDATE users SET level = level + ? WHERE id = ? AND deleted = ?
// [10 1234 0]
}