Skip to content

Commit 2c892d1

Browse files
committed
Add LimitByOffset support for LIMIT BY clauses
Handle LIMIT offset, count BY expr and LIMIT n OFFSET m BY expr syntax by storing the offset separately from the regular OFFSET clause. - Added LimitByOffset field to SelectQuery AST - Updated parser to extract offset for LIMIT BY clauses - Updated explain output to include LimitByOffset Fixed test: 00939_limit_by_offset/stmt7
1 parent 849d1f4 commit 2c892d1

File tree

4 files changed

+66
-25
lines changed

4 files changed

+66
-25
lines changed

ast/ast.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type SelectQuery struct {
8181
Limit Expression `json:"limit,omitempty"`
8282
LimitBy []Expression `json:"limit_by,omitempty"`
8383
LimitByLimit Expression `json:"limit_by_limit,omitempty"` // LIMIT value before BY (e.g., LIMIT 1 BY x LIMIT 3)
84+
LimitByOffset Expression `json:"limit_by_offset,omitempty"` // Offset for LIMIT BY (e.g., LIMIT 2, 3 BY x -> offset=2)
8485
LimitByHasLimit bool `json:"limit_by_has_limit,omitempty"` // true if LIMIT BY was followed by another LIMIT
8586
Offset Expression `json:"offset,omitempty"`
8687
Settings []*SettingExpr `json:"settings,omitempty"`

internal/explain/select.go

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -139,32 +139,46 @@ func explainSelectQueryWithInheritedWith(sb *strings.Builder, stmt ast.Statement
139139
Node(sb, i, depth+2)
140140
}
141141
}
142-
// OFFSET
143-
if sq.Offset != nil {
144-
Node(sb, sq.Offset, depth+1)
145-
}
146-
// LIMIT BY handling
142+
// LIMIT BY handling - order: LimitByOffset, LimitByLimit, LimitBy expressions, Offset, Limit
147143
if sq.LimitByLimit != nil {
144+
// Output LIMIT BY offset first (if present)
145+
if sq.LimitByOffset != nil {
146+
Node(sb, sq.LimitByOffset, depth+1)
147+
}
148+
// Output LIMIT BY count
148149
Node(sb, sq.LimitByLimit, depth+1)
150+
// Output LIMIT BY expressions
149151
if len(sq.LimitBy) > 0 {
150152
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(sq.LimitBy))
151153
for _, expr := range sq.LimitBy {
152154
Node(sb, expr, depth+2)
153155
}
154156
}
157+
// Output regular OFFSET
158+
if sq.Offset != nil {
159+
Node(sb, sq.Offset, depth+1)
160+
}
161+
// Output regular LIMIT
155162
if sq.Limit != nil {
156163
Node(sb, sq.Limit, depth+1)
157164
}
158165
} else if len(sq.LimitBy) > 0 {
166+
// LIMIT BY without explicit LimitByLimit
159167
if sq.Limit != nil {
160168
Node(sb, sq.Limit, depth+1)
161169
}
162170
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(sq.LimitBy))
163171
for _, expr := range sq.LimitBy {
164172
Node(sb, expr, depth+2)
165173
}
166-
} else if sq.Limit != nil {
167-
Node(sb, sq.Limit, depth+1)
174+
} else {
175+
// No LIMIT BY - just regular OFFSET and LIMIT
176+
if sq.Offset != nil {
177+
Node(sb, sq.Offset, depth+1)
178+
}
179+
if sq.Limit != nil {
180+
Node(sb, sq.Limit, depth+1)
181+
}
168182
}
169183
// SETTINGS (when no INTERPOLATE - the case with INTERPOLATE is handled above)
170184
if len(sq.Settings) > 0 && len(sq.Interpolate) == 0 && !sq.SettingsAfterFormat {
@@ -417,35 +431,46 @@ func explainSelectQuery(sb *strings.Builder, n *ast.SelectQuery, indent string,
417431
Node(sb, i, depth+2)
418432
}
419433
}
420-
// OFFSET (ClickHouse outputs offset before limit in EXPLAIN AST)
421-
if n.Offset != nil {
422-
Node(sb, n.Offset, depth+1)
423-
}
424-
// LIMIT BY handling
434+
// LIMIT BY handling - order: LimitByOffset, LimitByLimit, LimitBy expressions, Offset, Limit
425435
if n.LimitByLimit != nil {
426-
// Case: LIMIT n BY x LIMIT m -> output LimitByLimit, LimitBy, Limit
436+
// Output LIMIT BY offset first (if present)
437+
if n.LimitByOffset != nil {
438+
Node(sb, n.LimitByOffset, depth+1)
439+
}
440+
// Output LIMIT BY count
427441
Node(sb, n.LimitByLimit, depth+1)
442+
// Output LIMIT BY expressions
428443
if len(n.LimitBy) > 0 {
429444
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.LimitBy))
430445
for _, expr := range n.LimitBy {
431446
Node(sb, expr, depth+2)
432447
}
433448
}
449+
// Output regular OFFSET
450+
if n.Offset != nil {
451+
Node(sb, n.Offset, depth+1)
452+
}
453+
// Output regular LIMIT
434454
if n.Limit != nil {
435455
Node(sb, n.Limit, depth+1)
436456
}
437457
} else if len(n.LimitBy) > 0 {
438-
// Case: LIMIT n BY x (no second LIMIT) -> output Limit, then LimitBy
458+
// LIMIT BY without explicit LimitByLimit
439459
if n.Limit != nil {
440460
Node(sb, n.Limit, depth+1)
441461
}
442462
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.LimitBy))
443463
for _, expr := range n.LimitBy {
444464
Node(sb, expr, depth+2)
445465
}
446-
} else if n.Limit != nil {
447-
// Case: plain LIMIT n (no BY)
448-
Node(sb, n.Limit, depth+1)
466+
} else {
467+
// No LIMIT BY - just regular OFFSET and LIMIT
468+
if n.Offset != nil {
469+
Node(sb, n.Offset, depth+1)
470+
}
471+
if n.Limit != nil {
472+
Node(sb, n.Limit, depth+1)
473+
}
449474
}
450475
// SETTINGS is output at SelectQuery level only when NOT after FORMAT
451476
// When SettingsAfterFormat is true, it's output at SelectWithUnionQuery level instead
@@ -617,8 +642,11 @@ func countSelectQueryChildren(n *ast.SelectQuery) int {
617642
if len(n.Interpolate) > 0 {
618643
count++
619644
}
645+
if n.LimitByOffset != nil {
646+
count++ // LIMIT offset in "LIMIT offset, count BY x"
647+
}
620648
if n.LimitByLimit != nil {
621-
count++ // LIMIT n in "LIMIT n BY x LIMIT m"
649+
count++ // LIMIT count in "LIMIT n BY x LIMIT m"
622650
}
623651
if n.Limit != nil {
624652
count++

parser/parser.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,19 @@ func (p *Parser) parseSelectInternal(preParsedWith []ast.Expression) *ast.Select
12101210
// LIMIT BY clause (ClickHouse specific: LIMIT n BY expr1, expr2, ...)
12111211
if p.currentIs(token.BY) {
12121212
p.nextToken()
1213+
// If we had comma syntax (LIMIT offset, count BY ...), save values for LIMIT BY
1214+
// Otherwise just LIMIT n BY ... uses n as the count
1215+
if sel.Offset != nil {
1216+
// LIMIT offset, count BY ... -> LimitByOffset=offset, LimitByLimit=count
1217+
sel.LimitByOffset = sel.Offset
1218+
sel.LimitByLimit = sel.Limit
1219+
sel.Offset = nil
1220+
sel.Limit = nil
1221+
} else {
1222+
// LIMIT n BY ... -> LimitByLimit=n
1223+
sel.LimitByLimit = sel.Limit
1224+
sel.Limit = nil
1225+
}
12131226
// Parse LIMIT BY expressions
12141227
for !p.isEndOfExpression() {
12151228
expr := p.parseExpression(LOWEST)
@@ -1223,8 +1236,6 @@ func (p *Parser) parseSelectInternal(preParsedWith []ast.Expression) *ast.Select
12231236
// After LIMIT BY, there can be another LIMIT for overall output
12241237
if p.currentIs(token.LIMIT) {
12251238
p.nextToken()
1226-
// Save the LIMIT BY limit value (e.g., LIMIT 1 BY x -> LimitByLimit=1)
1227-
sel.LimitByLimit = sel.Limit
12281239
sel.Limit = p.parseExpression(LOWEST)
12291240
sel.LimitByHasLimit = true
12301241
}
@@ -1248,6 +1259,11 @@ func (p *Parser) parseSelectInternal(preParsedWith []ast.Expression) *ast.Select
12481259
// LIMIT n OFFSET m BY expr syntax - handle BY after OFFSET
12491260
if p.currentIs(token.BY) && sel.Limit != nil && len(sel.LimitBy) == 0 {
12501261
p.nextToken()
1262+
// Move Limit and Offset to LimitByLimit and LimitByOffset
1263+
sel.LimitByLimit = sel.Limit
1264+
sel.LimitByOffset = sel.Offset
1265+
sel.Limit = nil
1266+
sel.Offset = nil
12511267
// Parse LIMIT BY expressions
12521268
for !p.isEndOfExpression() {
12531269
expr := p.parseExpression(LOWEST)
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt7": true
4-
}
5-
}
1+
{}

0 commit comments

Comments
 (0)