Skip to content

Commit 55936be

Browse files
authored
Merge pull request #216 from zhangyongding/union-support-limit
fix: union limit
2 parents 4e4c7b0 + 756c9cf commit 55936be

File tree

2 files changed

+261
-7
lines changed

2 files changed

+261
-7
lines changed

union.go

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
package sqlbuilder
55

6+
import (
7+
"fmt"
8+
)
9+
610
const (
711
unionDistinct = " UNION " // Default union type is DISTINCT.
812
unionAll = " UNION ALL "
@@ -140,9 +144,16 @@ func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
140144
buf := newStringBuilder()
141145
ub.injection.WriteTo(buf, unionMarkerInit)
142146

147+
nestedSelect := (flavor == Oracle && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0)) ||
148+
(flavor == Informix && len(ub.limitVar) > 0)
149+
143150
if len(ub.builderVars) > 0 {
144151
needParen := flavor != SQLite
145152

153+
if nestedSelect {
154+
buf.WriteLeadingString("SELECT * FROM (")
155+
}
156+
146157
if needParen {
147158
buf.WriteLeadingString("(")
148159
buf.WriteString(ub.builderVars[0])
@@ -164,6 +175,10 @@ func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
164175
buf.WriteRune(')')
165176
}
166177
}
178+
179+
if nestedSelect {
180+
buf.WriteLeadingString(")")
181+
}
167182
}
168183

169184
ub.injection.WriteTo(buf, unionMarkerAfterUnion)
@@ -180,16 +195,114 @@ func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
180195
ub.injection.WriteTo(buf, unionMarkerAfterOrderBy)
181196
}
182197

183-
if len(ub.limitVar) > 0 {
184-
buf.WriteLeadingString("LIMIT ")
185-
buf.WriteString(ub.limitVar)
198+
switch flavor {
199+
case MySQL, SQLite, ClickHouse:
200+
if len(ub.limitVar) > 0 {
201+
buf.WriteLeadingString("LIMIT ")
202+
buf.WriteString(ub.limitVar)
186203

187-
}
204+
if len(ub.offsetVar) > 0 {
205+
buf.WriteLeadingString("OFFSET ")
206+
buf.WriteString(ub.offsetVar)
207+
}
208+
}
209+
210+
case CQL:
211+
if len(ub.limitVar) > 0 {
212+
buf.WriteLeadingString("LIMIT ")
213+
buf.WriteString(ub.limitVar)
214+
}
215+
216+
case PostgreSQL:
217+
if len(ub.limitVar) > 0 {
218+
buf.WriteLeadingString("LIMIT ")
219+
buf.WriteString(ub.limitVar)
220+
}
221+
222+
if len(ub.offsetVar) > 0 {
223+
buf.WriteLeadingString("OFFSET ")
224+
buf.WriteString(ub.offsetVar)
225+
}
226+
227+
case Presto:
228+
// There might be a hidden constraint in Presto requiring offset to be set before limit.
229+
// The select statement documentation (https://prestodb.io/docs/current/sql/select.html)
230+
// puts offset before limit, and Trino, which is based on Presto, seems
231+
// to require this specific order.
232+
if len(ub.offsetVar) > 0 {
233+
buf.WriteLeadingString("OFFSET ")
234+
buf.WriteString(ub.offsetVar)
235+
}
188236

189-
if ((MySQL == flavor || Informix == flavor) && len(ub.limitVar) > 0) || PostgreSQL == flavor {
237+
if len(ub.limitVar) > 0 {
238+
buf.WriteLeadingString("LIMIT ")
239+
buf.WriteString(ub.limitVar)
240+
}
241+
242+
case SQLServer:
243+
// If ORDER BY is not set, sort column #1 by default.
244+
// It's required to make OFFSET...FETCH work.
245+
if len(ub.orderByCols) == 0 && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0) {
246+
buf.WriteLeadingString("ORDER BY 1")
247+
}
248+
249+
if len(ub.offsetVar) > 0 {
250+
buf.WriteLeadingString("OFFSET ")
251+
buf.WriteString(ub.offsetVar)
252+
buf.WriteString(" ROWS")
253+
}
254+
255+
if len(ub.limitVar) > 0 {
256+
if len(ub.offsetVar) == 0 {
257+
buf.WriteLeadingString("OFFSET 0 ROWS")
258+
}
259+
260+
buf.WriteLeadingString("FETCH NEXT ")
261+
buf.WriteString(ub.limitVar)
262+
buf.WriteString(" ROWS ONLY")
263+
}
264+
265+
case Oracle:
266+
// It's required to make OFFSET...FETCH work.
190267
if len(ub.offsetVar) > 0 {
191268
buf.WriteLeadingString("OFFSET ")
192269
buf.WriteString(ub.offsetVar)
270+
buf.WriteString(" ROWS")
271+
}
272+
273+
if len(ub.limitVar) > 0 {
274+
if len(ub.offsetVar) == 0 {
275+
buf.WriteLeadingString("OFFSET 0 ROWS")
276+
}
277+
278+
buf.WriteLeadingString("FETCH NEXT ")
279+
buf.WriteString(ub.limitVar)
280+
buf.WriteString(" ROWS ONLY")
281+
}
282+
283+
case Informix:
284+
// [SKIP N] FIRST M
285+
// M must be greater than 0
286+
if len(ub.limitVar) > 0 {
287+
if len(ub.offsetVar) > 0 {
288+
buf.WriteLeadingString("SKIP ")
289+
buf.WriteString(ub.offsetVar)
290+
}
291+
292+
buf.WriteLeadingString("FIRST ")
293+
buf.WriteString(ub.limitVar)
294+
}
295+
296+
case Doris:
297+
// #192: Doris doesn't support ? in OFFSET and LIMIT.
298+
if len(ub.limitVar) > 0 {
299+
buf.WriteLeadingString("LIMIT ")
300+
buf.WriteString(fmt.Sprint(ub.args.Value(ub.limitVar)))
301+
302+
if len(ub.offsetVar) > 0 {
303+
buf.WriteLeadingString("OFFSET ")
304+
buf.WriteString(fmt.Sprint(ub.args.Value(ub.offsetVar)))
305+
}
193306
}
194307
}
195308

union_test.go

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ func TestUnionForSQLite(t *testing.T) {
8787
a := assert.New(t)
8888
sb1 := Select("id", "name").From("users").Where("created_at > DATE('now', '-15 days')")
8989
sb2 := Select("id", "nick_name").From("user_extras").Where("status IN (1, 2, 3)")
90-
sql, _ := UnionAll(sb1, sb2).OrderBy("id").BuildWithFlavor(SQLite)
90+
sql, _ := UnionAll(sb1, sb2).OrderBy("id").Limit(100).Offset(5).BuildWithFlavor(SQLite)
9191

92-
a.Equal(sql, "SELECT id, name FROM users WHERE created_at > DATE('now', '-15 days') UNION ALL SELECT id, nick_name FROM user_extras WHERE status IN (1, 2, 3) ORDER BY id")
92+
a.Equal(sql, "SELECT id, name FROM users WHERE created_at > DATE('now', '-15 days') UNION ALL SELECT id, nick_name FROM user_extras WHERE status IN (1, 2, 3) ORDER BY id LIMIT ? OFFSET ?")
9393
}
9494

9595
func TestUnionBuilderGetFlavor(t *testing.T) {
@@ -104,3 +104,144 @@ func TestUnionBuilderGetFlavor(t *testing.T) {
104104
flavor = ubClick.Flavor()
105105
a.Equal(ClickHouse, flavor)
106106
}
107+
108+
func ExampleUnionBuilder_limit_offset() {
109+
flavors := []Flavor{MySQL, PostgreSQL, SQLite, SQLServer, CQL, ClickHouse, Presto, Oracle, Informix, Doris}
110+
results := make([][]string, len(flavors))
111+
112+
ub := NewUnionBuilder()
113+
saveResults := func() {
114+
sb1 := NewSelectBuilder()
115+
sb1.Select("*").From("user1")
116+
sb2 := NewSelectBuilder()
117+
sb2.Select("*").From("user2")
118+
ub.Union(sb1, sb2)
119+
for i, f := range flavors {
120+
s, _ := ub.BuildWithFlavor(f)
121+
results[i] = append(results[i], s)
122+
}
123+
}
124+
125+
// Case #1: limit < 0 and offset < 0
126+
//
127+
// All: No limit or offset in query.
128+
ub.Limit(-1)
129+
ub.Offset(-1)
130+
saveResults()
131+
132+
// Case #2: limit < 0 and offset >= 0
133+
//
134+
// MySQL and SQLite: Ignore offset if the limit is not set.
135+
// PostgreSQL: Offset can be set without limit.
136+
// SQLServer: Offset can be set without limit.
137+
// CQL: Ignore offset.
138+
// Oracle: Offset can be set without limit.
139+
ub.Limit(-1)
140+
ub.Offset(0)
141+
saveResults()
142+
143+
// Case #3: limit >= 0 and offset >= 0
144+
//
145+
// CQL: Ignore offset.
146+
// All others: Set both limit and offset.
147+
ub.Limit(1)
148+
ub.Offset(0)
149+
saveResults()
150+
151+
// Case #4: limit >= 0 and offset < 0
152+
//
153+
// All: Set limit in query.
154+
ub.Limit(1)
155+
ub.Offset(-1)
156+
saveResults()
157+
158+
// Case #5: limit >= 0 and offset >= 0 order by id
159+
//
160+
// CQL: Ignore offset.
161+
// All others: Set both limit and offset.
162+
ub.Limit(1)
163+
ub.Offset(1)
164+
ub.OrderBy("id")
165+
saveResults()
166+
167+
for i, result := range results {
168+
fmt.Println()
169+
fmt.Println(flavors[i])
170+
171+
for n, s := range result {
172+
fmt.Printf("#%d: %s\n", n+1, s)
173+
}
174+
}
175+
176+
// Output:
177+
//
178+
// MySQL
179+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
180+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2)
181+
// #3: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT ? OFFSET ?
182+
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT ?
183+
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id LIMIT ? OFFSET ?
184+
//
185+
// PostgreSQL
186+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
187+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2) OFFSET $1
188+
// #3: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT $1 OFFSET $2
189+
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT $1
190+
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id LIMIT $1 OFFSET $2
191+
//
192+
// SQLite
193+
// #1: SELECT * FROM user1 UNION SELECT * FROM user2
194+
// #2: SELECT * FROM user1 UNION SELECT * FROM user2
195+
// #3: SELECT * FROM user1 UNION SELECT * FROM user2 LIMIT ? OFFSET ?
196+
// #4: SELECT * FROM user1 UNION SELECT * FROM user2 LIMIT ?
197+
// #5: SELECT * FROM user1 UNION SELECT * FROM user2 ORDER BY id LIMIT ? OFFSET ?
198+
//
199+
// SQLServer
200+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
201+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY 1 OFFSET @p1 ROWS
202+
// #3: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY 1 OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY
203+
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY 1 OFFSET 0 ROWS FETCH NEXT @p1 ROWS ONLY
204+
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id OFFSET @p1 ROWS FETCH NEXT @p2 ROWS ONLY
205+
//
206+
// CQL
207+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
208+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2)
209+
// #3: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT ?
210+
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT ?
211+
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id LIMIT ?
212+
//
213+
// ClickHouse
214+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
215+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2)
216+
// #3: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT ? OFFSET ?
217+
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT ?
218+
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id LIMIT ? OFFSET ?
219+
//
220+
// Presto
221+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
222+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2) OFFSET ?
223+
// #3: (SELECT * FROM user1) UNION (SELECT * FROM user2) OFFSET ? LIMIT ?
224+
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT ?
225+
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id OFFSET ? LIMIT ?
226+
//
227+
// Oracle
228+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
229+
// #2: SELECT * FROM ( (SELECT * FROM user1) UNION (SELECT * FROM user2) ) OFFSET :1 ROWS
230+
// #3: SELECT * FROM ( (SELECT * FROM user1) UNION (SELECT * FROM user2) ) OFFSET :1 ROWS FETCH NEXT :2 ROWS ONLY
231+
// #4: SELECT * FROM ( (SELECT * FROM user1) UNION (SELECT * FROM user2) ) OFFSET 0 ROWS FETCH NEXT :1 ROWS ONLY
232+
// #5: SELECT * FROM ( (SELECT * FROM user1) UNION (SELECT * FROM user2) ) ORDER BY id OFFSET :1 ROWS FETCH NEXT :2 ROWS ONLY
233+
//
234+
// Informix
235+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
236+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2)
237+
// #3: SELECT * FROM ( (SELECT * FROM user1) UNION (SELECT * FROM user2) ) SKIP ? FIRST ?
238+
// #4: SELECT * FROM ( (SELECT * FROM user1) UNION (SELECT * FROM user2) ) FIRST ?
239+
// #5: SELECT * FROM ( (SELECT * FROM user1) UNION (SELECT * FROM user2) ) ORDER BY id SKIP ? FIRST ?
240+
//
241+
// Doris
242+
// #1: (SELECT * FROM user1) UNION (SELECT * FROM user2)
243+
// #2: (SELECT * FROM user1) UNION (SELECT * FROM user2)
244+
// #3: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT 1 OFFSET 0
245+
// #4: (SELECT * FROM user1) UNION (SELECT * FROM user2) LIMIT 1
246+
// #5: (SELECT * FROM user1) UNION (SELECT * FROM user2) ORDER BY id LIMIT 1 OFFSET 1
247+
}

0 commit comments

Comments
 (0)