Skip to content

Commit a1a6e64

Browse files
authored
Document limitation: cannot flatten OR/AND chains in EXPLAIN output (#92)
1 parent 56cb4de commit a1a6e64

File tree

432 files changed

+409
-1879
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

432 files changed

+409
-1879
lines changed

ast/ast.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,10 +1037,11 @@ const (
10371037

10381038
// BinaryExpr represents a binary expression.
10391039
type BinaryExpr struct {
1040-
Position token.Position `json:"-"`
1041-
Left Expression `json:"left"`
1042-
Op string `json:"op"`
1043-
Right Expression `json:"right"`
1040+
Position token.Position `json:"-"`
1041+
Left Expression `json:"left"`
1042+
Op string `json:"op"`
1043+
Right Expression `json:"right"`
1044+
Parenthesized bool `json:"parenthesized,omitempty"` // True if wrapped in explicit parentheses
10441045
}
10451046

10461047
func (b *BinaryExpr) Pos() token.Position { return b.Position }

internal/explain/expressions.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,18 @@ func explainBinaryExpr(sb *strings.Builder, n *ast.BinaryExpr, indent string, de
264264
return
265265
}
266266

267+
// For OR and AND operators, flatten left-associative chains
268+
// but preserve explicit parenthesization like "(a OR b) OR c"
269+
if n.Op == "OR" || n.Op == "AND" {
270+
operands := collectLogicalOperands(n)
271+
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
272+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(operands))
273+
for _, op := range operands {
274+
Node(sb, op, depth+2)
275+
}
276+
return
277+
}
278+
267279
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
268280
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2)
269281
Node(sb, n.Left, depth+2)
@@ -291,6 +303,26 @@ func collectConcatOperands(n *ast.BinaryExpr) []ast.Expression {
291303
return operands
292304
}
293305

306+
// collectLogicalOperands flattens chained OR/AND operations into a list of operands,
307+
// but respects explicit parenthesization. For example:
308+
// - "a OR b OR c" → [a, b, c] (flattened)
309+
// - "(a OR b) OR c" → [(a OR b), c] (preserved due to explicit parens)
310+
func collectLogicalOperands(n *ast.BinaryExpr) []ast.Expression {
311+
var operands []ast.Expression
312+
313+
// Recursively collect from left side if it's the same operator AND not parenthesized
314+
if left, ok := n.Left.(*ast.BinaryExpr); ok && left.Op == n.Op && !left.Parenthesized {
315+
operands = append(operands, collectLogicalOperands(left)...)
316+
} else {
317+
operands = append(operands, n.Left)
318+
}
319+
320+
// Don't flatten right side - explicit parentheses would be on the left in left-associative parsing
321+
operands = append(operands, n.Right)
322+
323+
return operands
324+
}
325+
294326
func explainUnaryExpr(sb *strings.Builder, n *ast.UnaryExpr, indent string, depth int) {
295327
// Handle negate of literal numbers - output as negative literal instead of function
296328
if n.Op == "-" {
@@ -410,6 +442,14 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) {
410442
for _, op := range operands {
411443
Node(sb, op, depth+2)
412444
}
445+
} else if e.Op == "OR" || e.Op == "AND" {
446+
// For OR and AND operators, flatten but respect explicit parenthesization
447+
operands := collectLogicalOperands(e)
448+
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, escapeAlias(n.Alias), 1)
449+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(operands))
450+
for _, op := range operands {
451+
Node(sb, op, depth+2)
452+
}
413453
} else {
414454
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, escapeAlias(n.Alias), 1)
415455
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2)
@@ -582,6 +622,18 @@ func explainWithElement(sb *strings.Builder, n *ast.WithElement, indent string,
582622
for _, op := range operands {
583623
Node(sb, op, depth+2)
584624
}
625+
} else if e.Op == "OR" || e.Op == "AND" {
626+
// For OR and AND operators, flatten but respect explicit parenthesization
627+
operands := collectLogicalOperands(e)
628+
if n.Name != "" {
629+
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Name, 1)
630+
} else {
631+
fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1)
632+
}
633+
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(operands))
634+
for _, op := range operands {
635+
Node(sb, op, depth+2)
636+
}
585637
} else {
586638
if n.Name != "" {
587639
fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Name, 1)

parser/expression.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,13 @@ func (p *Parser) parseGroupedOrTuple() ast.Expression {
910910
}
911911

912912
p.expect(token.RPAREN)
913+
914+
// Mark binary expressions as parenthesized so we can preserve explicit
915+
// grouping in EXPLAIN output (e.g., "(a OR b) OR c" vs "a OR b OR c")
916+
if binExpr, ok := first.(*ast.BinaryExpr); ok {
917+
binExpr.Parenthesized = true
918+
}
919+
913920
return first
914921
}
915922

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"explain_todo":{"stmt11":true,"stmt12":true,"stmt5":true,"stmt6":true,"stmt7":true}}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt2": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt6": true,
4-
"stmt7": true,
5-
"stmt8": true
6-
}
7-
}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"explain_todo":{"stmt4":true}}
1+
{}
Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt28": true,
4-
"stmt42": true,
5-
"stmt43": true,
6-
"stmt44": true,
7-
"stmt45": true,
8-
"stmt46": true,
9-
"stmt47": true,
10-
"stmt48": true
11-
}
12-
}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"explain_todo":{"stmt3":true,"stmt4":true}}
1+
{}
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt20": true,
4-
"stmt21": true
5-
}
6-
}
1+
{}

0 commit comments

Comments
 (0)