Skip to content

Commit 044cf35

Browse files
authored
Merge pull request #218 from huandu/feature/copy-builder
Fix #211: Support clone (deep copy) in all builders
2 parents 55936be + b69f05d commit 044cf35

21 files changed

+572
-14
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Use `sql.Named` in a builder](#use-sqlnamed-in-a-builder)
1818
- [Argument modifiers](#argument-modifiers)
1919
- [Freestyle builder](#freestyle-builder)
20+
- [Clone builders](#clone-builders)
2021
- [Using special syntax to build SQL](#using-special-syntax-to-build-sql)
2122
- [Interpolate `args` in the `sql`](#interpolate-args-in-the-sql)
2223
- [License](#license)
@@ -381,6 +382,51 @@ fmt.Println(args)
381382
// [1 2]
382383
```
383384

385+
### Clone builders
386+
387+
The `Clone` methods make any builder reusable as a template. You can create a partially initialized builder once (even as a global), then call `Clone()` to get an independent copy to customize per request. This avoids repeated setup while keeping shared templates immutable and safe for concurrent use.
388+
389+
Supported builders with `Clone`:
390+
391+
- [CreateTableBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CreateTableBuilder)
392+
- [CTEBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEBuilder)
393+
- [CTEQueryBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEQueryBuilder)
394+
- [DeleteBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#DeleteBuilder)
395+
- [InsertBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#InsertBuilder)
396+
- [SelectBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#SelectBuilder)
397+
- [UnionBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UnionBuilder)
398+
- [UpdateBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UpdateBuilder)
399+
400+
Example: define a global SELECT template and clone it per call
401+
402+
```go
403+
package yourpkg
404+
405+
import "github.com/huandu/go-sqlbuilder"
406+
407+
// Global template — safe to reuse by cloning.
408+
var baseUserSelect = sqlbuilder.NewSelectBuilder().
409+
Select("id", "name", "email").
410+
From("users").
411+
Where("deleted_at IS NULL")
412+
413+
func ListActiveUsers(limit, offset int) (string, []interface{}) {
414+
sb := baseUserSelect.Clone() // independent copy
415+
sb.OrderBy("id").Asc()
416+
sb.Limit(limit).Offset(offset)
417+
return sb.Build()
418+
}
419+
420+
func GetActiveUserByID(id int64) (string, []interface{}) {
421+
sb := baseUserSelect.Clone() // start from the same template
422+
sb.Where(sb.Equal("id", id))
423+
sb.Limit(1)
424+
return sb.Build()
425+
}
426+
```
427+
428+
The same template pattern applies to other builders. For example, keep a base `UpdateBuilder` with the table and common `SET` clauses, or a base `CTEBuilder` defining reusable CTEs, then `Clone()` and add query-specific `WHERE`/`ORDER BY`/`LIMIT`/`RETURNING` as needed.
429+
384430
### Using special syntax to build SQL
385431

386432
The `sqlbuilder` package incorporates special syntax for representing uncompiled SQL internally. To leverage this syntax for developing customized tools, the `Build` function can be utilized to compile it with the necessary arguments.

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+
}

0 commit comments

Comments
 (0)