Skip to content

Commit e0cd9ed

Browse files
authored
Fix 10+ parser tests with various improvements (#41)
1 parent cfc2d6e commit e0cd9ed

34 files changed

Lines changed: 319 additions & 49 deletions

File tree

ast/ast.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ func (s *SelectWithUnionQuery) Pos() token.Position { return s.Position }
4141
func (s *SelectWithUnionQuery) End() token.Position { return s.Position }
4242
func (s *SelectWithUnionQuery) statementNode() {}
4343

44+
// SelectIntersectExceptQuery represents SELECT ... INTERSECT/EXCEPT ... queries.
45+
type SelectIntersectExceptQuery struct {
46+
Position token.Position `json:"-"`
47+
Selects []Statement `json:"selects"`
48+
}
49+
50+
func (s *SelectIntersectExceptQuery) Pos() token.Position { return s.Position }
51+
func (s *SelectIntersectExceptQuery) End() token.Position { return s.Position }
52+
func (s *SelectIntersectExceptQuery) statementNode() {}
53+
4454
// SelectQuery represents a SELECT statement.
4555
type SelectQuery struct {
4656
Position token.Position `json:"-"`
@@ -212,6 +222,8 @@ type InsertQuery struct {
212222
Function *FunctionCall `json:"function,omitempty"` // For INSERT INTO FUNCTION syntax
213223
Columns []*Identifier `json:"columns,omitempty"`
214224
PartitionBy Expression `json:"partition_by,omitempty"` // For PARTITION BY clause
225+
Infile string `json:"infile,omitempty"` // For FROM INFILE clause
226+
Compression string `json:"compression,omitempty"` // For COMPRESSION clause
215227
Select Statement `json:"select,omitempty"`
216228
Format *Identifier `json:"format,omitempty"`
217229
HasSettings bool `json:"has_settings,omitempty"` // For SETTINGS clause

internal/explain/explain.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) {
3131
// Select statements
3232
case *ast.SelectWithUnionQuery:
3333
explainSelectWithUnionQuery(sb, n, indent, depth)
34+
case *ast.SelectIntersectExceptQuery:
35+
explainSelectIntersectExceptQuery(sb, n, indent, depth)
3436
case *ast.SelectQuery:
3537
explainSelectQuery(sb, n, indent, depth)
3638

internal/explain/expressions.go

Lines changed: 127 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,58 @@ func explainLiteral(sb *strings.Builder, n *ast.Literal, indent string, depth in
8181
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
8282
return
8383
}
84-
hasComplexExpr := false
84+
// Check if we should render as Function array
85+
// This happens when:
86+
// 1. Contains non-literal, non-negation expressions OR
87+
// 2. Contains tuples OR
88+
// 3. Contains nested arrays that all have exactly 1 element (homogeneous single-element arrays) OR
89+
// 4. Contains nested arrays with non-literal expressions OR
90+
// 5. Contains nested arrays that are empty or contain tuples/non-literals
91+
shouldUseFunctionArray := false
92+
allAreSingleElementArrays := true
93+
hasNestedArrays := false
94+
nestedArraysNeedFunctionFormat := false
95+
8596
for _, e := range exprs {
86-
if !isSimpleLiteralOrNegation(e) {
87-
hasComplexExpr = true
88-
break
97+
if lit, ok := e.(*ast.Literal); ok {
98+
if lit.Type == ast.LiteralArray {
99+
hasNestedArrays = true
100+
// Check if this inner array has exactly 1 element
101+
if innerExprs, ok := lit.Value.([]ast.Expression); ok {
102+
if len(innerExprs) != 1 {
103+
allAreSingleElementArrays = false
104+
}
105+
// Check if inner array needs Function array format:
106+
// - Contains non-literal expressions OR
107+
// - Contains tuples OR
108+
// - Is empty OR
109+
// - Contains empty arrays
110+
if containsNonLiteralExpressions(innerExprs) ||
111+
len(innerExprs) == 0 ||
112+
containsTuples(innerExprs) ||
113+
containsEmptyArrays(innerExprs) {
114+
nestedArraysNeedFunctionFormat = true
115+
}
116+
} else {
117+
allAreSingleElementArrays = false
118+
}
119+
} else if lit.Type == ast.LiteralTuple {
120+
// Tuples are complex
121+
shouldUseFunctionArray = true
122+
}
123+
} else if !isSimpleLiteralOrNegation(e) {
124+
shouldUseFunctionArray = true
89125
}
90126
}
91-
if hasComplexExpr {
127+
128+
// Use Function array when:
129+
// - nested arrays that are ALL single-element
130+
// - nested arrays that need Function format (contain non-literals, tuples, or empty arrays)
131+
if hasNestedArrays && (allAreSingleElementArrays || nestedArraysNeedFunctionFormat) {
132+
shouldUseFunctionArray = true
133+
}
134+
135+
if shouldUseFunctionArray {
92136
// Render as Function array instead of Literal
93137
fmt.Fprintf(sb, "%sFunction array (children %d)\n", indent, 1)
94138
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(exprs))
@@ -124,6 +168,58 @@ func isSimpleLiteralOrNegation(e ast.Expression) bool {
124168
return false
125169
}
126170

171+
// containsOnlyArraysOrTuples checks if a slice of expressions contains
172+
// only array or tuple literals (including empty arrays).
173+
// Returns true if the slice is empty or contains only arrays/tuples.
174+
func containsOnlyArraysOrTuples(exprs []ast.Expression) bool {
175+
if len(exprs) == 0 {
176+
return true // empty is considered "only arrays"
177+
}
178+
for _, e := range exprs {
179+
if lit, ok := e.(*ast.Literal); ok {
180+
if lit.Type != ast.LiteralArray && lit.Type != ast.LiteralTuple {
181+
return false
182+
}
183+
} else {
184+
return false
185+
}
186+
}
187+
return true
188+
}
189+
190+
// containsNonLiteralExpressions checks if a slice of expressions contains
191+
// any non-literal expressions (identifiers, function calls, etc.)
192+
func containsNonLiteralExpressions(exprs []ast.Expression) bool {
193+
for _, e := range exprs {
194+
if _, ok := e.(*ast.Literal); !ok {
195+
return true
196+
}
197+
}
198+
return false
199+
}
200+
201+
// containsTuples checks if a slice of expressions contains any tuple literals
202+
func containsTuples(exprs []ast.Expression) bool {
203+
for _, e := range exprs {
204+
if lit, ok := e.(*ast.Literal); ok && lit.Type == ast.LiteralTuple {
205+
return true
206+
}
207+
}
208+
return false
209+
}
210+
211+
// containsEmptyArrays checks if a slice of expressions contains any empty array literals
212+
func containsEmptyArrays(exprs []ast.Expression) bool {
213+
for _, e := range exprs {
214+
if lit, ok := e.(*ast.Literal); ok && lit.Type == ast.LiteralArray {
215+
if innerExprs, ok := lit.Value.([]ast.Expression); ok && len(innerExprs) == 0 {
216+
return true
217+
}
218+
}
219+
}
220+
return false
221+
}
222+
127223
func explainBinaryExpr(sb *strings.Builder, n *ast.BinaryExpr, indent string, depth int) {
128224
// Convert operator to function name
129225
fnName := OperatorToFunction(n.Op)
@@ -303,11 +399,20 @@ func explainAsterisk(sb *strings.Builder, n *ast.Asterisk, indent string) {
303399

304400
func explainWithElement(sb *strings.Builder, n *ast.WithElement, indent string, depth int) {
305401
// For WITH elements, we need to show the underlying expression with the name as alias
402+
// When name is empty, don't show the alias part
306403
switch e := n.Query.(type) {
307404
case *ast.Literal:
308-
fmt.Fprintf(sb, "%sLiteral %s (alias %s)\n", indent, FormatLiteral(e), n.Name)
405+
if n.Name != "" {
406+
fmt.Fprintf(sb, "%sLiteral %s (alias %s)\n", indent, FormatLiteral(e), n.Name)
407+
} else {
408+
fmt.Fprintf(sb, "%sLiteral %s\n", indent, FormatLiteral(e))
409+
}
309410
case *ast.Identifier:
310-
fmt.Fprintf(sb, "%sIdentifier %s (alias %s)\n", indent, e.Name(), n.Name)
411+
if n.Name != "" {
412+
fmt.Fprintf(sb, "%sIdentifier %s (alias %s)\n", indent, e.Name(), n.Name)
413+
} else {
414+
fmt.Fprintf(sb, "%sIdentifier %s\n", indent, e.Name())
415+
}
311416
case *ast.FunctionCall:
312417
explainFunctionCallWithAlias(sb, e, n.Name, indent, depth)
313418
case *ast.BinaryExpr:
@@ -316,19 +421,31 @@ func explainWithElement(sb *strings.Builder, n *ast.WithElement, indent string,
316421
// For || (concat) operator, flatten chained concatenations
317422
if e.Op == "||" {
318423
operands := collectConcatOperands(e)
319-
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Name, 1)
424+
if n.Name != "" {
425+
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Name, 1)
426+
} else {
427+
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
428+
}
320429
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(operands))
321430
for _, op := range operands {
322431
Node(sb, op, depth+2)
323432
}
324433
} else {
325-
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Name, 1)
434+
if n.Name != "" {
435+
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Name, 1)
436+
} else {
437+
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
438+
}
326439
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2)
327440
Node(sb, e.Left, depth+2)
328441
Node(sb, e.Right, depth+2)
329442
}
330443
case *ast.Subquery:
331-
fmt.Fprintf(sb, "%sSubquery (alias %s) (children %d)\n", indent, n.Name, 1)
444+
if n.Name != "" {
445+
fmt.Fprintf(sb, "%sSubquery (alias %s) (children %d)\n", indent, n.Name, 1)
446+
} else {
447+
fmt.Fprintf(sb, "%sSubquery (children %d)\n", indent, 1)
448+
}
332449
Node(sb, e.Query, depth+1)
333450
default:
334451
// For other types, just output the expression (alias may be lost)

internal/explain/format.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ func NormalizeFunctionName(name string) string {
208208
"lcase": "lower",
209209
"ucase": "upper",
210210
"mid": "substring",
211-
"substr": "substring",
212211
"ceiling": "ceil",
213212
"ln": "log",
214213
"log10": "log10",

internal/explain/functions.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,39 @@ func explainInExpr(sb *strings.Builder, n *ast.InExpr, indent string, depth int)
179179
fnName = "global" + strings.Title(fnName)
180180
}
181181
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
182+
183+
// Determine if the IN list should be combined into a single tuple literal
184+
// This happens when we have multiple literals of the same type:
185+
// - All numeric literals (integers/floats)
186+
// - All tuple literals
187+
canBeTupleLiteral := false
188+
if n.Query == nil && len(n.List) > 1 {
189+
allNumeric := true
190+
allTuples := true
191+
for _, item := range n.List {
192+
if lit, ok := item.(*ast.Literal); ok {
193+
if lit.Type != ast.LiteralInteger && lit.Type != ast.LiteralFloat {
194+
allNumeric = false
195+
}
196+
if lit.Type != ast.LiteralTuple {
197+
allTuples = false
198+
}
199+
} else {
200+
allNumeric = false
201+
allTuples = false
202+
break
203+
}
204+
}
205+
canBeTupleLiteral = allNumeric || allTuples
206+
}
207+
182208
// Count arguments: expr + list items or subquery
183209
argCount := 1
184210
if n.Query != nil {
185211
argCount++
212+
} else if canBeTupleLiteral {
213+
// Multiple literals will be combined into a single tuple
214+
argCount++
186215
} else {
187216
// Check if we have a single tuple literal that should be wrapped in Function tuple
188217
if len(n.List) == 1 {
@@ -198,10 +227,18 @@ func explainInExpr(sb *strings.Builder, n *ast.InExpr, indent string, depth int)
198227
}
199228
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, argCount)
200229
Node(sb, n.Expr, depth+2)
230+
201231
if n.Query != nil {
202232
// Subqueries in IN should be wrapped in Subquery node
203233
fmt.Fprintf(sb, "%s Subquery (children %d)\n", indent, 1)
204234
Node(sb, n.Query, depth+3)
235+
} else if canBeTupleLiteral {
236+
// Combine multiple literals into a single Tuple literal
237+
tupleLit := &ast.Literal{
238+
Type: ast.LiteralTuple,
239+
Value: n.List,
240+
}
241+
fmt.Fprintf(sb, "%s Literal %s\n", indent, FormatLiteral(tupleLit))
205242
} else if len(n.List) == 1 {
206243
// Single element in the list
207244
// If it's a tuple literal, wrap it in Function tuple

internal/explain/select.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import (
77
"github.com/sqlc-dev/doubleclick/ast"
88
)
99

10+
func explainSelectIntersectExceptQuery(sb *strings.Builder, n *ast.SelectIntersectExceptQuery, indent string, depth int) {
11+
fmt.Fprintf(sb, "%sSelectIntersectExceptQuery (children %d)\n", indent, len(n.Selects))
12+
for _, sel := range n.Selects {
13+
Node(sb, sel, depth+1)
14+
}
15+
}
16+
1017
func explainSelectWithUnionQuery(sb *strings.Builder, n *ast.SelectWithUnionQuery, indent string, depth int) {
1118
children := countSelectUnionChildren(n)
1219
fmt.Fprintf(sb, "%sSelectWithUnionQuery (children %d)\n", indent, children)

internal/explain/statements.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import (
1010
func explainInsertQuery(sb *strings.Builder, n *ast.InsertQuery, indent string, depth int) {
1111
// Count children
1212
children := 0
13+
if n.Infile != "" {
14+
children++
15+
}
16+
if n.Compression != "" {
17+
children++
18+
}
1319
if n.Function != nil {
1420
children++
1521
} else if n.Table != "" {
@@ -24,6 +30,15 @@ func explainInsertQuery(sb *strings.Builder, n *ast.InsertQuery, indent string,
2430
// Note: InsertQuery uses 3 spaces after name in ClickHouse explain
2531
fmt.Fprintf(sb, "%sInsertQuery (children %d)\n", indent, children)
2632

33+
// FROM INFILE path comes first
34+
if n.Infile != "" {
35+
fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, n.Infile)
36+
}
37+
// COMPRESSION value comes next
38+
if n.Compression != "" {
39+
fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, n.Compression)
40+
}
41+
2742
if n.Function != nil {
2843
Node(sb, n.Function, depth+1)
2944
} else if n.Table != "" {

lexer/lexer.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,16 +324,26 @@ func (l *Lexer) readBlockComment() Item {
324324
sb.WriteRune(l.ch)
325325
l.readChar()
326326

327-
for !l.eof {
327+
// Track nesting level for nested comments (ClickHouse supports nested /* */ comments)
328+
nesting := 1
329+
330+
for !l.eof && nesting > 0 {
328331
if l.ch == '*' && l.peekChar() == '/' {
329332
sb.WriteRune(l.ch)
330333
l.readChar()
331334
sb.WriteRune(l.ch)
332335
l.readChar()
333-
break
336+
nesting--
337+
} else if l.ch == '/' && l.peekChar() == '*' {
338+
sb.WriteRune(l.ch)
339+
l.readChar()
340+
sb.WriteRune(l.ch)
341+
l.readChar()
342+
nesting++
343+
} else {
344+
sb.WriteRune(l.ch)
345+
l.readChar()
334346
}
335-
sb.WriteRune(l.ch)
336-
l.readChar()
337347
}
338348
return Item{Token: token.COMMENT, Value: sb.String(), Pos: pos}
339349
}

0 commit comments

Comments
 (0)