Skip to content

Commit 1e11326

Browse files
committed
feature: new builder WhereClause dedicated for WHERE
1 parent c35040f commit 1e11326

16 files changed

+560
-86
lines changed

README.md

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
- [Basic usage](#basic-usage)
1111
- [Pre-defined SQL builders](#pre-defined-sql-builders)
1212
- [Build `WHERE` clause](#build-where-clause)
13+
- [Share `WHERE` clause among builders](#share-where-clause-among-builders)
1314
- [Build SQL for different systems](#build-sql-for-different-systems)
1415
- [Using `Struct` as a light weight ORM](#using-struct-as-a-light-weight-orm)
1516
- [Nested SQL](#nested-sql)
@@ -138,12 +139,12 @@ fmt.Println(args)
138139

139140
There are many methods for building conditions.
140141

141-
- [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`.
142-
- [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`.
143-
- [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`.
144-
- [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`.
145-
- [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`.
146-
- [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`.
142+
- [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`.
143+
- [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`.
144+
- [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`.
145+
- [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`.
146+
- [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`.
147+
- [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`.
147148
- [Cond.In](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.In): `field IN (value1, value2, ...)`.
148149
- [Cond.NotIn](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotIn): `field NOT IN (value1, value2, ...)`.
149150
- [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.
164165
- [Cond.And](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.And): Combine conditions with `AND` operator.
165166
- [Cond.Or](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Or): Combine conditions with `OR` operator.
166167

168+
### Share `WHERE` clause among builders
169+
170+
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.
171+
172+
Here is a sample to show how to copy `WHERE` clause from a `SelectBuilder` to an `UpdateBuilder`.
173+
174+
```go
175+
// Build a SQL to select a user from database.
176+
sb := Select("name", "level").From("users")
177+
sb.Where(
178+
sb.Equal("id", 1234),
179+
)
180+
fmt.Println(sb)
181+
182+
ub := Update("users")
183+
ub.Set(
184+
ub.Add("level", 10),
185+
)
186+
187+
// Set the WHERE clause of UPDATE to the WHERE clause of SELECT.
188+
ub.WhereClause = sb.WhereClause
189+
fmt.Println(ub)
190+
191+
// Output:
192+
// SELECT name, level FROM users WHERE id = ?
193+
// UPDATE users SET level = level + ? WHERE id = ?
194+
```
195+
196+
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.
197+
167198
### Build SQL for different systems
168199

169200
SQL syntax and parameter marks vary in different systems. In this package, we introduce a concept called "flavor" to smooth out these difference.

args.go

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -141,22 +141,7 @@ func (args *Args) CompileWithFlavor(format string, flavor Flavor, initialValue .
141141
}
142142

143143
query = buf.String()
144-
145-
if len(args.sqlNamedArgs) > 0 {
146-
// Stabilize the sequence to make it easier to write test cases.
147-
ints := make([]int, 0, len(args.sqlNamedArgs))
148-
149-
for _, p := range args.sqlNamedArgs {
150-
ints = append(ints, p)
151-
}
152-
153-
sort.Ints(ints)
154-
155-
for _, i := range ints {
156-
values = append(values, args.args[i])
157-
}
158-
}
159-
144+
values = args.mergeSQLNamedArgs(values)
160145
return
161146
}
162147

@@ -253,8 +238,71 @@ func (args *Args) compileArg(buf *stringBuilder, flavor Flavor, values []interfa
253238
panic(fmt.Errorf("Args.CompileWithFlavor: invalid flavor %v (%v)", flavor, int(flavor)))
254239
}
255240

256-
values = append(values, arg)
241+
namedValues := parseNamedArgs(values)
242+
243+
if n := len(namedValues); n == 0 {
244+
values = append(values, arg)
245+
} else {
246+
index := len(values) - n
247+
values = append(values[:index+1], namedValues...)
248+
values[index] = arg
249+
}
250+
}
251+
252+
return values
253+
}
254+
255+
func (args *Args) mergeSQLNamedArgs(values []interface{}) []interface{} {
256+
if len(args.sqlNamedArgs) == 0 {
257+
return values
258+
}
259+
260+
namedValues := parseNamedArgs(values)
261+
existingNames := make(map[string]struct{}, len(namedValues))
262+
263+
for _, v := range namedValues {
264+
if a, ok := v.(sql.NamedArg); ok {
265+
existingNames[a.Name] = struct{}{}
266+
}
267+
}
268+
269+
// Stabilize the sequence to make it easier to write test cases.
270+
ints := make([]int, 0, len(args.sqlNamedArgs))
271+
272+
for n, p := range args.sqlNamedArgs {
273+
if _, ok := existingNames[n]; ok {
274+
continue
275+
}
276+
277+
ints = append(ints, p)
278+
}
279+
280+
sort.Ints(ints)
281+
282+
for _, i := range ints {
283+
values = append(values, args.args[i])
257284
}
258285

259286
return values
260287
}
288+
289+
func parseNamedArgs(initialValue []interface{}) (namedValues []interface{}) {
290+
if len(initialValue) == 0 {
291+
return nil
292+
}
293+
294+
// sql.NamedArgs must be placed at the end of the initial value.
295+
i := len(initialValue)
296+
297+
for ; i > 0; i-- {
298+
switch initialValue[i-1].(type) {
299+
case sql.NamedArg:
300+
continue
301+
}
302+
303+
break
304+
}
305+
306+
namedValues = initialValue[i:]
307+
return
308+
}

cond.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33

44
package sqlbuilder
55

6-
import (
7-
"strings"
8-
)
9-
106
// Cond provides several helper methods to build conditions.
117
type Cond struct {
128
Args *Args
139
}
1410

11+
// NewCond returns a new Cond.
12+
func NewCond() *Cond {
13+
return &Cond{
14+
Args: &Args{},
15+
}
16+
}
17+
1518
// Equal represents "field = value".
1619
func (c *Cond) Equal(field string, value interface{}) string {
1720
buf := newStringBuilder()
@@ -137,7 +140,7 @@ func (c *Cond) In(field string, value ...interface{}) string {
137140
buf := newStringBuilder()
138141
buf.WriteString(Escape(field))
139142
buf.WriteString(" IN (")
140-
buf.WriteString(strings.Join(vs, ", "))
143+
buf.WriteStrings(vs, ", ")
141144
buf.WriteString(")")
142145
return buf.String()
143146
}
@@ -153,7 +156,7 @@ func (c *Cond) NotIn(field string, value ...interface{}) string {
153156
buf := newStringBuilder()
154157
buf.WriteString(Escape(field))
155158
buf.WriteString(" NOT IN (")
156-
buf.WriteString(strings.Join(vs, ", "))
159+
buf.WriteStrings(vs, ", ")
157160
buf.WriteString(")")
158161
return buf.String()
159162
}
@@ -218,7 +221,7 @@ func (c *Cond) NotBetween(field string, lower, upper interface{}) string {
218221
func (c *Cond) Or(orExpr ...string) string {
219222
buf := newStringBuilder()
220223
buf.WriteString("(")
221-
buf.WriteString(strings.Join(orExpr, " OR "))
224+
buf.WriteStrings(orExpr, " OR ")
222225
buf.WriteString(")")
223226
return buf.String()
224227
}
@@ -227,7 +230,7 @@ func (c *Cond) Or(orExpr ...string) string {
227230
func (c *Cond) And(andExpr ...string) string {
228231
buf := newStringBuilder()
229232
buf.WriteString("(")
230-
buf.WriteString(strings.Join(andExpr, " AND "))
233+
buf.WriteStrings(andExpr, " AND ")
231234
buf.WriteString(")")
232235
return buf.String()
233236
}
@@ -263,7 +266,7 @@ func (c *Cond) Any(field, op string, value ...interface{}) string {
263266
buf.WriteString(" ")
264267
buf.WriteString(op)
265268
buf.WriteString(" ANY (")
266-
buf.WriteString(strings.Join(vs, ", "))
269+
buf.WriteStrings(vs, ", ")
267270
buf.WriteString(")")
268271
return buf.String()
269272
}
@@ -281,7 +284,7 @@ func (c *Cond) All(field, op string, value ...interface{}) string {
281284
buf.WriteString(" ")
282285
buf.WriteString(op)
283286
buf.WriteString(" ALL (")
284-
buf.WriteString(strings.Join(vs, ", "))
287+
buf.WriteStrings(vs, ", ")
285288
buf.WriteString(")")
286289
return buf.String()
287290
}
@@ -299,7 +302,7 @@ func (c *Cond) Some(field, op string, value ...interface{}) string {
299302
buf.WriteString(" ")
300303
buf.WriteString(op)
301304
buf.WriteString(" SOME (")
302-
buf.WriteString(strings.Join(vs, ", "))
305+
buf.WriteStrings(vs, ", ")
303306
buf.WriteString(")")
304307
return buf.String()
305308
}

createtable.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func (ctb *CreateTableBuilder) BuildWithFlavor(flavor Flavor, initialArg ...inte
131131
defs = append(defs, strings.Join(def, " "))
132132
}
133133

134-
buf.WriteString(strings.Join(defs, ", "))
134+
buf.WriteStrings(defs, ", ")
135135
buf.WriteRune(')')
136136

137137
ctb.injection.WriteTo(buf, createTableMarkerAfterDefine)

delete.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package sqlbuilder
55

66
import (
77
"strconv"
8-
"strings"
98
)
109

1110
const (
@@ -23,7 +22,11 @@ func NewDeleteBuilder() *DeleteBuilder {
2322

2423
func newDeleteBuilder() *DeleteBuilder {
2524
args := &Args{}
25+
proxy := &whereClauseProxy{}
2626
return &DeleteBuilder{
27+
whereClauseProxy: proxy,
28+
whereClauseExpr: args.Add(proxy),
29+
2730
Cond: Cond{
2831
Args: args,
2932
},
@@ -35,10 +38,13 @@ func newDeleteBuilder() *DeleteBuilder {
3538

3639
// DeleteBuilder is a builder to build DELETE.
3740
type DeleteBuilder struct {
41+
*WhereClause
3842
Cond
3943

44+
whereClauseProxy *whereClauseProxy
45+
whereClauseExpr string
46+
4047
table string
41-
whereExprs []string
4248
orderByCols []string
4349
order string
4450
limit int
@@ -65,11 +71,25 @@ func (db *DeleteBuilder) DeleteFrom(table string) *DeleteBuilder {
6571

6672
// Where sets expressions of WHERE in DELETE.
6773
func (db *DeleteBuilder) Where(andExpr ...string) *DeleteBuilder {
68-
db.whereExprs = append(db.whereExprs, andExpr...)
74+
if db.WhereClause == nil {
75+
db.WhereClause = NewWhereClause()
76+
}
77+
78+
db.WhereClause.AddWhereExpr(db.args, andExpr...)
6979
db.marker = deleteMarkerAfterWhere
7080
return db
7181
}
7282

83+
// AddWhereClause adds all clauses in the whereClause to SELECT.
84+
func (db *DeleteBuilder) AddWhereClause(whereClause *WhereClause) *DeleteBuilder {
85+
if db.WhereClause == nil {
86+
db.WhereClause = NewWhereClause()
87+
}
88+
89+
db.WhereClause.AddWhereClause(whereClause)
90+
return db
91+
}
92+
7393
// OrderBy sets columns of ORDER BY in DELETE.
7494
func (db *DeleteBuilder) OrderBy(col ...string) *DeleteBuilder {
7595
db.orderByCols = col
@@ -123,16 +143,19 @@ func (db *DeleteBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
123143

124144
db.injection.WriteTo(buf, deleteMarkerAfterDeleteFrom)
125145

126-
if len(db.whereExprs) > 0 {
127-
buf.WriteLeadingString("WHERE ")
128-
buf.WriteString(strings.Join(db.whereExprs, " AND "))
146+
if db.WhereClause != nil {
147+
db.whereClauseProxy.WhereClause = db.WhereClause
148+
defer func() {
149+
db.whereClauseProxy.WhereClause = nil
150+
}()
129151

152+
buf.WriteLeadingString(db.whereClauseExpr)
130153
db.injection.WriteTo(buf, deleteMarkerAfterWhere)
131154
}
132155

133156
if len(db.orderByCols) > 0 {
134157
buf.WriteLeadingString("ORDER BY ")
135-
buf.WriteString(strings.Join(db.orderByCols, ", "))
158+
buf.WriteStrings(db.orderByCols, ", ")
136159

137160
if db.order != "" {
138161
buf.WriteRune(' ')

injection.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33

44
package sqlbuilder
55

6-
import (
7-
"strings"
8-
)
9-
106
// injection is a helper type to manage injected SQLs in all builders.
117
type injection struct {
128
markerSQLs map[injectionMarker][]string
@@ -36,6 +32,6 @@ func (injection *injection) WriteTo(buf *stringBuilder, marker injectionMarker)
3632
return
3733
}
3834

39-
s := strings.Join(sqls, " ")
40-
buf.WriteLeadingString(s)
35+
buf.WriteLeadingString("")
36+
buf.WriteStrings(sqls, " ")
4137
}

insert.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
147147
ib.injection.WriteTo(buf, insertMarkerAfterInsertInto)
148148
if len(ib.cols) > 0 {
149149
buf.WriteLeadingString("(")
150-
buf.WriteString(strings.Join(ib.cols, ", "))
150+
buf.WriteStrings(ib.cols, ", ")
151151
buf.WriteString(")")
152152

153153
ib.injection.WriteTo(buf, insertMarkerAfterCols)
@@ -156,7 +156,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
156156
buf.WriteLeadingString("VALUES ")
157157
values := make([]string, 0, len(ib.values))
158158
values = append(values, fmt.Sprintf("(%v)", strings.Join(v, ", ")))
159-
buf.WriteString(strings.Join(values, ", "))
159+
buf.WriteStrings(values, ", ")
160160
}
161161

162162
buf.WriteString(" SELECT 1 from DUAL")
@@ -176,7 +176,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
176176

177177
if len(ib.cols) > 0 {
178178
buf.WriteLeadingString("(")
179-
buf.WriteString(strings.Join(ib.cols, ", "))
179+
buf.WriteStrings(ib.cols, ", ")
180180
buf.WriteString(")")
181181

182182
ib.injection.WriteTo(buf, insertMarkerAfterCols)
@@ -198,7 +198,7 @@ func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
198198
values = append(values, fmt.Sprintf("(%v)", strings.Join(v, ", ")))
199199
}
200200

201-
buf.WriteString(strings.Join(values, ", "))
201+
buf.WriteStrings(values, ", ")
202202
}
203203

204204
ib.injection.WriteTo(buf, insertMarkerAfterValues)

0 commit comments

Comments
 (0)