Skip to content

Commit 01acaab

Browse files
committed
fix #163: CTE API refactory; see issue for details
1 parent 8cd72ce commit 01acaab

File tree

13 files changed

+343
-148
lines changed

13 files changed

+343
-148
lines changed

cte.go

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ const (
99
)
1010

1111
// With creates a new CTE builder with default flavor.
12-
func With(tables ...*CTETableBuilder) *CTEBuilder {
12+
func With(tables ...*CTEQueryBuilder) *CTEBuilder {
1313
return DefaultFlavor.NewCTEBuilder().With(tables...)
1414
}
1515

1616
// WithRecursive creates a new recursive CTE builder with default flavor.
17-
func WithRecursive(tables ...*CTETableBuilder) *CTEBuilder {
17+
func WithRecursive(tables ...*CTEQueryBuilder) *CTEBuilder {
1818
return DefaultFlavor.NewCTEBuilder().WithRecursive(tables...)
1919
}
2020

@@ -28,8 +28,8 @@ func newCTEBuilder() *CTEBuilder {
2828
// CTEBuilder is a CTE (Common Table Expression) builder.
2929
type CTEBuilder struct {
3030
recursive bool
31-
tableNames []string
32-
tableBuilderVars []string
31+
queries []*CTEQueryBuilder
32+
queryBuilderVars []string
3333

3434
args *Args
3535

@@ -40,24 +40,22 @@ type CTEBuilder struct {
4040
var _ Builder = new(CTEBuilder)
4141

4242
// With sets the CTE name and columns.
43-
func (cteb *CTEBuilder) With(tables ...*CTETableBuilder) *CTEBuilder {
44-
tableNames := make([]string, 0, len(tables))
45-
tableBuilderVars := make([]string, 0, len(tables))
43+
func (cteb *CTEBuilder) With(queries ...*CTEQueryBuilder) *CTEBuilder {
44+
queryBuilderVars := make([]string, 0, len(queries))
4645

47-
for _, table := range tables {
48-
tableNames = append(tableNames, table.TableName())
49-
tableBuilderVars = append(tableBuilderVars, cteb.args.Add(table))
46+
for _, query := range queries {
47+
queryBuilderVars = append(queryBuilderVars, cteb.args.Add(query))
5048
}
5149

52-
cteb.tableNames = tableNames
53-
cteb.tableBuilderVars = tableBuilderVars
50+
cteb.queries = queries
51+
cteb.queryBuilderVars = queryBuilderVars
5452
cteb.marker = cteMarkerAfterWith
5553
return cteb
5654
}
5755

5856
// WithRecursive sets the CTE name and columns and turns on the RECURSIVE keyword.
59-
func (cteb *CTEBuilder) WithRecursive(tables ...*CTETableBuilder) *CTEBuilder {
60-
cteb.With(tables...).recursive = true
57+
func (cteb *CTEBuilder) WithRecursive(queries ...*CTEQueryBuilder) *CTEBuilder {
58+
cteb.With(queries...).recursive = true
6159
return cteb
6260
}
6361

@@ -67,6 +65,18 @@ func (cteb *CTEBuilder) Select(col ...string) *SelectBuilder {
6765
return sb.With(cteb).Select(col...)
6866
}
6967

68+
// DeleteFrom creates a new DeleteBuilder to build a DELETE statement using this CTE.
69+
func (cteb *CTEBuilder) DeleteFrom(table string) *DeleteBuilder {
70+
db := cteb.args.Flavor.NewDeleteBuilder()
71+
return db.With(cteb).DeleteFrom(table)
72+
}
73+
74+
// Update creates a new UpdateBuilder to build an UPDATE statement using this CTE.
75+
func (cteb *CTEBuilder) Update(table string) *UpdateBuilder {
76+
ub := cteb.args.Flavor.NewUpdateBuilder()
77+
return ub.With(cteb).Update(table)
78+
}
79+
7080
// String returns the compiled CTE string.
7181
func (cteb *CTEBuilder) String() string {
7282
sql, _ := cteb.Build()
@@ -83,12 +93,12 @@ func (cteb *CTEBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
8393
buf := newStringBuilder()
8494
cteb.injection.WriteTo(buf, cteMarkerInit)
8595

86-
if len(cteb.tableBuilderVars) > 0 {
96+
if len(cteb.queryBuilderVars) > 0 {
8797
buf.WriteLeadingString("WITH ")
8898
if cteb.recursive {
8999
buf.WriteString("RECURSIVE ")
90100
}
91-
buf.WriteStrings(cteb.tableBuilderVars, ", ")
101+
buf.WriteStrings(cteb.queryBuilderVars, ", ")
92102
}
93103

94104
cteb.injection.WriteTo(buf, cteMarkerAfterWith)
@@ -110,5 +120,43 @@ func (cteb *CTEBuilder) SQL(sql string) *CTEBuilder {
110120

111121
// TableNames returns all table names in a CTE.
112122
func (cteb *CTEBuilder) TableNames() []string {
113-
return cteb.tableNames
123+
if len(cteb.queryBuilderVars) == 0 {
124+
return nil
125+
}
126+
127+
tableNames := make([]string, 0, len(cteb.queries))
128+
129+
for _, query := range cteb.queries {
130+
tableNames = append(tableNames, query.TableName())
131+
}
132+
133+
return tableNames
134+
}
135+
136+
// tableNamesForSelect returns a list of table names which should be automatically added to FROM clause.
137+
// It's not public, as this feature is designed only for SelectBuilder right now.
138+
func (cteb *CTEBuilder) tableNamesForSelect() []string {
139+
cnt := 0
140+
141+
// It's rare that the ShouldAddToTableList() returns true.
142+
// Count it before allocating any memory for better performance.
143+
for _, query := range cteb.queries {
144+
if query.ShouldAddToTableList() {
145+
cnt++
146+
}
147+
}
148+
149+
if cnt == 0 {
150+
return nil
151+
}
152+
153+
tableNames := make([]string, 0, cnt)
154+
155+
for _, query := range cteb.queries {
156+
if query.ShouldAddToTableList() {
157+
tableNames = append(tableNames, query.TableName())
158+
}
159+
}
160+
161+
return tableNames
114162
}

cte_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func ExampleWith() {
3232

3333
func ExampleWithRecursive() {
3434
sb := WithRecursive(
35-
CTETable("source_accounts", "id", "parent_id").As(
35+
CTEQuery("source_accounts", "id", "parent_id").As(
3636
UnionAll(
3737
Select("p.id", "p.parent_id").
3838
From("accounts AS p").
@@ -85,7 +85,7 @@ func ExampleCTEBuilder() {
8585
func TestCTEBuilder(t *testing.T) {
8686
a := assert.New(t)
8787
cteb := newCTEBuilder()
88-
ctetb := newCTETableBuilder()
88+
ctetb := newCTEQueryBuilder()
8989
cteb.SQL("/* init */")
9090
cteb.With(ctetb)
9191
cteb.SQL("/* after with */")
@@ -97,6 +97,8 @@ func TestCTEBuilder(t *testing.T) {
9797
ctetb.As(Select("a", "b").From("t"))
9898
ctetb.SQL("/* after table as */")
9999

100+
a.Equal(cteb.TableNames(), []string{ctetb.TableName()})
101+
100102
sql, args := cteb.Build()
101103
a.Equal(sql, "/* init */ WITH /* table init */ t (a, b) /* after table */ AS (SELECT a, b FROM t) /* after table as */ /* after with */")
102104
a.Assert(args == nil)
@@ -109,7 +111,7 @@ func TestRecursiveCTEBuilder(t *testing.T) {
109111
a := assert.New(t)
110112
cteb := newCTEBuilder()
111113
cteb.recursive = true
112-
ctetb := newCTETableBuilder()
114+
ctetb := newCTEQueryBuilder()
113115
cteb.SQL("/* init */")
114116
cteb.With(ctetb)
115117
cteb.SQL("/* after with */")

ctequery.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
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+
cteQueryMarkerInit injectionMarker = iota
8+
cteQueryMarkerAfterTable
9+
cteQueryMarkerAfterAs
10+
)
11+
12+
// CTETable creates a new CTE query builder with default flavor, marking it as a table.
13+
//
14+
// The resulting CTE query can be used in a `SelectBuilder“, where its table name will be
15+
// automatically included in the FROM clause.
16+
func CTETable(name string, cols ...string) *CTEQueryBuilder {
17+
return DefaultFlavor.NewCTEQueryBuilder().AddToTableList().Table(name, cols...)
18+
}
19+
20+
// CTEQuery creates a new CTE query builder with default flavor.
21+
func CTEQuery(name string, cols ...string) *CTEQueryBuilder {
22+
return DefaultFlavor.NewCTEQueryBuilder().Table(name, cols...)
23+
}
24+
25+
func newCTEQueryBuilder() *CTEQueryBuilder {
26+
return &CTEQueryBuilder{
27+
args: &Args{},
28+
injection: newInjection(),
29+
}
30+
}
31+
32+
// CTEQueryBuilder is a builder to build one table in CTE (Common Table Expression).
33+
type CTEQueryBuilder struct {
34+
name string
35+
cols []string
36+
builderVar string
37+
38+
// if true, this query's table name will be automatically added to the table list
39+
// in FROM clause of SELECT statement.
40+
autoAddToTableList bool
41+
42+
args *Args
43+
44+
injection *injection
45+
marker injectionMarker
46+
}
47+
48+
var _ Builder = new(CTEQueryBuilder)
49+
50+
// CTETableBuilder is an alias of CTEQueryBuilder for backward compatibility.
51+
// Deprecated: use CTEQueryBuilder instead.
52+
type CTETableBuilder = CTEQueryBuilder
53+
54+
// Table sets the table name and columns in a CTE table.
55+
func (ctetb *CTEQueryBuilder) Table(name string, cols ...string) *CTEQueryBuilder {
56+
ctetb.name = name
57+
ctetb.cols = cols
58+
ctetb.marker = cteQueryMarkerAfterTable
59+
return ctetb
60+
}
61+
62+
// As sets the builder to select data.
63+
func (ctetb *CTEQueryBuilder) As(builder Builder) *CTEQueryBuilder {
64+
ctetb.builderVar = ctetb.args.Add(builder)
65+
ctetb.marker = cteQueryMarkerAfterAs
66+
return ctetb
67+
}
68+
69+
// AddToTableList sets flag to add table name to table list in FROM clause of SELECT statement.
70+
func (ctetb *CTEQueryBuilder) AddToTableList() *CTEQueryBuilder {
71+
ctetb.autoAddToTableList = true
72+
return ctetb
73+
}
74+
75+
// ShouldAddToTableList returns flag to add table name to table list in FROM clause of SELECT statement.
76+
func (ctetb *CTEQueryBuilder) ShouldAddToTableList() bool {
77+
return ctetb.autoAddToTableList
78+
}
79+
80+
// String returns the compiled CTE string.
81+
func (ctetb *CTEQueryBuilder) String() string {
82+
sql, _ := ctetb.Build()
83+
return sql
84+
}
85+
86+
// Build returns compiled CTE string and args.
87+
func (ctetb *CTEQueryBuilder) Build() (sql string, args []interface{}) {
88+
return ctetb.BuildWithFlavor(ctetb.args.Flavor)
89+
}
90+
91+
// BuildWithFlavor builds a CTE with the specified flavor and initial arguments.
92+
func (ctetb *CTEQueryBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
93+
buf := newStringBuilder()
94+
ctetb.injection.WriteTo(buf, cteQueryMarkerInit)
95+
96+
if ctetb.name != "" {
97+
buf.WriteLeadingString(ctetb.name)
98+
99+
if len(ctetb.cols) > 0 {
100+
buf.WriteLeadingString("(")
101+
buf.WriteStrings(ctetb.cols, ", ")
102+
buf.WriteString(")")
103+
}
104+
105+
ctetb.injection.WriteTo(buf, cteQueryMarkerAfterTable)
106+
}
107+
108+
if ctetb.builderVar != "" {
109+
buf.WriteLeadingString("AS (")
110+
buf.WriteString(ctetb.builderVar)
111+
buf.WriteRune(')')
112+
113+
ctetb.injection.WriteTo(buf, cteQueryMarkerAfterAs)
114+
}
115+
116+
return ctetb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
117+
}
118+
119+
// SetFlavor sets the flavor of compiled sql.
120+
func (ctetb *CTEQueryBuilder) SetFlavor(flavor Flavor) (old Flavor) {
121+
old = ctetb.args.Flavor
122+
ctetb.args.Flavor = flavor
123+
return
124+
}
125+
126+
// SQL adds an arbitrary sql to current position.
127+
func (ctetb *CTEQueryBuilder) SQL(sql string) *CTEQueryBuilder {
128+
ctetb.injection.SQL(ctetb.marker, sql)
129+
return ctetb
130+
}
131+
132+
// TableName returns the CTE table name.
133+
func (ctetb *CTEQueryBuilder) TableName() string {
134+
return ctetb.name
135+
}

0 commit comments

Comments
 (0)