Skip to content

Commit 5f7015c

Browse files
committed
Add parser support for multiple SQL features
- view() function: Handle subquery arguments in keyword-as-function context - PASTE JOIN: Add new join type with token and parser support - INTO OUTFILE TRUNCATE: Parse TRUNCATE option and update explain output - REGEXP: Add REGEXP operator that translates to match() function - EXPLAIN AST subquery: Allow EXPLAIN in subquery context - QUALIFY clause: Add window function filter clause support - GROUPING SETS: Add GROUPING and SETS tokens, parse GROUPING SETS syntax - Fix TableJoin explain output to not show "(children 0)"
1 parent cb4e975 commit 5f7015c

8 files changed

Lines changed: 172 additions & 10 deletions

File tree

ast/ast.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type SelectQuery struct {
5757
WithCube bool `json:"with_cube,omitempty"`
5858
WithTotals bool `json:"with_totals,omitempty"`
5959
Having Expression `json:"having,omitempty"`
60+
Qualify Expression `json:"qualify,omitempty"`
6061
Window []*WindowDefinition `json:"window,omitempty"`
6162
OrderBy []*OrderByElement `json:"order_by,omitempty"`
6263
Limit Expression `json:"limit,omitempty"`
@@ -90,6 +91,7 @@ func (w *WindowDefinition) End() token.Position { return w.Position }
9091
type IntoOutfileClause struct {
9192
Position token.Position `json:"-"`
9293
Filename string `json:"filename"`
94+
Truncate bool `json:"truncate,omitempty"`
9395
}
9496

9597
func (i *IntoOutfileClause) Pos() token.Position { return i.Position }
@@ -162,6 +164,7 @@ const (
162164
JoinRight JoinType = "RIGHT"
163165
JoinFull JoinType = "FULL"
164166
JoinCross JoinType = "CROSS"
167+
JoinPaste JoinType = "PASTE"
165168
)
166169

167170
// JoinStrictness represents the join strictness.
@@ -458,6 +461,7 @@ type DescribeQuery struct {
458461
Database string `json:"database,omitempty"`
459462
Table string `json:"table,omitempty"`
460463
TableFunction *FunctionCall `json:"table_function,omitempty"`
464+
Settings []*SettingExpr `json:"settings,omitempty"`
461465
}
462466

463467
func (d *DescribeQuery) Pos() token.Position { return d.Position }

internal/explain/functions.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ func explainFunctionCallWithAlias(sb *strings.Builder, n *ast.FunctionCall, alia
3737
}
3838
fmt.Fprintln(sb)
3939
for _, arg := range n.Arguments {
40+
// For view() table function, unwrap Subquery wrapper
41+
if strings.ToLower(n.Name) == "view" {
42+
if sq, ok := arg.(*ast.Subquery); ok {
43+
Node(sb, sq.Query, depth+2)
44+
continue
45+
}
46+
}
4047
Node(sb, arg, depth+2)
4148
}
4249
// Settings appear as Set node inside ExpressionList

internal/explain/select.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ func explainSelectWithUnionQuery(sb *strings.Builder, n *ast.SelectWithUnionQuer
1515
for _, sel := range n.Selects {
1616
Node(sb, sel, depth+2)
1717
}
18+
// INTO OUTFILE clause - check if any SelectQuery has IntoOutfile set
19+
for _, sel := range n.Selects {
20+
if sq, ok := sel.(*ast.SelectQuery); ok && sq.IntoOutfile != nil {
21+
fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, sq.IntoOutfile.Filename)
22+
break
23+
}
24+
}
1825
// FORMAT clause - check if any SelectQuery has Format set
1926
var hasFormat bool
2027
for _, sel := range n.Selects {
@@ -131,6 +138,13 @@ func explainOrderByElement(sb *strings.Builder, n *ast.OrderByElement, indent st
131138

132139
func countSelectUnionChildren(n *ast.SelectWithUnionQuery) int {
133140
count := 1 // ExpressionList of selects
141+
// Check if any SelectQuery has IntoOutfile set
142+
for _, sel := range n.Selects {
143+
if sq, ok := sel.(*ast.SelectQuery); ok && sq.IntoOutfile != nil {
144+
count++
145+
break
146+
}
147+
}
134148
// Check if any SelectQuery has Format set
135149
var hasFormat bool
136150
for _, sel := range n.Selects {

internal/explain/statements.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,15 @@ func explainUseQuery(sb *strings.Builder, n *ast.UseQuery, indent string) {
273273
func explainDescribeQuery(sb *strings.Builder, n *ast.DescribeQuery, indent string) {
274274
if n.TableFunction != nil {
275275
// DESCRIBE on a table function
276-
fmt.Fprintf(sb, "%sDescribeQuery (children 1)\n", indent)
276+
children := 1
277+
if len(n.Settings) > 0 {
278+
children++
279+
}
280+
fmt.Fprintf(sb, "%sDescribeQuery (children %d)\n", indent, children)
277281
explainFunctionCall(sb, n.TableFunction, indent+" ", 1)
282+
if len(n.Settings) > 0 {
283+
fmt.Fprintf(sb, "%s Set\n", indent)
284+
}
278285
} else {
279286
name := n.Table
280287
if n.Database != "" {

internal/explain/tables.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ func explainTableJoin(sb *strings.Builder, n *ast.TableJoin, indent string, dept
8484
if len(n.Using) > 0 {
8585
children++
8686
}
87-
fmt.Fprintf(sb, "%sTableJoin (children %d)\n", indent, children)
87+
if children > 0 {
88+
fmt.Fprintf(sb, "%sTableJoin (children %d)\n", indent, children)
89+
} else {
90+
fmt.Fprintf(sb, "%sTableJoin\n", indent)
91+
}
8892
if n.On != nil {
8993
Node(sb, n.On, depth+1)
9094
}

parser/expression.go

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func (p *Parser) precedence(tok token.Token) int {
3636
case token.NOT:
3737
return NOT_PREC
3838
case token.EQ, token.NEQ, token.LT, token.GT, token.LTE, token.GTE,
39-
token.LIKE, token.ILIKE, token.IN, token.BETWEEN, token.IS,
39+
token.LIKE, token.ILIKE, token.REGEXP, token.IN, token.BETWEEN, token.IS,
4040
token.NULL_SAFE_EQ, token.GLOBAL:
4141
return COMPARE
4242
case token.QUESTION:
@@ -101,6 +101,38 @@ func (p *Parser) parseExpressionList() []ast.Expression {
101101
return exprs
102102
}
103103

104+
// parseGroupingSets parses GROUPING SETS ((a), (b), (a, b))
105+
func (p *Parser) parseGroupingSets() []ast.Expression {
106+
var exprs []ast.Expression
107+
108+
if !p.expect(token.LPAREN) {
109+
return exprs
110+
}
111+
112+
for !p.currentIs(token.RPAREN) && !p.currentIs(token.EOF) {
113+
// Each element in GROUPING SETS is a tuple or a single expression
114+
if p.currentIs(token.LPAREN) {
115+
// Parse as tuple
116+
tuple := p.parseGroupedOrTuple()
117+
exprs = append(exprs, tuple)
118+
} else {
119+
// Single expression
120+
expr := p.parseExpression(LOWEST)
121+
if expr != nil {
122+
exprs = append(exprs, expr)
123+
}
124+
}
125+
126+
// Skip comma if present
127+
if p.currentIs(token.COMMA) {
128+
p.nextToken()
129+
}
130+
}
131+
132+
p.expect(token.RPAREN)
133+
return exprs
134+
}
135+
104136
// parseFunctionArgumentList parses arguments for function calls, stopping at SETTINGS
105137
func (p *Parser) parseFunctionArgumentList() []ast.Expression {
106138
var exprs []ast.Expression
@@ -263,8 +295,10 @@ func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression {
263295
return p.parseTernary(left)
264296
case token.LIKE, token.ILIKE:
265297
return p.parseLikeExpression(left, false)
298+
case token.REGEXP:
299+
return p.parseRegexpExpression(left, false)
266300
case token.NOT:
267-
// NOT IN, NOT LIKE, NOT BETWEEN, IS NOT
301+
// NOT IN, NOT LIKE, NOT BETWEEN, NOT REGEXP, IS NOT
268302
p.nextToken()
269303
switch p.current.Token {
270304
case token.IN:
@@ -273,6 +307,8 @@ func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression {
273307
return p.parseLikeExpression(left, true)
274308
case token.ILIKE:
275309
return p.parseLikeExpression(left, true)
310+
case token.REGEXP:
311+
return p.parseRegexpExpression(left, true)
276312
case token.BETWEEN:
277313
return p.parseBetweenExpression(left, true)
278314
default:
@@ -674,7 +710,7 @@ func (p *Parser) parseGroupedOrTuple() ast.Expression {
674710
}
675711
}
676712

677-
// Check for subquery
713+
// Check for subquery (SELECT, WITH, or EXPLAIN)
678714
if p.currentIs(token.SELECT) || p.currentIs(token.WITH) {
679715
subquery := p.parseSelectWithUnion()
680716
p.expect(token.RPAREN)
@@ -683,6 +719,15 @@ func (p *Parser) parseGroupedOrTuple() ast.Expression {
683719
Query: subquery,
684720
}
685721
}
722+
// EXPLAIN as subquery
723+
if p.currentIs(token.EXPLAIN) {
724+
explain := p.parseExplain()
725+
p.expect(token.RPAREN)
726+
return &ast.Subquery{
727+
Position: pos,
728+
Query: explain,
729+
}
730+
}
686731

687732
// Parse first expression
688733
first := p.parseExpression(LOWEST)
@@ -1075,6 +1120,30 @@ func (p *Parser) parseLikeExpression(left ast.Expression, not bool) ast.Expressi
10751120
return expr
10761121
}
10771122

1123+
func (p *Parser) parseRegexpExpression(left ast.Expression, not bool) ast.Expression {
1124+
pos := p.current.Pos
1125+
p.nextToken() // skip REGEXP
1126+
1127+
pattern := p.parseExpression(COMPARE)
1128+
1129+
// REGEXP translates to match(expr, pattern) function
1130+
fnCall := &ast.FunctionCall{
1131+
Position: pos,
1132+
Name: "match",
1133+
Arguments: []ast.Expression{left, pattern},
1134+
}
1135+
1136+
if not {
1137+
// NOT REGEXP uses NOT match(...)
1138+
return &ast.UnaryExpr{
1139+
Position: pos,
1140+
Op: "NOT",
1141+
Operand: fnCall,
1142+
}
1143+
}
1144+
return fnCall
1145+
}
1146+
10781147
func (p *Parser) parseInExpression(left ast.Expression, not bool) ast.Expression {
10791148
expr := &ast.InExpr{
10801149
Position: p.current.Pos,
@@ -1478,7 +1547,11 @@ func (p *Parser) parseKeywordAsFunction() ast.Expression {
14781547
}
14791548

14801549
var args []ast.Expression
1481-
if !p.currentIs(token.RPAREN) {
1550+
// Handle view() and similar functions that take a subquery as argument
1551+
if name == "view" && (p.currentIs(token.SELECT) || p.currentIs(token.WITH)) {
1552+
subquery := p.parseSelectWithUnion()
1553+
args = []ast.Expression{&ast.Subquery{Position: pos, Query: subquery}}
1554+
} else if !p.currentIs(token.RPAREN) {
14821555
args = p.parseExpressionList()
14831556
}
14841557

parser/parser.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,30 @@ func (p *Parser) parseSelect() *ast.SelectQuery {
254254
if !p.expect(token.BY) {
255255
return nil
256256
}
257-
sel.GroupBy = p.parseExpressionList()
257+
258+
// Handle GROUPING SETS, ROLLUP(...), CUBE(...) as special expressions
259+
if p.currentIs(token.GROUPING) && p.peekIs(token.SETS) {
260+
// GROUPING SETS ((a), (b), (a, b))
261+
p.nextToken() // skip GROUPING
262+
p.nextToken() // skip SETS
263+
sel.GroupBy = p.parseGroupingSets()
264+
} else if p.currentIs(token.ROLLUP) && p.peekIs(token.LPAREN) {
265+
// ROLLUP(a, b, c)
266+
p.nextToken() // skip ROLLUP
267+
p.nextToken() // skip (
268+
sel.GroupBy = p.parseExpressionList()
269+
p.expect(token.RPAREN)
270+
sel.WithRollup = true
271+
} else if p.currentIs(token.CUBE) && p.peekIs(token.LPAREN) {
272+
// CUBE(a, b, c)
273+
p.nextToken() // skip CUBE
274+
p.nextToken() // skip (
275+
sel.GroupBy = p.parseExpressionList()
276+
p.expect(token.RPAREN)
277+
sel.WithCube = true
278+
} else {
279+
sel.GroupBy = p.parseExpressionList()
280+
}
258281

259282
// WITH ROLLUP
260283
if p.currentIs(token.WITH) && p.peekIs(token.ROLLUP) {
@@ -284,6 +307,12 @@ func (p *Parser) parseSelect() *ast.SelectQuery {
284307
sel.Having = p.parseExpression(LOWEST)
285308
}
286309

310+
// Parse QUALIFY clause (window function filter)
311+
if p.currentIs(token.QUALIFY) {
312+
p.nextToken()
313+
sel.Qualify = p.parseExpression(LOWEST)
314+
}
315+
287316
// Parse WINDOW clause for named windows
288317
if p.currentIs(token.WINDOW) {
289318
p.nextToken()
@@ -390,6 +419,11 @@ func (p *Parser) parseSelect() *ast.SelectQuery {
390419
Filename: p.current.Value,
391420
}
392421
p.nextToken()
422+
// Parse optional TRUNCATE
423+
if p.currentIs(token.TRUNCATE) {
424+
sel.IntoOutfile.Truncate = true
425+
p.nextToken()
426+
}
393427
}
394428
}
395429
}
@@ -528,7 +562,7 @@ func (p *Parser) isJoinKeyword() bool {
528562
}
529563
switch p.current.Token {
530564
case token.JOIN, token.INNER, token.LEFT, token.RIGHT, token.FULL, token.CROSS,
531-
token.GLOBAL, token.ANY, token.ALL, token.ASOF, token.SEMI, token.ANTI:
565+
token.GLOBAL, token.ANY, token.ALL, token.ASOF, token.SEMI, token.ANTI, token.PASTE:
532566
return true
533567
case token.COMMA:
534568
return true
@@ -613,6 +647,9 @@ func (p *Parser) parseTableElementWithJoin() *ast.TablesInSelectQueryElement {
613647
case token.CROSS:
614648
join.Type = ast.JoinCross
615649
p.nextToken()
650+
case token.PASTE:
651+
join.Type = ast.JoinPaste
652+
p.nextToken()
616653
default:
617654
join.Type = ast.JoinInner
618655
}
@@ -720,10 +757,10 @@ func (p *Parser) parseTableExpression() *ast.TableExpression {
720757

721758
func (p *Parser) isKeywordForClause() bool {
722759
switch p.current.Token {
723-
case token.WHERE, token.GROUP, token.HAVING, token.ORDER, token.LIMIT,
760+
case token.WHERE, token.GROUP, token.HAVING, token.QUALIFY, token.ORDER, token.LIMIT,
724761
token.OFFSET, token.UNION, token.EXCEPT, token.SETTINGS, token.FORMAT,
725762
token.PREWHERE, token.JOIN, token.LEFT, token.RIGHT, token.INNER,
726-
token.FULL, token.CROSS, token.ON, token.USING, token.GLOBAL,
763+
token.FULL, token.CROSS, token.PASTE, token.ON, token.USING, token.GLOBAL,
727764
token.ANY, token.ALL, token.SEMI, token.ANTI, token.ASOF:
728765
return true
729766
}
@@ -2201,6 +2238,12 @@ func (p *Parser) parseDescribe() *ast.DescribeQuery {
22012238
}
22022239
}
22032240

2241+
// Parse SETTINGS clause
2242+
if p.currentIs(token.SETTINGS) {
2243+
p.nextToken()
2244+
desc.Settings = p.parseSettingsList()
2245+
}
2246+
22042247
return desc
22052248
}
22062249

0 commit comments

Comments
 (0)