fix #211: Support clone (deep copy) in all builders

This commit is contained in:
Huan Du
2025-09-17 16:19:09 +08:00
parent 88a656d237
commit 18f7db8816
18 changed files with 407 additions and 10 deletions
+79 -8
View File
@@ -6,9 +6,12 @@ package sqlbuilder
import (
"database/sql"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/huandu/go-clone"
)
// Args stores arguments associated with a SQL.
@@ -17,7 +20,7 @@ type Args struct {
Flavor Flavor
indexBase int
argValues []interface{}
argValues *valueStore
namedArgs map[string]int
sqlNamedArgs map[string]int
onlyNamed bool
@@ -48,7 +51,7 @@ func (args *Args) Add(arg interface{}) string {
}
func (args *Args) add(arg interface{}) int {
idx := len(args.argValues) + args.indexBase
idx := args.argValues.Len() + args.indexBase
switch a := arg.(type) {
case sql.NamedArg:
@@ -57,7 +60,7 @@ func (args *Args) add(arg interface{}) int {
}
if p, ok := args.sqlNamedArgs[a.Name]; ok {
arg = args.argValues[p]
arg = args.argValues.Load(p)
break
}
@@ -68,7 +71,7 @@ func (args *Args) add(arg interface{}) int {
}
if p, ok := args.namedArgs[a.name]; ok {
arg = args.argValues[p]
arg = args.argValues.Load(p)
break
}
@@ -78,10 +81,31 @@ func (args *Args) add(arg interface{}) int {
return idx
}
args.argValues = append(args.argValues, arg)
if args.argValues == nil {
args.argValues = &valueStore{}
}
args.argValues.Add(arg)
return idx
}
// Replace replaces the placeholder with arg.
//
// The placeholder must be the value returned by `Add`, e.g. "$1".
// If the placeholder is not found, this method does nothing.
func (args *Args) Replace(placeholder string, arg interface{}) {
dollar := strings.IndexRune(placeholder, '$')
if dollar != 0 {
return
}
if i, err := strconv.Atoi(placeholder[1:]); err == nil {
i -= args.indexBase
args.argValues.Set(i, arg)
}
}
// Compile compiles builder's format to standard sql and returns associated args.
//
// The format string uses a special syntax to represent arguments.
@@ -201,14 +225,14 @@ func (args *Args) compileDigits(ctx *argsCompileContext, format string, offset i
}
func (args *Args) compileSuccessive(ctx *argsCompileContext, format string, offset int) (string, int) {
if offset < 0 || offset >= len(args.argValues) {
if offset < 0 || offset >= args.argValues.Len() {
ctx.WriteString("/* INVALID ARG $")
ctx.WriteString(strconv.Itoa(offset))
ctx.WriteString(" */")
return format, offset
}
arg := args.argValues[offset]
arg := args.argValues.Load(offset)
ctx.WriteValue(arg)
return format, offset + 1
@@ -245,7 +269,7 @@ func (args *Args) mergeSQLNamedArgs(ctx *argsCompileContext) []interface{} {
sort.Ints(ints)
for _, i := range ints {
values = append(values, args.argValues[i])
values = append(values, args.argValues.Load(i))
}
return values
@@ -364,3 +388,50 @@ func (ctx *argsCompileContext) WriteValues(values []interface{}, sep string) {
ctx.WriteValue(v)
}
}
type valueStore struct {
Values []interface{}
}
func init() {
// The values in valueStore should be shadow-copied to avoid unnecessary cost.
t := reflect.TypeOf(valueStore{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
values := old.FieldByName("Values")
newValues := allocator.Clone(values)
new.FieldByName("Values").Set(newValues)
})
}
func (as *valueStore) Len() int {
if as == nil {
return 0
}
return len(as.Values)
}
// Add adds an arg to argsValues and returns its index.
func (as *valueStore) Add(arg interface{}) int {
as.Values = append(as.Values, arg)
return len(as.Values) - 1
}
// Set sets the arg value by index.
func (as *valueStore) Set(index int, arg interface{}) {
if as == nil || index < 0 || index >= len(as.Values) {
return
}
as.Values[index] = arg
}
// Load returns the arg value by index.
// Returns nil if index is out of range or as itself is nil.
func (as *valueStore) Load(index int) interface{} {
if as == nil || index < 0 || index >= len(as.Values) {
return nil
}
return as.Values[index]
}
+8
View File
@@ -5,6 +5,8 @@ package sqlbuilder
import (
"strings"
"github.com/huandu/go-clone"
)
const (
@@ -29,6 +31,12 @@ func newCreateTableBuilder() *CreateTableBuilder {
}
}
// Clone returns a deep copy of CreateTableBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ctb *CreateTableBuilder) Clone() *CreateTableBuilder {
return clone.Clone(ctb).(*CreateTableBuilder)
}
// CreateTableBuilder is a builder to build CREATE TABLE.
type CreateTableBuilder struct {
verb string
+19
View File
@@ -102,3 +102,22 @@ func TestCreateTableGetFlavor(t *testing.T) {
flavor = ctbClick.Flavor()
a.Equal(ClickHouse, flavor)
}
func TestCreateTableClone(t *testing.T) {
a := assert.New(t)
ctb := CreateTable("demo.user").IfNotExists().
Define("id", "BIGINT(20)", "NOT NULL", "AUTO_INCREMENT", "PRIMARY KEY", `COMMENT "user id"`).
Option("DEFAULT CHARACTER SET", "utf8mb4")
ctb2 := ctb.Clone()
ctb2.Define("name", "VARCHAR(255)", "NOT NULL", `COMMENT "user name"`)
sql1, args1 := ctb.Build()
sql2, args2 := ctb2.Build()
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)
a.Equal(0, len(args1))
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)
a.Equal(0, len(args2))
}
+26 -1
View File
@@ -3,6 +3,12 @@
package sqlbuilder
import (
"reflect"
"github.com/huandu/go-clone"
)
const (
cteMarkerInit injectionMarker = iota
cteMarkerAfterWith
@@ -25,6 +31,25 @@ func newCTEBuilder() *CTEBuilder {
}
}
// Clone returns a deep copy of CTEBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (cteb *CTEBuilder) Clone() *CTEBuilder {
return clone.Clone(cteb).(*CTEBuilder)
}
func init() {
t := reflect.TypeOf(CTEBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
cteb := cloned.Addr().Interface().(*CTEBuilder)
for i, b := range cteb.queries {
cteb.args.Replace(cteb.queryBuilderVars[i], b)
}
})
}
// CTEBuilder is a CTE (Common Table Expression) builder.
type CTEBuilder struct {
recursive bool
@@ -47,7 +72,7 @@ func (cteb *CTEBuilder) With(queries ...*CTEQueryBuilder) *CTEBuilder {
queryBuilderVars = append(queryBuilderVars, cteb.args.Add(query))
}
cteb.queries = queries
cteb.queries = append([]*CTEQueryBuilder(nil), queries...)
cteb.queryBuilderVars = queryBuilderVars
cteb.marker = cteMarkerAfterWith
return cteb
+50
View File
@@ -193,3 +193,53 @@ func TestCTEQueryBuilderGetFlavor(t *testing.T) {
flavor = ctetbClick.Flavor()
a.Equal(ClickHouse, flavor)
}
func TestCTEBuildClone(t *testing.T) {
a := assert.New(t)
cteb := With(
CTETable("users", "id", "name").As(
Select("id", "name").From("users").Where("name IS NOT NULL"),
),
)
ctebClone := cteb.Clone()
a.Equal(cteb.String(), ctebClone.String())
sb := Select("users.id", "users.name", "orders.id").From("users").With(cteb)
sbClone := sb.Clone()
a.Equal(sb.String(), sbClone.String())
sql, args := sb.Build()
sqlClone, argsClone := sbClone.Build()
a.Equal(sql, sqlClone)
a.Equal(args, argsClone)
sb.Limit(20)
a.NotEqual(sb.String(), sbClone.String())
}
func TestCTEQueryBuilderClone(t *testing.T) {
a := assert.New(t)
ctetb := CTETable("users", "id", "name").As(Select("id", "name").From("users").Where("id > 0"))
// Ensure AddToTableList flag is respected in clone as default for CTETable
clone := ctetb.Clone()
a.Equal(ctetb.String(), clone.String())
a.Equal(ctetb.ShouldAddToTableList(), clone.ShouldAddToTableList())
}
func TestCTEBuilderClone(t *testing.T) {
a := assert.New(t)
q1 := CTETable("u", "id").As(Select("id").From("users"))
q2 := CTEQuery("o", "id").As(Select("id").From("orders"))
cte := With(q1, q2)
clone := cte.Clone()
s1, args1 := cte.Build()
s2, args2 := clone.Build()
a.Equal(s1, s2)
a.Equal(args1, args2)
// mutate clone and verify original unchanged
q3 := CTETable("p", "id").As(Select("id").From("profiles"))
clone.With(q3)
a.NotEqual(cte.String(), clone.String())
}
+25
View File
@@ -3,6 +3,12 @@
package sqlbuilder
import (
"reflect"
"github.com/huandu/go-clone"
)
const (
cteQueryMarkerInit injectionMarker = iota
cteQueryMarkerAfterTable
@@ -29,10 +35,28 @@ func newCTEQueryBuilder() *CTEQueryBuilder {
}
}
// Clone returns a deep copy of CTEQueryBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ctetb *CTEQueryBuilder) Clone() *CTEQueryBuilder {
return clone.Clone(ctetb).(*CTEQueryBuilder)
}
func init() {
t := reflect.TypeOf(CTEQueryBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
ctetb := cloned.Addr().Interface().(*CTEQueryBuilder)
ctetb.args.Replace(ctetb.builderVar, ctetb.builder)
})
}
// CTEQueryBuilder is a builder to build one table in CTE (Common Table Expression).
type CTEQueryBuilder struct {
name string
cols []string
builder Builder
builderVar string
// 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
// As sets the builder to select data.
func (ctetb *CTEQueryBuilder) As(builder Builder) *CTEQueryBuilder {
ctetb.builder = builder
ctetb.builderVar = ctetb.args.Add(builder)
ctetb.marker = cteQueryMarkerAfterAs
return ctetb
+24
View File
@@ -3,6 +3,12 @@
package sqlbuilder
import (
"reflect"
"github.com/huandu/go-clone"
)
const (
deleteMarkerInit injectionMarker = iota
deleteMarkerAfterWith
@@ -33,6 +39,24 @@ func newDeleteBuilder() *DeleteBuilder {
}
}
// Clone returns a deep copy of DeleteBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (db *DeleteBuilder) Clone() *DeleteBuilder {
return clone.Clone(db).(*DeleteBuilder)
}
func init() {
t := reflect.TypeOf(DeleteBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
db := cloned.Addr().Interface().(*DeleteBuilder)
db.args.Replace(db.whereClauseExpr, db.whereClauseProxy)
db.args.Replace(db.cteBuilderVar, db.cteBuilder)
})
}
// DeleteBuilder is a builder to build DELETE.
type DeleteBuilder struct {
*WhereClause
+17
View File
@@ -193,3 +193,20 @@ func TestDeleteBuilderReturning(t *testing.T) {
sql, _ = db7.BuildWithFlavor(PostgreSQL)
a.Equal("WITH temp_user AS (SELECT id FROM inactive_users) DELETE FROM user, temp_user WHERE user.id IN (SELECT id FROM temp_user) RETURNING id, deleted_at", sql)
}
func TestDeleteBuilderClone(t *testing.T) {
a := assert.New(t)
cte := With(
CTETable("temp").As(Select("id").From("to_delete")),
)
db := cte.DeleteFrom("target").Where("temp.id = target.id").OrderBy("id").Asc().Limit(3).Returning("id")
clone := db.Clone()
s1, args1 := db.BuildWithFlavor(PostgreSQL)
s2, args2 := clone.BuildWithFlavor(PostgreSQL)
a.Equal(s1, s2)
a.Equal(args1, args2)
clone.Desc().Limit(5)
a.NotEqual(db.String(), clone.String())
}
+1
View File
@@ -4,6 +4,7 @@ go 1.18
require (
github.com/huandu/go-assert v1.1.6
github.com/huandu/go-clone v1.7.3
github.com/huandu/xstrings v1.4.0
)
+2
View File
@@ -2,5 +2,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
github.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ=
github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+8
View File
@@ -6,6 +6,8 @@ package sqlbuilder
import (
"fmt"
"strings"
"github.com/huandu/go-clone"
)
const (
@@ -31,6 +33,12 @@ func newInsertBuilder() *InsertBuilder {
}
}
// Clone returns a deep copy of InsertBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ib *InsertBuilder) Clone() *InsertBuilder {
return clone.Clone(ib).(*InsertBuilder)
}
// InsertBuilder is a builder to build INSERT.
type InsertBuilder struct {
verb string
+18
View File
@@ -317,3 +317,21 @@ func TestIssue200(t *testing.T) {
query, _ := ib.Build()
a.Equal(query, "INSERT INTO table (data) SELECT id, data FROM table WHERE id = $1 ON CONFLICT DO NOTHING")
}
func TestInsertBuilderClone(t *testing.T) {
a := assert.New(t)
ib := InsertInto("demo.user").Cols("id", "name")
ib.Values(1, "A")
clone := ib.Clone()
s1, args1 := ib.Build()
s2, args2 := clone.Build()
a.Equal(s1, s2)
a.Equal(args1, args2)
// mutate clone and verify original unchanged
clone.Values(2, "B")
a.NotEqual(ib.String(), clone.String())
}
+21
View File
@@ -5,7 +5,10 @@ package sqlbuilder
import (
"fmt"
"reflect"
"strings"
"github.com/huandu/go-clone"
)
const (
@@ -55,6 +58,24 @@ func newSelectBuilder() *SelectBuilder {
}
}
// Clone returns a deep copy of SelectBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (sb *SelectBuilder) Clone() *SelectBuilder {
return clone.Clone(sb).(*SelectBuilder)
}
func init() {
t := reflect.TypeOf(SelectBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
sb := cloned.Addr().Interface().(*SelectBuilder)
sb.args.Replace(sb.whereClauseExpr, sb.whereClauseProxy)
sb.args.Replace(sb.cteBuilderVar, sb.cteBuilder)
})
}
// SelectBuilder is a builder to build SELECT.
type SelectBuilder struct {
*WhereClause
+27
View File
@@ -422,3 +422,30 @@ func TestNilPointerWhere(t *testing.T) {
NewSelectBuilder().SQL("$0").Build()
NewSelectBuilder().SQL("$0").BuildWithFlavor(DefaultFlavor)
}
func TestSelectBuilderClone(t *testing.T) {
a := assert.New(t)
cte := With(
CTETable("users").As(
Select("id", "name").From("users").Where("name IS NOT NULL"),
),
)
sb := cte.Select("users.id", "orders.id").From("orders").Where(
"users.id = orders.user_id",
).OrderBy("orders.id").Desc().Limit(10).Offset(2)
// Clone and compare
clone := sb.Clone()
s1, args1 := sb.Build()
s2, args2 := clone.Build()
a.Equal(s1, s2)
a.Equal(args1, args2)
// Mutate clone and ensure original unchanged
clone.Limit(100).Asc()
s1After := sb.String()
s2After := clone.String()
a.NotEqual(s1After, s2After)
}
+26 -1
View File
@@ -5,6 +5,9 @@ package sqlbuilder
import (
"fmt"
"reflect"
"github.com/huandu/go-clone"
)
const (
@@ -31,15 +34,36 @@ func newUnionBuilder() *UnionBuilder {
}
}
// Clone returns a deep copy of UnionBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ub *UnionBuilder) Clone() *UnionBuilder {
return clone.Clone(ub).(*UnionBuilder)
}
func init() {
t := reflect.TypeOf(UnionBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
ub := cloned.Addr().Interface().(*UnionBuilder)
for i, b := range ub.builders {
ub.args.Replace(ub.builderVars[i], b)
}
})
}
// UnionBuilder is a builder to build UNION.
type UnionBuilder struct {
opt string
builderVars []string
orderByCols []string
order string
limitVar string
offsetVar string
builders []Builder
builderVars []string
args *Args
injection *injection
@@ -76,6 +100,7 @@ func (ub *UnionBuilder) union(opt string, builders ...Builder) *UnionBuilder {
}
ub.opt = opt
ub.builders = append([]Builder(nil), builders...)
ub.builderVars = builderVars
ub.marker = unionMarkerAfterUnion
return ub
+18
View File
@@ -245,3 +245,21 @@ func ExampleUnionBuilder_limit_offset() {
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT 1
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id LIMIT 1 OFFSET 1
}
func TestUnionBuilderClone(t *testing.T) {
a := assert.New(t)
sb1 := Select("id", "name").From("users").Where("active = 1")
sb2 := Select("id", "nick").From("profiles").Where("status IN (1,2)")
ub := UnionAll(sb1, sb2).OrderBy("id").Desc().Limit(5).Offset(1)
clone := ub.Clone()
s1, args1 := ub.Build()
s2, args2 := clone.Build()
a.Equal(s1, s2)
a.Equal(args1, args2)
// Change clone and ensure original unchanged
clone.Asc().Limit(10)
a.NotEqual(ub.String(), clone.String())
}
+21
View File
@@ -5,6 +5,9 @@ package sqlbuilder
import (
"fmt"
"reflect"
"github.com/huandu/go-clone"
)
const (
@@ -38,6 +41,24 @@ func newUpdateBuilder() *UpdateBuilder {
}
}
// Clone returns a deep copy of UpdateBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ub *UpdateBuilder) Clone() *UpdateBuilder {
return clone.Clone(ub).(*UpdateBuilder)
}
func init() {
t := reflect.TypeOf(UpdateBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
ub := cloned.Addr().Interface().(*UpdateBuilder)
ub.args.Replace(ub.whereClauseExpr, ub.whereClauseProxy)
ub.args.Replace(ub.cteBuilderVar, ub.cteBuilder)
})
}
// UpdateBuilder is a builder to build UPDATE.
type UpdateBuilder struct {
*WhereClause
+17
View File
@@ -266,3 +266,20 @@ func TestUpdateBuilderReturning(t *testing.T) {
sql, _ = ub7.BuildWithFlavor(PostgreSQL)
a.Equal("WITH temp_user AS (SELECT id FROM active_users) UPDATE user SET status = $1 FROM temp_user WHERE user.id IN (SELECT id FROM temp_user) RETURNING id, status", sql)
}
func TestUpdateBuilderClone(t *testing.T) {
a := assert.New(t)
cte := With(
CTETable("vip").As(Select("user_id").From("vip_users")),
)
ub := cte.Update("orders").Set("discount = 1").Where("orders.user_id = vip.user_id").OrderBy("orders.id").Desc().Limit(2).Returning("orders.id")
clone := ub.Clone()
s1, args1 := ub.BuildWithFlavor(PostgreSQL)
s2, args2 := clone.BuildWithFlavor(PostgreSQL)
a.Equal(s1, s2)
a.Equal(args1, args2)
clone.Asc().Limit(5)
a.NotEqual(ub.String(), clone.String())
}