Skip to content

Commit a0af5e4

Browse files
committed
support multiple tables in a WITH clause
1 parent 5588d0a commit a0af5e4

File tree

5 files changed

+162
-54
lines changed

5 files changed

+162
-54
lines changed

cte.go

Lines changed: 21 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@ package sqlbuilder
66
const (
77
cteMarkerInit injectionMarker = iota
88
cteMarkerAfterWith
9-
cteMarkerAfterAs
109
)
1110

1211
// With creates a new CTE builder with default flavor.
13-
func With(name string, cols ...string) *CTEBuilder {
14-
return DefaultFlavor.NewCTEBuilder().With(name, cols...)
12+
func With(tables ...*CTETableBuilder) *CTEBuilder {
13+
return DefaultFlavor.NewCTEBuilder().With(tables...)
1514
}
1615

1716
func newCTEBuilder() *CTEBuilder {
@@ -23,9 +22,8 @@ func newCTEBuilder() *CTEBuilder {
2322

2423
// CTEBuilder is a CTE (Common Table Expression) builder.
2524
type CTEBuilder struct {
26-
name string
27-
cols []string
28-
builderVar string
25+
tableNames []string
26+
tableBuilderVars []string
2927

3028
args *Args
3129

@@ -36,17 +34,18 @@ type CTEBuilder struct {
3634
var _ Builder = new(CTEBuilder)
3735

3836
// With sets the CTE name and columns.
39-
func (cteb *CTEBuilder) With(name string, cols ...string) *CTEBuilder {
40-
cteb.name = name
41-
cteb.cols = cols
42-
cteb.marker = cteMarkerAfterWith
43-
return cteb
44-
}
37+
func (cteb *CTEBuilder) With(tables ...*CTETableBuilder) *CTEBuilder {
38+
tableNames := make([]string, 0, len(tables))
39+
tableBuilderVars := make([]string, 0, len(tables))
4540

46-
// As sets the builder to select data.
47-
func (cteb *CTEBuilder) As(builder Builder) *CTEBuilder {
48-
cteb.builderVar = cteb.args.Add(builder)
49-
cteb.marker = cteMarkerAfterAs
41+
for _, table := range tables {
42+
tableNames = append(tableNames, table.TableName())
43+
tableBuilderVars = append(tableBuilderVars, cteb.args.Add(table))
44+
}
45+
46+
cteb.tableNames = tableNames
47+
cteb.tableBuilderVars = tableBuilderVars
48+
cteb.marker = cteMarkerAfterWith
5049
return cteb
5150
}
5251

@@ -72,27 +71,12 @@ func (cteb *CTEBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
7271
buf := newStringBuilder()
7372
cteb.injection.WriteTo(buf, cteMarkerInit)
7473

75-
if cteb.name != "" {
74+
if len(cteb.tableBuilderVars) > 0 {
7675
buf.WriteLeadingString("WITH ")
77-
buf.WriteString(cteb.name)
78-
79-
if len(cteb.cols) > 0 {
80-
buf.WriteLeadingString("(")
81-
buf.WriteStrings(cteb.cols, ", ")
82-
buf.WriteString(")")
83-
}
84-
85-
cteb.injection.WriteTo(buf, cteMarkerAfterWith)
86-
}
87-
88-
if cteb.builderVar != "" {
89-
buf.WriteLeadingString("AS (")
90-
buf.WriteString(cteb.builderVar)
91-
buf.WriteRune(')')
92-
93-
cteb.injection.WriteTo(buf, cteMarkerAfterAs)
76+
buf.WriteStrings(cteb.tableBuilderVars, ", ")
9477
}
9578

79+
cteb.injection.WriteTo(buf, cteMarkerAfterWith)
9680
return cteb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
9781
}
9882

@@ -103,18 +87,13 @@ func (cteb *CTEBuilder) SetFlavor(flavor Flavor) (old Flavor) {
10387
return
10488
}
10589

106-
// Var returns a placeholder for value.
107-
func (cteb *CTEBuilder) Var(arg interface{}) string {
108-
return cteb.args.Add(arg)
109-
}
110-
11190
// SQL adds an arbitrary sql to current position.
11291
func (cteb *CTEBuilder) SQL(sql string) *CTEBuilder {
11392
cteb.injection.SQL(cteb.marker, sql)
11493
return cteb
11594
}
11695

117-
// TableName returns the CTE table name.
118-
func (cteb *CTEBuilder) TableName() string {
119-
return cteb.name
96+
// TableNames returns all table names in a CTE.
97+
func (cteb *CTEBuilder) TableNames() []string {
98+
return cteb.tableNames
12099
}

cte_test.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,33 @@ import (
1111
)
1212

1313
func ExampleWith() {
14-
sb := With("users", "id", "name").As(
15-
Select("id", "name").From("users").Where("name IS NOT NULL"),
16-
).Select("users.id", "orders.id").Join("orders", "users.id = orders.user_id")
14+
sb := With(
15+
CTETable("users", "id", "name").As(
16+
Select("id", "name").From("users").Where("name IS NOT NULL"),
17+
),
18+
CTETable("devices").As(
19+
Select("device_id").From("devices"),
20+
),
21+
).Select("users.id", "orders.id", "devices.device_id").Join(
22+
"orders",
23+
"users.id = orders.user_id",
24+
"devices.device_id = orders.device_id",
25+
)
1726

1827
fmt.Println(sb)
1928

2029
// Output:
21-
// WITH users (id, name) AS (SELECT id, name FROM users WHERE name IS NOT NULL) SELECT users.id, orders.id FROM users JOIN orders ON users.id = orders.user_id
30+
// WITH users (id, name) AS (SELECT id, name FROM users WHERE name IS NOT NULL), devices AS (SELECT device_id FROM devices) SELECT users.id, orders.id, devices.device_id FROM users, devices JOIN orders ON users.id = orders.user_id AND devices.device_id = orders.device_id
2231
}
2332

2433
func ExampleCTEBuilder() {
2534
usersBuilder := Select("id", "name", "level").From("users")
2635
usersBuilder.Where(
2736
usersBuilder.GreaterEqualThan("level", 10),
2837
)
29-
cteb := With("valid_users").As(usersBuilder)
38+
cteb := With(
39+
CTETable("valid_users").As(usersBuilder),
40+
)
3041
fmt.Println(cteb)
3142

3243
sb := Select("valid_users.id", "valid_users.name", "orders.id").With(cteb)
@@ -49,17 +60,22 @@ func ExampleCTEBuilder() {
4960
func TestCTEBuilder(t *testing.T) {
5061
a := assert.New(t)
5162
cteb := newCTEBuilder()
63+
ctetb := newCTETableBuilder()
5264
cteb.SQL("/* init */")
53-
cteb.With("t", "a", "b")
65+
cteb.With(ctetb)
5466
cteb.SQL("/* after with */")
5567

56-
// Make sure that calling Var() will not affect the As().
57-
cteb.Var(123)
68+
ctetb.SQL("/* table init */")
69+
ctetb.Table("t", "a", "b")
70+
ctetb.SQL("/* after table */")
5871

59-
cteb.As(Select("a", "b").From("t"))
60-
cteb.SQL("/* after as */")
72+
ctetb.As(Select("a", "b").From("t"))
73+
ctetb.SQL("/* after table as */")
6174

6275
sql, args := cteb.Build()
63-
a.Equal(sql, "/* init */ WITH t (a, b) /* after with */ AS (SELECT a, b FROM t) /* after as */")
76+
a.Equal(sql, "/* init */ WITH /* table init */ t (a, b) /* after table */ AS (SELECT a, b FROM t) /* after table as */ /* after with */")
6477
a.Assert(args == nil)
78+
79+
sql = ctetb.String()
80+
a.Equal(sql, "/* table init */ t (a, b) /* after table */ AS (SELECT a, b FROM t) /* after table as */")
6581
}

ctetable.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2024 Huan Du. All rights reserved.
2+
// Licensed under the MIT license that can be found in the LICENSE file.
3+
4+
package sqlbuilder
5+
6+
const (
7+
cteTableMarkerInit injectionMarker = iota
8+
cteTableMarkerAfterTable
9+
cteTableMarkerAfterAs
10+
)
11+
12+
// CTETable creates a new CTE table builder with default flavor.
13+
func CTETable(name string, cols ...string) *CTETableBuilder {
14+
return DefaultFlavor.NewCTETableBuilder().Table(name, cols...)
15+
}
16+
17+
func newCTETableBuilder() *CTETableBuilder {
18+
return &CTETableBuilder{
19+
args: &Args{},
20+
injection: newInjection(),
21+
}
22+
}
23+
24+
// CTETableBuilder is a builder to build one table in CTE (Common Table Expression).
25+
type CTETableBuilder struct {
26+
name string
27+
cols []string
28+
builderVar string
29+
30+
args *Args
31+
32+
injection *injection
33+
marker injectionMarker
34+
}
35+
36+
// Table sets the table name and columns in a CTE table.
37+
func (ctetb *CTETableBuilder) Table(name string, cols ...string) *CTETableBuilder {
38+
ctetb.name = name
39+
ctetb.cols = cols
40+
ctetb.marker = cteTableMarkerAfterTable
41+
return ctetb
42+
}
43+
44+
// As sets the builder to select data.
45+
func (ctetb *CTETableBuilder) As(builder Builder) *CTETableBuilder {
46+
ctetb.builderVar = ctetb.args.Add(builder)
47+
ctetb.marker = cteTableMarkerAfterAs
48+
return ctetb
49+
}
50+
51+
// String returns the compiled CTE string.
52+
func (ctetb *CTETableBuilder) String() string {
53+
sql, _ := ctetb.Build()
54+
return sql
55+
}
56+
57+
// Build returns compiled CTE string and args.
58+
func (ctetb *CTETableBuilder) Build() (sql string, args []interface{}) {
59+
return ctetb.BuildWithFlavor(ctetb.args.Flavor)
60+
}
61+
62+
// BuildWithFlavor builds a CTE with the specified flavor and initial arguments.
63+
func (ctetb *CTETableBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
64+
buf := newStringBuilder()
65+
ctetb.injection.WriteTo(buf, cteTableMarkerInit)
66+
67+
if ctetb.name != "" {
68+
buf.WriteLeadingString(ctetb.name)
69+
70+
if len(ctetb.cols) > 0 {
71+
buf.WriteLeadingString("(")
72+
buf.WriteStrings(ctetb.cols, ", ")
73+
buf.WriteString(")")
74+
}
75+
76+
ctetb.injection.WriteTo(buf, cteTableMarkerAfterTable)
77+
}
78+
79+
if ctetb.builderVar != "" {
80+
buf.WriteLeadingString("AS (")
81+
buf.WriteString(ctetb.builderVar)
82+
buf.WriteRune(')')
83+
84+
ctetb.injection.WriteTo(buf, cteTableMarkerAfterAs)
85+
}
86+
87+
return ctetb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
88+
}
89+
90+
// SetFlavor sets the flavor of compiled sql.
91+
func (ctetb *CTETableBuilder) SetFlavor(flavor Flavor) (old Flavor) {
92+
old = ctetb.args.Flavor
93+
ctetb.args.Flavor = flavor
94+
return
95+
}
96+
97+
// SQL adds an arbitrary sql to current position.
98+
func (ctetb *CTETableBuilder) SQL(sql string) *CTETableBuilder {
99+
ctetb.injection.SQL(ctetb.marker, sql)
100+
return ctetb
101+
}
102+
103+
// TableName returns the CTE table name.
104+
func (ctetb *CTETableBuilder) TableName() string {
105+
return ctetb.name
106+
}

flavor.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,13 @@ func (f Flavor) NewCTEBuilder() *CTEBuilder {
148148
return b
149149
}
150150

151+
// NewCTETableBuilder creates a new CTE table builder with flavor.
152+
func (f Flavor) NewCTETableBuilder() *CTETableBuilder {
153+
b := newCTETableBuilder()
154+
b.SetFlavor(f)
155+
return b
156+
}
157+
151158
// Quote adds quote for name to make sure the name can be used safely
152159
// as table name or field name.
153160
//

select.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func Select(col ...string) *SelectBuilder {
9898
func (sb *SelectBuilder) With(builder *CTEBuilder) *SelectBuilder {
9999
sb.marker = selectMarkerAfterWith
100100
sb.cteBuilder = sb.Var(builder)
101-
sb.tables = []string{builder.TableName()}
101+
sb.tables = builder.TableNames()
102102
return sb
103103
}
104104

0 commit comments

Comments
 (0)