mirror of
https://github.com/hanzoai/sqlbuilder.git
synced 2026-10-01 00:37:42 +00:00
fix #211: Support clone (deep copy) in all builders
This commit is contained in:
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user