Skip to content

Commit 77d3e53

Browse files
committed
fix #211: Support clone (deep copy) in all builders
1 parent 55936be commit 77d3e53

18 files changed

+407
-10
lines changed

args.go

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ package sqlbuilder
66
import (
77
"database/sql"
88
"fmt"
9+
"reflect"
910
"sort"
1011
"strconv"
1112
"strings"
13+
14+
"github.com/huandu/go-clone"
1215
)
1316

1417
// Args stores arguments associated with a SQL.
@@ -17,7 +20,7 @@ type Args struct {
1720
Flavor Flavor
1821

1922
indexBase int
20-
argValues []interface{}
23+
argValues *valueStore
2124
namedArgs map[string]int
2225
sqlNamedArgs map[string]int
2326
onlyNamed bool
@@ -48,7 +51,7 @@ func (args *Args) Add(arg interface{}) string {
4851
}
4952

5053
func (args *Args) add(arg interface{}) int {
51-
idx := len(args.argValues) + args.indexBase
54+
idx := args.argValues.Len() + args.indexBase
5255

5356
switch a := arg.(type) {
5457
case sql.NamedArg:
@@ -57,7 +60,7 @@ func (args *Args) add(arg interface{}) int {
5760
}
5861

5962
if p, ok := args.sqlNamedArgs[a.Name]; ok {
60-
arg = args.argValues[p]
63+
arg = args.argValues.Load(p)
6164
break
6265
}
6366

@@ -68,7 +71,7 @@ func (args *Args) add(arg interface{}) int {
6871
}
6972

7073
if p, ok := args.namedArgs[a.name]; ok {
71-
arg = args.argValues[p]
74+
arg = args.argValues.Load(p)
7275
break
7376
}
7477

@@ -78,10 +81,31 @@ func (args *Args) add(arg interface{}) int {
7881
return idx
7982
}
8083

81-
args.argValues = append(args.argValues, arg)
84+
if args.argValues == nil {
85+
args.argValues = &valueStore{}
86+
}
87+
88+
args.argValues.Add(arg)
8289
return idx
8390
}
8491

92+
// Replace replaces the placeholder with arg.
93+
//
94+
// The placeholder must be the value returned by `Add`, e.g. "$1".
95+
// If the placeholder is not found, this method does nothing.
96+
func (args *Args) Replace(placeholder string, arg interface{}) {
97+
dollar := strings.IndexRune(placeholder, '$')
98+
99+
if dollar != 0 {
100+
return
101+
}
102+
103+
if i, err := strconv.Atoi(placeholder[1:]); err == nil {
104+
i -= args.indexBase
105+
args.argValues.Set(i, arg)
106+
}
107+
}
108+
85109
// Compile compiles builder's format to standard sql and returns associated args.
86110
//
87111
// The format string uses a special syntax to represent arguments.
@@ -201,14 +225,14 @@ func (args *Args) compileDigits(ctx *argsCompileContext, format string, offset i
201225
}
202226

203227
func (args *Args) compileSuccessive(ctx *argsCompileContext, format string, offset int) (string, int) {
204-
if offset < 0 || offset >= len(args.argValues) {
228+
if offset < 0 || offset >= args.argValues.Len() {
205229
ctx.WriteString("/* INVALID ARG $")
206230
ctx.WriteString(strconv.Itoa(offset))
207231
ctx.WriteString(" */")
208232
return format, offset
209233
}
210234

211-
arg := args.argValues[offset]
235+
arg := args.argValues.Load(offset)
212236
ctx.WriteValue(arg)
213237

214238
return format, offset + 1
@@ -245,7 +269,7 @@ func (args *Args) mergeSQLNamedArgs(ctx *argsCompileContext) []interface{} {
245269
sort.Ints(ints)
246270

247271
for _, i := range ints {
248-
values = append(values, args.argValues[i])
272+
values = append(values, args.argValues.Load(i))
249273
}
250274

251275
return values
@@ -364,3 +388,50 @@ func (ctx *argsCompileContext) WriteValues(values []interface{}, sep string) {
364388
ctx.WriteValue(v)
365389
}
366390
}
391+
392+
type valueStore struct {
393+
Values []interface{}
394+
}
395+
396+
func init() {
397+
// The values in valueStore should be shadow-copied to avoid unnecessary cost.
398+
t := reflect.TypeOf(valueStore{})
399+
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
400+
values := old.FieldByName("Values")
401+
newValues := allocator.Clone(values)
402+
new.FieldByName("Values").Set(newValues)
403+
})
404+
}
405+
406+
func (as *valueStore) Len() int {
407+
if as == nil {
408+
return 0
409+
}
410+
411+
return len(as.Values)
412+
}
413+
414+
// Add adds an arg to argsValues and returns its index.
415+
func (as *valueStore) Add(arg interface{}) int {
416+
as.Values = append(as.Values, arg)
417+
return len(as.Values) - 1
418+
}
419+
420+
// Set sets the arg value by index.
421+
func (as *valueStore) Set(index int, arg interface{}) {
422+
if as == nil || index < 0 || index >= len(as.Values) {
423+
return
424+
}
425+
426+
as.Values[index] = arg
427+
}
428+
429+
// Load returns the arg value by index.
430+
// Returns nil if index is out of range or as itself is nil.
431+
func (as *valueStore) Load(index int) interface{} {
432+
if as == nil || index < 0 || index >= len(as.Values) {
433+
return nil
434+
}
435+
436+
return as.Values[index]
437+
}

createtable.go

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

66
import (
77
"strings"
8+
9+
"github.com/huandu/go-clone"
810
)
911

1012
const (
@@ -29,6 +31,12 @@ func newCreateTableBuilder() *CreateTableBuilder {
2931
}
3032
}
3133

34+
// Clone returns a deep copy of CreateTableBuilder.
35+
// It's useful when you want to create a base builder and clone it to build similar queries.
36+
func (ctb *CreateTableBuilder) Clone() *CreateTableBuilder {
37+
return clone.Clone(ctb).(*CreateTableBuilder)
38+
}
39+
3240
// CreateTableBuilder is a builder to build CREATE TABLE.
3341
type CreateTableBuilder struct {
3442
verb string

createtable_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,22 @@ func TestCreateTableGetFlavor(t *testing.T) {
102102
flavor = ctbClick.Flavor()
103103
a.Equal(ClickHouse, flavor)
104104
}
105+
106+
func TestCreateTableClone(t *testing.T) {
107+
a := assert.New(t)
108+
ctb := CreateTable("demo.user").IfNotExists().
109+
Define("id", "BIGINT(20)", "NOT NULL", "AUTO_INCREMENT", "PRIMARY KEY", `COMMENT "user id"`).
110+
Option("DEFAULT CHARACTER SET", "utf8mb4")
111+
112+
ctb2 := ctb.Clone()
113+
ctb2.Define("name", "VARCHAR(255)", "NOT NULL", `COMMENT "user name"`)
114+
115+
sql1, args1 := ctb.Build()
116+
sql2, args2 := ctb2.Build()
117+
118+
a.Equal("CREATE TABLE IF NOT EXISTS demo.user (id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT \"user id\") DEFAULT CHARACTER SET utf8mb4", sql1)
119+
a.Equal(0, len(args1))
120+
121+
a.Equal("CREATE TABLE IF NOT EXISTS demo.user (id BIGINT(20) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT \"user id\", name VARCHAR(255) NOT NULL COMMENT \"user name\") DEFAULT CHARACTER SET utf8mb4", sql2)
122+
a.Equal(0, len(args2))
123+
}

cte.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
package sqlbuilder
55

6+
import (
7+
"reflect"
8+
9+
"github.com/huandu/go-clone"
10+
)
11+
612
const (
713
cteMarkerInit injectionMarker = iota
814
cteMarkerAfterWith
@@ -25,6 +31,25 @@ func newCTEBuilder() *CTEBuilder {
2531
}
2632
}
2733

34+
// Clone returns a deep copy of CTEBuilder.
35+
// It's useful when you want to create a base builder and clone it to build similar queries.
36+
func (cteb *CTEBuilder) Clone() *CTEBuilder {
37+
return clone.Clone(cteb).(*CTEBuilder)
38+
}
39+
40+
func init() {
41+
t := reflect.TypeOf(CTEBuilder{})
42+
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
43+
cloned := allocator.CloneSlowly(old)
44+
new.Set(cloned)
45+
46+
cteb := cloned.Addr().Interface().(*CTEBuilder)
47+
for i, b := range cteb.queries {
48+
cteb.args.Replace(cteb.queryBuilderVars[i], b)
49+
}
50+
})
51+
}
52+
2853
// CTEBuilder is a CTE (Common Table Expression) builder.
2954
type CTEBuilder struct {
3055
recursive bool
@@ -47,7 +72,7 @@ func (cteb *CTEBuilder) With(queries ...*CTEQueryBuilder) *CTEBuilder {
4772
queryBuilderVars = append(queryBuilderVars, cteb.args.Add(query))
4873
}
4974

50-
cteb.queries = queries
75+
cteb.queries = append([]*CTEQueryBuilder(nil), queries...)
5176
cteb.queryBuilderVars = queryBuilderVars
5277
cteb.marker = cteMarkerAfterWith
5378
return cteb

cte_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,53 @@ func TestCTEQueryBuilderGetFlavor(t *testing.T) {
193193
flavor = ctetbClick.Flavor()
194194
a.Equal(ClickHouse, flavor)
195195
}
196+
197+
func TestCTEBuildClone(t *testing.T) {
198+
a := assert.New(t)
199+
cteb := With(
200+
CTETable("users", "id", "name").As(
201+
Select("id", "name").From("users").Where("name IS NOT NULL"),
202+
),
203+
)
204+
ctebClone := cteb.Clone()
205+
a.Equal(cteb.String(), ctebClone.String())
206+
207+
sb := Select("users.id", "users.name", "orders.id").From("users").With(cteb)
208+
sbClone := sb.Clone()
209+
a.Equal(sb.String(), sbClone.String())
210+
211+
sql, args := sb.Build()
212+
sqlClone, argsClone := sbClone.Build()
213+
a.Equal(sql, sqlClone)
214+
a.Equal(args, argsClone)
215+
216+
sb.Limit(20)
217+
a.NotEqual(sb.String(), sbClone.String())
218+
}
219+
220+
func TestCTEQueryBuilderClone(t *testing.T) {
221+
a := assert.New(t)
222+
ctetb := CTETable("users", "id", "name").As(Select("id", "name").From("users").Where("id > 0"))
223+
// Ensure AddToTableList flag is respected in clone as default for CTETable
224+
clone := ctetb.Clone()
225+
a.Equal(ctetb.String(), clone.String())
226+
a.Equal(ctetb.ShouldAddToTableList(), clone.ShouldAddToTableList())
227+
}
228+
229+
func TestCTEBuilderClone(t *testing.T) {
230+
a := assert.New(t)
231+
q1 := CTETable("u", "id").As(Select("id").From("users"))
232+
q2 := CTEQuery("o", "id").As(Select("id").From("orders"))
233+
cte := With(q1, q2)
234+
clone := cte.Clone()
235+
236+
s1, args1 := cte.Build()
237+
s2, args2 := clone.Build()
238+
a.Equal(s1, s2)
239+
a.Equal(args1, args2)
240+
241+
// mutate clone and verify original unchanged
242+
q3 := CTETable("p", "id").As(Select("id").From("profiles"))
243+
clone.With(q3)
244+
a.NotEqual(cte.String(), clone.String())
245+
}

ctequery.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
package sqlbuilder
55

6+
import (
7+
"reflect"
8+
9+
"github.com/huandu/go-clone"
10+
)
11+
612
const (
713
cteQueryMarkerInit injectionMarker = iota
814
cteQueryMarkerAfterTable
@@ -29,10 +35,28 @@ func newCTEQueryBuilder() *CTEQueryBuilder {
2935
}
3036
}
3137

38+
// Clone returns a deep copy of CTEQueryBuilder.
39+
// It's useful when you want to create a base builder and clone it to build similar queries.
40+
func (ctetb *CTEQueryBuilder) Clone() *CTEQueryBuilder {
41+
return clone.Clone(ctetb).(*CTEQueryBuilder)
42+
}
43+
44+
func init() {
45+
t := reflect.TypeOf(CTEQueryBuilder{})
46+
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
47+
cloned := allocator.CloneSlowly(old)
48+
new.Set(cloned)
49+
50+
ctetb := cloned.Addr().Interface().(*CTEQueryBuilder)
51+
ctetb.args.Replace(ctetb.builderVar, ctetb.builder)
52+
})
53+
}
54+
3255
// CTEQueryBuilder is a builder to build one table in CTE (Common Table Expression).
3356
type CTEQueryBuilder struct {
3457
name string
3558
cols []string
59+
builder Builder
3660
builderVar string
3761

3862
// if true, this query's table name will be automatically added to the table list
@@ -62,6 +86,7 @@ func (ctetb *CTEQueryBuilder) Table(name string, cols ...string) *CTEQueryBuilde
6286

6387
// As sets the builder to select data.
6488
func (ctetb *CTEQueryBuilder) As(builder Builder) *CTEQueryBuilder {
89+
ctetb.builder = builder
6590
ctetb.builderVar = ctetb.args.Add(builder)
6691
ctetb.marker = cteQueryMarkerAfterAs
6792
return ctetb

delete.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
package sqlbuilder
55

6+
import (
7+
"reflect"
8+
9+
"github.com/huandu/go-clone"
10+
)
11+
612
const (
713
deleteMarkerInit injectionMarker = iota
814
deleteMarkerAfterWith
@@ -33,6 +39,24 @@ func newDeleteBuilder() *DeleteBuilder {
3339
}
3440
}
3541

42+
// Clone returns a deep copy of DeleteBuilder.
43+
// It's useful when you want to create a base builder and clone it to build similar queries.
44+
func (db *DeleteBuilder) Clone() *DeleteBuilder {
45+
return clone.Clone(db).(*DeleteBuilder)
46+
}
47+
48+
func init() {
49+
t := reflect.TypeOf(DeleteBuilder{})
50+
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
51+
cloned := allocator.CloneSlowly(old)
52+
new.Set(cloned)
53+
54+
db := cloned.Addr().Interface().(*DeleteBuilder)
55+
db.args.Replace(db.whereClauseExpr, db.whereClauseProxy)
56+
db.args.Replace(db.cteBuilderVar, db.cteBuilder)
57+
})
58+
}
59+
3660
// DeleteBuilder is a builder to build DELETE.
3761
type DeleteBuilder struct {
3862
*WhereClause

0 commit comments

Comments
 (0)