Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
848ba54
Fix INTERSECT/EXCEPT operator precedence in parser
claude Dec 31, 2025
2dae897
Add lambda alias support in WITH elements EXPLAIN output
claude Dec 31, 2025
810d692
Add lambda alias support in AliasedExpr EXPLAIN output
claude Dec 31, 2025
61a44f5
Fix nested array literal formatting in EXPLAIN output
claude Dec 31, 2025
ec74bfb
Parse table options after AS clause in CREATE TABLE
claude Dec 31, 2025
7baa9ea
Add YYYY support for EXTRACT and fix alias handling
claude Dec 31, 2025
0d45aba
Add implicit NULL in CASE WHEN without ELSE clause
claude Dec 31, 2025
3142456
Fix nested tuple literal formatting in EXPLAIN output
claude Dec 31, 2025
a9600c9
Fix MODIFY COLUMN with only CODEC specification
claude Dec 31, 2025
c396276
Add column TTL expression to EXPLAIN output
claude Dec 31, 2025
4e5e254
Wrap ALTER TABLE MODIFY TTL expression in ExpressionList and TTLElement
claude Dec 31, 2025
a899fb6
Add GROUPING SETS support for EXPLAIN AST output
claude Dec 31, 2025
5effbea
Fix FORMAT clause handling in EXPLAIN statement output
claude Dec 31, 2025
7ff6679
Fix large string list handling in IN expressions
claude Dec 31, 2025
259d663
Fix GROUPING SETS tuple unwrapping in EXPLAIN output
claude Dec 31, 2025
4ff9272
Fix GROUPING SETS tuple unwrapping and add TOP clause support
claude Dec 31, 2025
3adff40
Update metadata: TOP clause test now passes
claude Dec 31, 2025
4293898
Support join strictness after type (RIGHT ANTI JOIN)
claude Dec 31, 2025
d57311f
Support column definitions in CREATE VIEW statements
claude Dec 31, 2025
cf878e5
Add ORDER BY and table options support for MATERIALIZED VIEW
claude Dec 31, 2025
6aaa618
Fix empty tuple and WITH clause expression parsing
claude Dec 31, 2025
78070a7
Add SETTINGS support for ALTER TABLE statements
claude Dec 31, 2025
acb8bb2
Fix empty lambda parameter list EXPLAIN output
claude Dec 31, 2025
c983b1a
Fix WITH FILL STEP output format in ORDER BY
claude Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type SelectQuery struct {
PreWhere Expression `json:"prewhere,omitempty"`
Where Expression `json:"where,omitempty"`
GroupBy []Expression `json:"group_by,omitempty"`
GroupingSets bool `json:"grouping_sets,omitempty"` // true if GROUP BY uses GROUPING SETS
WithRollup bool `json:"with_rollup,omitempty"`
WithCube bool `json:"with_cube,omitempty"`
WithTotals bool `json:"with_totals,omitempty"`
Expand Down Expand Up @@ -503,6 +504,7 @@ type AlterQuery struct {
Table string `json:"table"`
Commands []*AlterCommand `json:"commands"`
OnCluster string `json:"on_cluster,omitempty"`
Settings []*SettingExpr `json:"settings,omitempty"`
}

func (a *AlterQuery) Pos() token.Position { return a.Position }
Expand Down
6 changes: 6 additions & 0 deletions internal/explain/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ func Column(sb *strings.Builder, col *ast.ColumnDeclaration, depth int) {
if col.Default != nil || hasEphemeralDefault {
children++
}
if col.TTL != nil {
children++
}
if col.Codec != nil {
children++
}
Expand All @@ -295,6 +298,9 @@ func Column(sb *strings.Builder, col *ast.ColumnDeclaration, depth int) {
// EPHEMERAL columns without explicit default value show defaultValueOfTypeName function
fmt.Fprintf(sb, "%s Function defaultValueOfTypeName\n", indent)
}
if col.TTL != nil {
Node(sb, col.TTL, depth+1)
}
if col.Codec != nil {
explainCodecExpr(sb, col.Codec, indent+" ", depth+1)
}
Expand Down
115 changes: 92 additions & 23 deletions internal/explain/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,16 @@ func explainLiteral(sb *strings.Builder, n *ast.Literal, indent string, depth in
for _, e := range exprs {
// Simple literals (numbers, strings, etc.) are OK
if lit, isLit := e.(*ast.Literal); isLit {
// Nested tuples/arrays are complex
if lit.Type == ast.LiteralTuple || lit.Type == ast.LiteralArray {
// Nested tuples that contain only primitive literals are OK
if lit.Type == ast.LiteralTuple {
if !containsOnlyPrimitiveLiteralsWithUnary(lit) {
hasComplexExpr = true
break
}
continue
}
// Arrays are always complex in tuple context
if lit.Type == ast.LiteralArray {
hasComplexExpr = true
break
}
Expand Down Expand Up @@ -114,36 +122,28 @@ func explainLiteral(sb *strings.Builder, n *ast.Literal, indent string, depth in
// This happens when:
// 1. Contains non-literal, non-negation expressions OR
// 2. Contains tuples OR
// 3. Contains nested arrays that all have exactly 1 element (homogeneous single-element arrays) OR
// 4. Contains nested arrays with non-literal expressions OR
// 5. Contains nested arrays that are empty or contain tuples/non-literals
// 3. Contains nested arrays with non-literal expressions OR
// 4. Contains nested arrays that are empty or contain tuples/non-literals
shouldUseFunctionArray := false
allAreSingleElementArrays := true
hasNestedArrays := false
nestedArraysNeedFunctionFormat := false

for _, e := range exprs {
if lit, ok := e.(*ast.Literal); ok {
if lit.Type == ast.LiteralArray {
hasNestedArrays = true
// Check if this inner array has exactly 1 element
// Check if inner array needs Function array format:
// - Contains non-literal expressions OR
// - Contains tuples OR
// - Is empty OR
// - Contains empty arrays
if innerExprs, ok := lit.Value.([]ast.Expression); ok {
if len(innerExprs) != 1 {
allAreSingleElementArrays = false
}
// Check if inner array needs Function array format:
// - Contains non-literal expressions OR
// - Contains tuples OR
// - Is empty OR
// - Contains empty arrays
if containsNonLiteralExpressions(innerExprs) ||
len(innerExprs) == 0 ||
containsTuples(innerExprs) ||
containsEmptyArrays(innerExprs) {
nestedArraysNeedFunctionFormat = true
}
} else {
allAreSingleElementArrays = false
}
} else if lit.Type == ast.LiteralTuple {
// Tuples are complex
Expand All @@ -155,9 +155,17 @@ func explainLiteral(sb *strings.Builder, n *ast.Literal, indent string, depth in
}

// Use Function array when:
// - nested arrays that are ALL single-element
// - nested arrays that need Function format (contain non-literals, tuples, or empty arrays)
if hasNestedArrays && (allAreSingleElementArrays || nestedArraysNeedFunctionFormat) {
// - nested arrays that need Function format (contain non-literals, tuples, or empty arrays at any depth)
// Note: nested arrays that are ALL single-element should still be Literal format
if hasNestedArrays && nestedArraysNeedFunctionFormat {
shouldUseFunctionArray = true
}
// Also check for empty arrays at any depth within nested arrays
if hasNestedArrays && containsEmptyArraysRecursive(exprs) {
shouldUseFunctionArray = true
}
// Also check for tuples at any depth within nested arrays
if hasNestedArrays && containsTuplesRecursive(exprs) {
shouldUseFunctionArray = true
}

Expand Down Expand Up @@ -249,6 +257,43 @@ func containsEmptyArrays(exprs []ast.Expression) bool {
return false
}

// containsEmptyArraysRecursive checks if any nested array at any depth is empty
func containsEmptyArraysRecursive(exprs []ast.Expression) bool {
for _, e := range exprs {
if lit, ok := e.(*ast.Literal); ok && lit.Type == ast.LiteralArray {
if innerExprs, ok := lit.Value.([]ast.Expression); ok {
if len(innerExprs) == 0 {
return true
}
// Recursively check nested arrays
if containsEmptyArraysRecursive(innerExprs) {
return true
}
}
}
}
return false
}

// containsTuplesRecursive checks if any nested array contains tuples at any depth
func containsTuplesRecursive(exprs []ast.Expression) bool {
for _, e := range exprs {
if lit, ok := e.(*ast.Literal); ok {
if lit.Type == ast.LiteralTuple {
return true
}
if lit.Type == ast.LiteralArray {
if innerExprs, ok := lit.Value.([]ast.Expression); ok {
if containsTuplesRecursive(innerExprs) {
return true
}
}
}
}
}
return false
}

func explainBinaryExpr(sb *strings.Builder, n *ast.BinaryExpr, indent string, depth int) {
// Convert operator to function name
fnName := OperatorToFunction(n.Op)
Expand Down Expand Up @@ -377,14 +422,18 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) {
// Check if this is a tuple with complex expressions that should be rendered as Function tuple
if e.Type == ast.LiteralTuple {
if exprs, ok := e.Value.([]ast.Expression); ok {
hasComplexExpr := false
needsFunctionFormat := false
// Empty tuples always use Function tuple format
if len(exprs) == 0 {
needsFunctionFormat = true
}
for _, expr := range exprs {
if _, isLit := expr.(*ast.Literal); !isLit {
hasComplexExpr = true
needsFunctionFormat = true
break
}
}
if hasComplexExpr {
if needsFunctionFormat {
// Render as Function tuple with alias
fmt.Fprintf(sb, "%sFunction tuple (alias %s) (children %d)\n", indent, escapeAlias(n.Alias), 1)
fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(exprs))
Expand Down Expand Up @@ -489,6 +538,12 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) {
case *ast.FunctionCall:
// Function calls already handle aliases
explainFunctionCallWithAlias(sb, e, n.Alias, indent, depth)
case *ast.Lambda:
// Lambda expressions with alias
explainLambdaWithAlias(sb, e, n.Alias, indent, depth)
case *ast.ExtractExpr:
// EXTRACT expressions with alias
explainExtractExprWithAlias(sb, e, n.Alias, indent, depth)
case *ast.Identifier:
// Identifiers with alias
fmt.Fprintf(sb, "%sIdentifier %s (alias %s)\n", indent, e.Name(), escapeAlias(n.Alias))
Expand Down Expand Up @@ -632,6 +687,18 @@ func explainWithElement(sb *strings.Builder, n *ast.WithElement, indent string,
// When name is empty, don't show the alias part
switch e := n.Query.(type) {
case *ast.Literal:
// Empty tuples should be rendered as Function tuple, not Literal
if e.Type == ast.LiteralTuple {
if exprs, ok := e.Value.([]ast.Expression); ok && len(exprs) == 0 {
if n.Name != "" {
fmt.Fprintf(sb, "%sFunction tuple (alias %s) (children %d)\n", indent, n.Name, 1)
} else {
fmt.Fprintf(sb, "%sFunction tuple (children %d)\n", indent, 1)
}
fmt.Fprintf(sb, "%s ExpressionList\n", indent)
return
}
}
if n.Name != "" {
fmt.Fprintf(sb, "%sLiteral %s (alias %s)\n", indent, FormatLiteral(e), n.Name)
} else {
Expand All @@ -645,6 +712,8 @@ func explainWithElement(sb *strings.Builder, n *ast.WithElement, indent string,
}
case *ast.FunctionCall:
explainFunctionCallWithAlias(sb, e, n.Name, indent, depth)
case *ast.Lambda:
explainLambdaWithAlias(sb, e, n.Name, indent, depth)
case *ast.BinaryExpr:
// Binary expressions become functions
fnName := OperatorToFunction(e.Op)
Expand Down
Loading