From a4d3923d8575a38da201f4a7eeeddf9a07f491f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 20:27:03 +0000 Subject: [PATCH 01/20] Add support for ANY/ALL comparison operators with subqueries Implement proper handling of SQL standard quantified comparison operators (x op ANY (subquery) and x op ALL (subquery)) in the parser and explain module. - Parser now encodes both the comparison operator and modifier in the function name (e.g., anyEquals, allLess, anyGreaterOrEquals) - Explain module transforms these to ClickHouse's internal representation with appropriate aggregate functions (max/min/singleValueOrNull) - Fixed all 15 failing tests in 02007_test_any_all_operators --- internal/explain/format.go | 6 +- internal/explain/functions.go | 139 ++++++++++++++++++ parser/expression.go | 30 +++- .../metadata.json | 20 +-- .../02812_subquery_operators/metadata.json | 6 +- .../metadata.json | 6 +- 6 files changed, 172 insertions(+), 35 deletions(-) diff --git a/internal/explain/format.go b/internal/explain/format.go index 751418d804..b996cd7e28 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -314,9 +314,9 @@ func NormalizeFunctionName(name string) string { "least": "least", "concat_ws": "concat", "position": "position", - // SQL standard ANY/ALL subquery operators - "anymatch": "in", - "allmatch": "notIn", + // SQL standard ANY/ALL subquery operators - simple cases + "anyequals": "in", + "allnotequals": "notIn", } if n, ok := normalized[strings.ToLower(name)]; ok { return n diff --git a/internal/explain/functions.go b/internal/explain/functions.go index 2ca9ed3475..4da28af3f0 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -109,6 +109,11 @@ func windowSpecHasContent(w *ast.WindowSpec) bool { func handleSpecialFunction(sb *strings.Builder, n *ast.FunctionCall, alias string, indent string, depth int) bool { fnName := strings.ToUpper(n.Name) + // Handle quantified comparison operators (ANY/ALL with comparison operators) + if handled := handleQuantifiedComparison(sb, n, alias, indent, depth); handled { + return true + } + // POSITION('ll' IN 'Hello') -> position('Hello', 'll') if fnName == "POSITION" && len(n.Arguments) == 1 { if inExpr, ok := n.Arguments[0].(*ast.InExpr); ok { @@ -136,6 +141,140 @@ func handleSpecialFunction(sb *strings.Builder, n *ast.FunctionCall, alias strin return false } +// handleQuantifiedComparison handles ANY/ALL with comparison operators +// Returns true if the function was handled, false otherwise. +func handleQuantifiedComparison(sb *strings.Builder, n *ast.FunctionCall, alias string, indent string, depth int) bool { + fnName := strings.ToLower(n.Name) + + // Check if this is a quantified comparison function + var modifier, op string + if strings.HasPrefix(fnName, "any") { + modifier = "any" + op = fnName[3:] + } else if strings.HasPrefix(fnName, "all") { + modifier = "all" + op = fnName[3:] + } else { + return false + } + + // Must have exactly 2 arguments: left expr and subquery + if len(n.Arguments) != 2 { + return false + } + + subquery, ok := n.Arguments[1].(*ast.Subquery) + if !ok { + return false + } + + // Handle based on the operator and modifier + switch op { + case "equals": + if modifier == "any" { + // x == ANY (subquery) -> in(x, subquery) + return false // Let NormalizeFunctionName handle this + } + // x == ALL (subquery) -> complex with singleValueOrNull + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "in", "singleValueOrNull", alias, indent, depth) + return true + + case "notequals": + if modifier == "all" { + // x != ALL (subquery) -> notIn(x, subquery) + return false // Let NormalizeFunctionName handle this + } + // x != ANY (subquery) -> complex notIn with singleValueOrNull + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "notIn", "singleValueOrNull", alias, indent, depth) + return true + + case "less": + if modifier == "any" { + // x < ANY (subquery) -> x < max(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "less", "max", alias, indent, depth) + } else { + // x < ALL (subquery) -> x < min(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "less", "min", alias, indent, depth) + } + return true + + case "lessorequals": + if modifier == "any" { + // x <= ANY (subquery) -> x <= max(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "lessOrEquals", "max", alias, indent, depth) + } else { + // x <= ALL (subquery) -> x <= min(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "lessOrEquals", "min", alias, indent, depth) + } + return true + + case "greater": + if modifier == "any" { + // x > ANY (subquery) -> x > min(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "greater", "min", alias, indent, depth) + } else { + // x > ALL (subquery) -> x > max(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "greater", "max", alias, indent, depth) + } + return true + + case "greaterorequals": + if modifier == "any" { + // x >= ANY (subquery) -> x >= min(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "greaterOrEquals", "min", alias, indent, depth) + } else { + // x >= ALL (subquery) -> x >= max(subquery) + outputQuantifiedWithAggregate(sb, n.Arguments[0], subquery, "greaterOrEquals", "max", alias, indent, depth) + } + return true + } + + return false +} + +// outputQuantifiedWithAggregate outputs the ClickHouse AST format for quantified comparisons +// with an aggregate function wrapped around the subquery +func outputQuantifiedWithAggregate(sb *strings.Builder, left ast.Expression, subquery *ast.Subquery, compFunc, aggFunc string, alias string, indent string, depth int) { + if alias != "" { + fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, compFunc, alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, compFunc, 1) + } + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2) + Node(sb, left, depth+2) + + // Output the subquery wrapped with aggregate function + // Structure: Subquery -> SelectWithUnionQuery -> ExpressionList -> SelectQuery with 4 children + fmt.Fprintf(sb, "%s Subquery (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s SelectWithUnionQuery (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s SelectQuery (children %d)\n", indent, 4) + + // First ExpressionList with aggregate function + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s Function %s (children %d)\n", indent, aggFunc, 1) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s Asterisk\n", indent) + + // First TablesInSelectQuery - wrap the original subquery + fmt.Fprintf(sb, "%s TablesInSelectQuery (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s TablesInSelectQueryElement (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s TableExpression (children %d)\n", indent, 1) + Node(sb, subquery, depth+9) + + // Second ExpressionList with aggregate function (repeated) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s Function %s (children %d)\n", indent, aggFunc, 1) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s Asterisk\n", indent) + + // Second TablesInSelectQuery (repeated) + fmt.Fprintf(sb, "%s TablesInSelectQuery (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s TablesInSelectQueryElement (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s TableExpression (children %d)\n", indent, 1) + Node(sb, subquery, depth+9) +} + // explainPositionWithIn outputs POSITION(needle IN haystack) as position(haystack, needle) func explainPositionWithIn(sb *strings.Builder, needle, haystack ast.Expression, alias string, indent string, depth int) { if alias != "" { diff --git a/parser/expression.go b/parser/expression.go index 1320ebefc5..fd6275abb3 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -1727,7 +1727,7 @@ func (p *Parser) parseBinaryExpression(left ast.Expression) ast.Expression { // Check for ANY/ALL subquery comparison modifier: expr >= ANY(subquery) if p.currentIs(token.ANY) || p.currentIs(token.ALL) { - modifier := strings.ToUpper(p.current.Value) + modifier := strings.ToLower(p.current.Value) p.nextToken() if p.currentIs(token.LPAREN) { p.nextToken() @@ -1735,10 +1735,13 @@ func (p *Parser) parseBinaryExpression(left ast.Expression) ast.Expression { if p.currentIs(token.SELECT) || p.currentIs(token.WITH) { subquery := p.parseSelectWithUnion() p.expect(token.RPAREN) - // Wrap the comparison in a function call representing ANY/ALL + // Create function name that encodes both modifier and operator + // e.g., anyEquals, allLess, anyGreaterOrEquals, etc. + opName := operatorToName(expr.Op) + fnName := modifier + opName return &ast.FunctionCall{ Position: expr.Position, - Name: strings.ToLower(modifier) + "Match", + Name: fnName, Arguments: []ast.Expression{ left, &ast.Subquery{Position: expr.Position, Query: subquery}, @@ -1765,6 +1768,27 @@ func (p *Parser) parseBinaryExpression(left ast.Expression) ast.Expression { return expr } +// operatorToName converts a comparison operator to a capitalized name for use +// in ANY/ALL function names (e.g., "==" -> "Equals", "<" -> "Less") +func operatorToName(op string) string { + switch op { + case "=", "==": + return "Equals" + case "!=", "<>": + return "NotEquals" + case "<": + return "Less" + case "<=": + return "LessOrEquals" + case ">": + return "Greater" + case ">=": + return "GreaterOrEquals" + default: + return "Equals" // fallback + } +} + func (p *Parser) parseLikeExpression(left ast.Expression, not bool) ast.Expression { expr := &ast.LikeExpr{ Position: p.current.Pos, diff --git a/parser/testdata/02007_test_any_all_operators/metadata.json b/parser/testdata/02007_test_any_all_operators/metadata.json index 4995e0b439..0967ef424b 100644 --- a/parser/testdata/02007_test_any_all_operators/metadata.json +++ b/parser/testdata/02007_test_any_all_operators/metadata.json @@ -1,19 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt14": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/02812_subquery_operators/metadata.json b/parser/testdata/02812_subquery_operators/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/02812_subquery_operators/metadata.json +++ b/parser/testdata/02812_subquery_operators/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} diff --git a/parser/testdata/03392_crash_group_by_use_nulls/metadata.json b/parser/testdata/03392_crash_group_by_use_nulls/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/03392_crash_group_by_use_nulls/metadata.json +++ b/parser/testdata/03392_crash_group_by_use_nulls/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} From b955b51949d15b198424259f161bab9690d9ac4c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 20:57:17 +0000 Subject: [PATCH 02/20] Add SAMPLE clause OFFSET support and precision-preserving fraction formatting - Add OFFSET output to explainSampleClause as second SampleRatio line - Update TableExpression children count to include OFFSET - Add sourceToFraction to use Literal.Source field for precision - Handle scientific notation (2e-2 -> 2/100) and decimal (0.40 -> 40/100) - Fixes all 15 failing statements in 00276_sample test --- internal/explain/tables.go | 102 ++++++++++++++++++++- parser/testdata/00276_sample/metadata.json | 20 +--- 2 files changed, 102 insertions(+), 20 deletions(-) diff --git a/internal/explain/tables.go b/internal/explain/tables.go index dbae494017..9d2028fcea 100644 --- a/internal/explain/tables.go +++ b/internal/explain/tables.go @@ -2,6 +2,7 @@ package explain import ( "fmt" + "strconv" "strings" "github.com/sqlc-dev/doubleclick/ast" @@ -44,7 +45,10 @@ func explainTablesInSelectQueryElement(sb *strings.Builder, n *ast.TablesInSelec func explainTableExpression(sb *strings.Builder, n *ast.TableExpression, indent string, depth int) { children := 1 // table if n.Sample != nil { - children++ + children++ // for sample ratio + if n.Sample.Offset != nil { + children++ // for sample offset + } } fmt.Fprintf(sb, "%sTableExpression (children %d)\n", indent, children) // If there's a subquery with an alias, pass the alias to the subquery output @@ -83,6 +87,14 @@ func explainSampleClause(sb *strings.Builder, n *ast.SampleClause, indent string sb.WriteString("SampleRatio ") formatSampleRatio(sb, n.Ratio) sb.WriteString("\n") + + // Output OFFSET as a second SampleRatio line if present + if n.Offset != nil { + sb.WriteString(indent) + sb.WriteString("SampleRatio ") + formatSampleRatio(sb, n.Offset) + sb.WriteString("\n") + } } func formatSampleRatio(sb *strings.Builder, expr ast.Expression) { @@ -106,6 +118,13 @@ func formatSampleRatioOperand(sb *strings.Builder, expr ast.Expression) { case float64: // Convert decimal to fraction for EXPLAIN AST output // ClickHouse shows 0.1 as "1 / 10", 0.01 as "1 / 100", etc. + // Use Source field if available to preserve precision (0.4 vs 0.40) + if lit.Source != "" { + if frac := sourceToFraction(lit.Source); frac != "" { + sb.WriteString(frac) + return + } + } if frac := floatToFraction(v); frac != "" { sb.WriteString(frac) } else { @@ -119,6 +138,87 @@ func formatSampleRatioOperand(sb *strings.Builder, expr ast.Expression) { } } +// sourceToFraction converts a source string representation to a fraction +// This preserves the original precision (e.g., "0.4" -> "4 / 10", "0.40" -> "40 / 100") +func sourceToFraction(source string) string { + source = strings.TrimSpace(source) + + // Handle scientific notation like "2e-2" -> "2 / 100" + if strings.ContainsAny(source, "eE") { + return scientificToFraction(source) + } + + // Handle decimal notation like "0.4" or "0.40" + if strings.Contains(source, ".") { + return decimalToFraction(source) + } + + return "" +} + +// scientificToFraction handles scientific notation like "2e-2" -> "2 / 100" +func scientificToFraction(source string) string { + // Parse scientific notation: coefficient * 10^exponent + // Split by 'e' or 'E' + lower := strings.ToLower(source) + parts := strings.Split(lower, "e") + if len(parts) != 2 { + return "" + } + + coef, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return "" + } + + exp, err := strconv.Atoi(parts[1]) + if err != nil { + return "" + } + + // For negative exponents, convert to fraction + // 2e-2 = 2 * 10^-2 = 2 / 100 + if exp < 0 { + denom := int64(1) + for i := 0; i < -exp; i++ { + denom *= 10 + } + num := int64(coef) + if coef == float64(num) { + return fmt.Sprintf("%d / %d", num, denom) + } + } + return "" +} + +// decimalToFraction converts a decimal string to a fraction preserving precision +// "0.4" -> "4 / 10", "0.40" -> "40 / 100" +func decimalToFraction(source string) string { + parts := strings.Split(source, ".") + if len(parts) != 2 { + return "" + } + + decimalPart := parts[1] + // Count decimal places (including trailing zeros) + decimalPlaces := len(decimalPart) + + // Calculate denominator based on decimal places + denom := int64(1) + for i := 0; i < decimalPlaces; i++ { + denom *= 10 + } + + // Parse the decimal part as an integer (numerator) + // Handle leading zeros correctly: "0.05" -> "5 / 100" + num, err := strconv.ParseInt(decimalPart, 10, 64) + if err != nil { + return "" + } + + return fmt.Sprintf("%d / %d", num, denom) +} + // floatToFraction converts a float to a fraction string like "1 / 10" // Returns empty string if the float can't be reasonably converted to a simple fraction func floatToFraction(f float64) string { diff --git a/parser/testdata/00276_sample/metadata.json b/parser/testdata/00276_sample/metadata.json index c19358090d..0967ef424b 100644 --- a/parser/testdata/00276_sample/metadata.json +++ b/parser/testdata/00276_sample/metadata.json @@ -1,19 +1 @@ -{ - "explain_todo": { - "stmt14": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true, - "stmt22": true, - "stmt27": true, - "stmt28": true, - "stmt29": true, - "stmt30": true, - "stmt31": true, - "stmt32": true, - "stmt33": true, - "stmt34": true, - "stmt39": true - } -} +{} From f98314956393c675a3455657f9d64ba1c15dbd67 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 21:02:08 +0000 Subject: [PATCH 03/20] Add NULL-safe comparison operator (<=> -> isNotDistinctFrom) mapping Map the <=> operator to isNotDistinctFrom function in OperatorToFunction. Fixes all 15 failing statements in 03611_null_safe_comparsion test. --- internal/explain/format.go | 2 ++ .../03611_null_safe_comparsion/metadata.json | 20 +------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/internal/explain/format.go b/internal/explain/format.go index b996cd7e28..4de841329e 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -351,6 +351,8 @@ func OperatorToFunction(op string) string { return "lessOrEquals" case ">=": return "greaterOrEquals" + case "<=>": + return "isNotDistinctFrom" case "AND": return "and" case "OR": diff --git a/parser/testdata/03611_null_safe_comparsion/metadata.json b/parser/testdata/03611_null_safe_comparsion/metadata.json index 468db56803..0967ef424b 100644 --- a/parser/testdata/03611_null_safe_comparsion/metadata.json +++ b/parser/testdata/03611_null_safe_comparsion/metadata.json @@ -1,19 +1 @@ -{ - "explain_todo": { - "stmt100": true, - "stmt101": true, - "stmt39": true, - "stmt41": true, - "stmt43": true, - "stmt47": true, - "stmt50": true, - "stmt51": true, - "stmt55": true, - "stmt57": true, - "stmt59": true, - "stmt82": true, - "stmt95": true, - "stmt96": true, - "stmt99": true - } -} +{} From 7d2492028d9241f8ee8f629fbb19e32cf1210e4c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 21:28:00 +0000 Subject: [PATCH 04/20] Add FORMAT clause support for CREATE VIEW, ALTER TABLE, and DROP statements - Add Format field to CreateQuery, AlterQuery, and DropQuery AST nodes - Extract FORMAT from inner SelectQuery and move to CreateQuery for views - Add inCreateQueryContext flag to prevent FORMAT duplication in explain - Update explain output for AlterQuery to include FORMAT as child - Handle FORMAT Null in parser for ALTER TABLE statements This fixes 02006_test_positional_arguments_on_cluster and enables FORMAT clause handling for various statement types. --- ast/ast.go | 3 + internal/explain/explain.go | 4 ++ internal/explain/select.go | 22 ++++--- internal/explain/statements.go | 63 +++++++++++++++++-- parser/parser.go | 44 ++++++++++++- .../metadata.json | 19 +----- .../testdata/02681_undrop_query/metadata.json | 4 -- .../metadata.json | 2 - .../metadata.json | 13 +--- .../metadata.json | 8 +-- .../03224_invalid_alter/metadata.json | 1 - .../03513_nullsafe_join_storage/metadata.json | 7 +-- .../metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 6 +- 15 files changed, 129 insertions(+), 79 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 7330196296..e8c72ea028 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -284,6 +284,7 @@ type CreateQuery struct { FunctionName string `json:"function_name,omitempty"` FunctionBody Expression `json:"function_body,omitempty"` UserName string `json:"user_name,omitempty"` + Format string `json:"format,omitempty"` // For FORMAT clause } func (c *CreateQuery) Pos() token.Position { return c.Position } @@ -493,6 +494,7 @@ type DropQuery struct { OnCluster string `json:"on_cluster,omitempty"` DropDatabase bool `json:"drop_database,omitempty"` Sync bool `json:"sync,omitempty"` + Format string `json:"format,omitempty"` // For FORMAT clause } func (d *DropQuery) Pos() token.Position { return d.Position } @@ -520,6 +522,7 @@ type AlterQuery struct { Commands []*AlterCommand `json:"commands"` OnCluster string `json:"on_cluster,omitempty"` Settings []*SettingExpr `json:"settings,omitempty"` + Format string `json:"format,omitempty"` // For FORMAT clause } func (a *AlterQuery) Pos() token.Position { return a.Position } diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 561fdb6329..02e76350eb 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -12,6 +12,10 @@ import ( // This affects how negated literals with aliases are formatted var inSubqueryContext bool +// inCreateQueryContext is a package-level flag to track when we're inside a CreateQuery +// This affects whether FORMAT is output at SelectWithUnionQuery level (it shouldn't be, as CreateQuery outputs it) +var inCreateQueryContext bool + // Explain returns the EXPLAIN AST output for a statement, matching ClickHouse's format. func Explain(stmt ast.Statement) string { var sb strings.Builder diff --git a/internal/explain/select.go b/internal/explain/select.go index 3c30c111aa..c867495c91 100644 --- a/internal/explain/select.go +++ b/internal/explain/select.go @@ -54,10 +54,13 @@ func explainSelectWithUnionQuery(sb *strings.Builder, n *ast.SelectWithUnionQuer } } // FORMAT clause - check if any SelectQuery has Format set - for _, sel := range n.Selects { - if sq, ok := sel.(*ast.SelectQuery); ok && sq.Format != nil { - Node(sb, sq.Format, depth+1) - break + // Skip this when inside CreateQuery context, as Format is output at CreateQuery level + if !inCreateQueryContext { + for _, sel := range n.Selects { + if sq, ok := sel.(*ast.SelectQuery); ok && sq.Format != nil { + Node(sb, sq.Format, depth+1) + break + } } } // When SETTINGS comes AFTER FORMAT, it's output at SelectWithUnionQuery level @@ -289,10 +292,13 @@ func countSelectUnionChildren(n *ast.SelectWithUnionQuery) int { } } // Check if any SelectQuery has Format set - for _, sel := range n.Selects { - if sq, ok := sel.(*ast.SelectQuery); ok && sq.Format != nil { - count++ - break + // Skip this when inside CreateQuery context, as Format is output at CreateQuery level + if !inCreateQueryContext { + for _, sel := range n.Selects { + if sq, ok := sel.(*ast.SelectQuery); ok && sq.Format != nil { + count++ + break + } } } // When SETTINGS comes AFTER FORMAT, it's counted at SelectWithUnionQuery level diff --git a/internal/explain/statements.go b/internal/explain/statements.go index 273d7cd098..a7a3c6974b 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -177,6 +177,11 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, if n.AsTableFunction != nil { children++ } + // Count Format as a child if present + hasFormat := n.Format != "" + if hasFormat { + children++ + } // ClickHouse adds an extra space before (children N) for CREATE DATABASE if n.CreateDatabase { fmt.Fprintf(sb, "%sCreateQuery %s (children %d)\n", indent, EscapeIdentifier(name), children) @@ -252,7 +257,15 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, } // For materialized views, output AsSelect before storage definition if n.Materialized && n.AsSelect != nil { + // Set context flag to prevent Format from being output at SelectWithUnionQuery level + // (it will be output at CreateQuery level instead) + if hasFormat { + inCreateQueryContext = true + } Node(sb, n.AsSelect, depth+1) + if hasFormat { + inCreateQueryContext = false + } } hasStorage := n.Engine != nil || len(n.OrderBy) > 0 || len(n.PrimaryKey) > 0 || n.PartitionBy != nil || n.SampleBy != nil || n.TTL != nil || len(n.Settings) > 0 if hasStorage { @@ -416,13 +429,25 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, } // For non-materialized views, output AsSelect after storage if n.AsSelect != nil && !n.Materialized { + // Set context flag to prevent Format from being output at SelectWithUnionQuery level + // (it will be output at CreateQuery level instead) + if hasFormat { + inCreateQueryContext = true + } // AS SELECT is output directly without Subquery wrapper Node(sb, n.AsSelect, depth+1) + if hasFormat { + inCreateQueryContext = false + } } if n.AsTableFunction != nil { // AS table_function(...) is output directly Node(sb, n.AsTableFunction, depth+1) } + // Output FORMAT clause if present + if hasFormat { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } } func explainDropQuery(sb *strings.Builder, n *ast.DropQuery, indent string, depth int) { @@ -490,18 +515,41 @@ func explainDropQuery(sb *strings.Builder, n *ast.DropQuery, indent string, dept } // Check if we have a database-qualified name (for DROP TABLE db.table) hasDatabase := n.Database != "" && !n.DropDatabase + hasFormat := n.Format != "" + if hasDatabase { - // Database-qualified: DropQuery db table (children 2) - fmt.Fprintf(sb, "%sDropQuery %s %s (children %d)\n", indent, EscapeIdentifier(n.Database), EscapeIdentifier(name), 2) + // Database-qualified: DropQuery db table (children 2 or 3) + children := 2 + if hasFormat { + children = 3 + } + fmt.Fprintf(sb, "%sDropQuery %s %s (children %d)\n", indent, EscapeIdentifier(n.Database), EscapeIdentifier(name), children) fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(n.Database)) fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) + if hasFormat { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } } else if n.DropDatabase { // DROP DATABASE uses different spacing - fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, EscapeIdentifier(name), 1) + children := 1 + if hasFormat { + children = 2 + } + fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, EscapeIdentifier(name), children) fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) + if hasFormat { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } } else { - fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, EscapeIdentifier(name), 1) + children := 1 + if hasFormat { + children = 2 + } + fmt.Fprintf(sb, "%sDropQuery %s (children %d)\n", indent, EscapeIdentifier(name), children) fmt.Fprintf(sb, "%s Identifier %s\n", indent, EscapeIdentifier(name)) + if hasFormat { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } } } @@ -1075,6 +1123,10 @@ func explainAlterQuery(sb *strings.Builder, n *ast.AlterQuery, indent string, de if len(n.Settings) > 0 { children++ // Add Set child for SETTINGS } + hasFormat := n.Format != "" + if hasFormat { + children++ // Add Identifier for FORMAT + } if n.Database != "" { fmt.Fprintf(sb, "%sAlterQuery %s %s (children %d)\n", indent, n.Database, n.Table, children) } else { @@ -1092,6 +1144,9 @@ func explainAlterQuery(sb *strings.Builder, n *ast.AlterQuery, indent string, de if len(n.Settings) > 0 { fmt.Fprintf(sb, "%s Set\n", indent) } + if hasFormat { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Format) + } } func explainAlterCommand(sb *strings.Builder, cmd *ast.AlterCommand, indent string, depth int) { diff --git a/parser/parser.go b/parser/parser.go index 954a5381fb..0e201812d4 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1567,6 +1567,19 @@ func (p *Parser) parseCreate() ast.Statement { return nil } + // Handle FORMAT clause (for things like CREATE TABLE ... FORMAT Null) + if p.currentIs(token.FORMAT) { + p.nextToken() + // Store format name (Null, etc.) + if p.currentIs(token.NULL) { + create.Format = "Null" + p.nextToken() + } else if p.currentIs(token.IDENT) { + create.Format = p.current.Value + p.nextToken() + } + } + return create } @@ -2010,6 +2023,17 @@ func (p *Parser) parseCreateView(create *ast.CreateQuery) { p.nextToken() if p.currentIs(token.SELECT) || p.currentIs(token.WITH) || p.currentIs(token.LPAREN) { create.AsSelect = p.parseSelectWithUnion() + // Extract FORMAT from inner SelectQuery and move it to CreateQuery + // For CREATE VIEW/MATERIALIZED VIEW, FORMAT should be at CreateQuery level + if swu, ok := create.AsSelect.(*ast.SelectWithUnionQuery); ok && swu != nil { + for _, sel := range swu.Selects { + if sq, ok := sel.(*ast.SelectQuery); ok && sq != nil && sq.Format != nil { + create.Format = sq.Format.Name() + sq.Format = nil + break + } + } + } } } } @@ -3917,8 +3941,12 @@ func (p *Parser) parseDrop() *ast.DropQuery { // Handle FORMAT clause (for things like DROP TABLE ... FORMAT Null) if p.currentIs(token.FORMAT) { p.nextToken() - // Skip format name (Null, etc.) - if p.currentIs(token.NULL) || p.currentIs(token.IDENT) { + // Store format name (Null, etc.) + if p.currentIs(token.NULL) { + drop.Format = "Null" + p.nextToken() + } else if p.currentIs(token.IDENT) { + drop.Format = p.current.Value p.nextToken() } } @@ -4002,6 +4030,18 @@ func (p *Parser) parseAlter() *ast.AlterQuery { alter.Settings = p.parseSettingsList() } + // Parse FORMAT clause + if p.currentIs(token.FORMAT) { + p.nextToken() + if p.currentIs(token.NULL) { + alter.Format = "Null" + p.nextToken() + } else if p.currentIs(token.IDENT) { + alter.Format = p.current.Value + p.nextToken() + } + } + return alter } diff --git a/parser/testdata/02006_test_positional_arguments_on_cluster/metadata.json b/parser/testdata/02006_test_positional_arguments_on_cluster/metadata.json index 63647f0427..0967ef424b 100644 --- a/parser/testdata/02006_test_positional_arguments_on_cluster/metadata.json +++ b/parser/testdata/02006_test_positional_arguments_on_cluster/metadata.json @@ -1,18 +1 @@ -{ - "explain_todo": { - "stmt1": true, - "stmt11": true, - "stmt12": true, - "stmt13": true, - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt2": true, - "stmt3": true, - "stmt4": true, - "stmt5": true, - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/02681_undrop_query/metadata.json b/parser/testdata/02681_undrop_query/metadata.json index cb6306de40..3457d8d58d 100644 --- a/parser/testdata/02681_undrop_query/metadata.json +++ b/parser/testdata/02681_undrop_query/metadata.json @@ -1,12 +1,8 @@ { "explain_todo": { "stmt22": true, - "stmt23": true, - "stmt25": true, "stmt27": true, "stmt31": true, - "stmt32": true, - "stmt34": true, "stmt36": true, "stmt38": true } diff --git a/parser/testdata/02868_operator_is_not_distinct_from_priority/metadata.json b/parser/testdata/02868_operator_is_not_distinct_from_priority/metadata.json index f77f4c2ade..f8bd367cbe 100644 --- a/parser/testdata/02868_operator_is_not_distinct_from_priority/metadata.json +++ b/parser/testdata/02868_operator_is_not_distinct_from_priority/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { "stmt1": true, - "stmt10": true, - "stmt11": true, "stmt2": true, "stmt3": true, "stmt4": true, diff --git a/parser/testdata/02911_join_on_nullsafe_optimization/metadata.json b/parser/testdata/02911_join_on_nullsafe_optimization/metadata.json index 1bce63c700..0967ef424b 100644 --- a/parser/testdata/02911_join_on_nullsafe_optimization/metadata.json +++ b/parser/testdata/02911_join_on_nullsafe_optimization/metadata.json @@ -1,12 +1 @@ -{ - "explain_todo": { - "stmt14": true, - "stmt15": true, - "stmt17": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt32": true, - "stmt33": true - } -} +{} diff --git a/parser/testdata/03222_create_timeseries_table/metadata.json b/parser/testdata/03222_create_timeseries_table/metadata.json index fffcb7d38b..0967ef424b 100644 --- a/parser/testdata/03222_create_timeseries_table/metadata.json +++ b/parser/testdata/03222_create_timeseries_table/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt2": true, - "stmt3": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03224_invalid_alter/metadata.json b/parser/testdata/03224_invalid_alter/metadata.json index 57e491f07c..e6b55c3499 100644 --- a/parser/testdata/03224_invalid_alter/metadata.json +++ b/parser/testdata/03224_invalid_alter/metadata.json @@ -3,7 +3,6 @@ "stmt21": true, "stmt22": true, "stmt23": true, - "stmt30": true, "stmt31": true, "stmt32": true, "stmt33": true, diff --git a/parser/testdata/03513_nullsafe_join_storage/metadata.json b/parser/testdata/03513_nullsafe_join_storage/metadata.json index ef382ce51e..0967ef424b 100644 --- a/parser/testdata/03513_nullsafe_join_storage/metadata.json +++ b/parser/testdata/03513_nullsafe_join_storage/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt4": true, - "stmt5": true - } -} +{} diff --git a/parser/testdata/03554_json_shared_data_map_serialization_compact_part_big/metadata.json b/parser/testdata/03554_json_shared_data_map_serialization_compact_part_big/metadata.json index 9be7220609..0967ef424b 100644 --- a/parser/testdata/03554_json_shared_data_map_serialization_compact_part_big/metadata.json +++ b/parser/testdata/03554_json_shared_data_map_serialization_compact_part_big/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt22": true - } -} +{} diff --git a/parser/testdata/03554_json_shared_data_map_serialization_wide_part_big/metadata.json b/parser/testdata/03554_json_shared_data_map_serialization_wide_part_big/metadata.json index 6f1e887072..0967ef424b 100644 --- a/parser/testdata/03554_json_shared_data_map_serialization_wide_part_big/metadata.json +++ b/parser/testdata/03554_json_shared_data_map_serialization_wide_part_big/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt46": true - } -} +{} diff --git a/parser/testdata/03554_json_shared_data_map_with_buckets_serialization_compact_part_big/metadata.json b/parser/testdata/03554_json_shared_data_map_with_buckets_serialization_compact_part_big/metadata.json index 7bf4b04abe..0967ef424b 100644 --- a/parser/testdata/03554_json_shared_data_map_with_buckets_serialization_compact_part_big/metadata.json +++ b/parser/testdata/03554_json_shared_data_map_with_buckets_serialization_compact_part_big/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt33": true - } -} +{} From bfb46d8f02cacd38ac4a57578a95acf9488397f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 21:39:59 +0000 Subject: [PATCH 05/20] Fix UNION followed by INTERSECT/EXCEPT parsing - Add parseSelectWithUnionOnly for parsing EXCEPT operands that may contain UNION/UNION ALL (since UNION has higher precedence than EXCEPT) - Add parseIntersectExceptWithFirstOperand to handle UNION followed by INTERSECT/EXCEPT creating proper SelectIntersectExceptQuery wrapper - Fix parsing of queries like: select 1 union all select 2 except select 3 which should parse as: (select 1 union all select 2) except (select 3) This fixes parse errors for complex queries mixing UNION and EXCEPT. --- parser/parser.go | 128 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 0e201812d4..f3682d87dc 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -291,7 +291,8 @@ func (p *Parser) parseSelectWithUnion() *ast.SelectWithUnionQuery { } ops = append(ops, op) - // Parse the next select + // Parse the next operand - can be a SELECT with UNION/UNION ALL + // (UNION has higher precedence than EXCEPT) var nextStmt ast.Statement if p.currentIs(token.LPAREN) { p.nextToken() // skip ( @@ -302,11 +303,11 @@ func (p *Parser) parseSelectWithUnion() *ast.SelectWithUnionQuery { p.expect(token.RPAREN) nextStmt = nested } else { - sel := p.parseSelect() - if sel == nil { + // Parse SELECT with possible UNION/UNION ALL + nextStmt = p.parseSelectWithUnionOnly() + if nextStmt == nil { break } - nextStmt = sel } stmts = append(stmts, nextStmt) } @@ -331,6 +332,13 @@ func (p *Parser) parseSelectWithUnion() *ast.SelectWithUnionQuery { // Parse UNION/INTERSECT ALL/EXCEPT ALL clauses for p.currentIs(token.UNION) || p.currentIs(token.EXCEPT) || p.currentIs(token.INTERSECT) { + // Check if we hit INTERSECT/EXCEPT that should use wrapper (not ALL) + // If so, we need to wrap the current UNION result as the first operand + if p.isIntersectExceptWithWrapper() { + // Wrap current query as first operand of INTERSECT/EXCEPT + return p.parseIntersectExceptWithFirstOperand(query) + } + var setOp string if p.currentIs(token.UNION) { setOp = "UNION" @@ -376,6 +384,118 @@ func (p *Parser) parseSelectWithUnion() *ast.SelectWithUnionQuery { return query } +// parseIntersectExceptWithFirstOperand handles the case where UNION is followed by INTERSECT/EXCEPT +// The unionQuery contains the selects that form the first operand +func (p *Parser) parseIntersectExceptWithFirstOperand(unionQuery *ast.SelectWithUnionQuery) *ast.SelectWithUnionQuery { + // Collect all operands and operators + stmts := []ast.Statement{unionQuery} + var ops []string + + // Parse all INTERSECT/EXCEPT clauses + for p.isIntersectExceptWithWrapper() { + var op string + if p.currentIs(token.EXCEPT) { + op = "EXCEPT" + } else { + op = "INTERSECT" + } + p.nextToken() // skip INTERSECT/EXCEPT + + // Handle DISTINCT if present + if p.currentIs(token.DISTINCT) { + op += " DISTINCT" + p.nextToken() + } + ops = append(ops, op) + + // Parse the next select + var nextStmt ast.Statement + if p.currentIs(token.LPAREN) { + p.nextToken() // skip ( + nested := p.parseSelectWithUnion() + if nested == nil { + break + } + p.expect(token.RPAREN) + nextStmt = nested + } else { + sel := p.parseSelect() + if sel == nil { + break + } + nextStmt = sel + } + stmts = append(stmts, nextStmt) + } + + // Build the tree with proper precedence + result := buildIntersectExceptTree(stmts, ops) + + // Return wrapped in SelectWithUnionQuery + return &ast.SelectWithUnionQuery{ + Position: unionQuery.Position, + Selects: []ast.Statement{result}, + } +} + +// parseSelectWithUnionOnly parses SELECT with UNION/UNION ALL but stops at INTERSECT/EXCEPT. +// This is used for parsing operands in EXCEPT expressions where UNION has higher precedence. +func (p *Parser) parseSelectWithUnionOnly() ast.Statement { + // Parse first SELECT + sel := p.parseSelect() + if sel == nil { + return nil + } + + // Check if followed by UNION (but not INTERSECT/EXCEPT which end this operand) + if !p.currentIs(token.UNION) { + return sel + } + + // Build SelectWithUnionQuery for UNION/UNION ALL chain + query := &ast.SelectWithUnionQuery{ + Position: sel.Pos(), + Selects: []ast.Statement{sel}, + } + + // Parse UNION/UNION ALL clauses + for p.currentIs(token.UNION) { + p.nextToken() // skip UNION + + var mode string + if p.currentIs(token.ALL) { + query.UnionAll = true + mode = "ALL" + p.nextToken() + } else if p.currentIs(token.DISTINCT) { + mode = "DISTINCT" + p.nextToken() + } + query.UnionModes = append(query.UnionModes, "UNION "+mode) + + // Handle parenthesized subqueries + if p.currentIs(token.LPAREN) { + p.nextToken() // skip ( + nested := p.parseSelectWithUnion() + if nested == nil { + break + } + p.expect(token.RPAREN) + for _, s := range nested.Selects { + query.Selects = append(query.Selects, s) + } + } else { + nextSel := p.parseSelect() + if nextSel == nil { + break + } + query.Selects = append(query.Selects, nextSel) + } + } + + return query +} + // isIntersectExceptWithWrapper checks if the current token is INTERSECT or EXCEPT // that should use a SelectIntersectExceptQuery wrapper. // Only INTERSECT ALL and EXCEPT ALL are flattened (no wrapper). From e0ee524da6cb7bfbeb9ba593b52e884d35344fd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 21:54:13 +0000 Subject: [PATCH 06/20] Support nested column names in ALTER TABLE ADD COLUMN Parse dotted column names like n.y for ClickHouse nested columns. After parsing the initial identifier, the parser now continues to accumulate dot-separated parts into a single column name. This fixes ALTER TABLE ADD COLUMN statements with nested column references and enables many additional tests across multiple test files. --- parser/parser.go | 11 +++++++++++ parser/testdata/00030_alter_table/metadata.json | 1 - parser/testdata/00061_merge_tree_alter/metadata.json | 3 +-- .../metadata.json | 3 +-- .../testdata/00147_alter_nested_default/metadata.json | 6 +----- parser/testdata/00262_alter_alias/metadata.json | 7 +------ parser/testdata/00392_enum_nested_alter/metadata.json | 10 +--------- .../testdata/00576_nested_and_prewhere/metadata.json | 7 +------ .../testdata/01576_alias_column_rewrite/metadata.json | 1 - .../metadata.json | 7 +------ .../02918_alter_temporary_table/metadata.json | 1 - .../metadata.json | 4 +--- .../03672_nested_array_nested_tuple/metadata.json | 2 -- 13 files changed, 19 insertions(+), 44 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index f3682d87dc..cce942f93d 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -3319,9 +3319,20 @@ func (p *Parser) parseColumnDeclaration() *ast.ColumnDeclaration { } // Parse column name (can be identifier or keyword like KEY) + // Also handles nested column names like n.y if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { col.Name = p.current.Value p.nextToken() + // Handle nested column names (e.g., n.y for nested columns) + for p.currentIs(token.DOT) { + p.nextToken() // skip . + if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() { + col.Name += "." + p.current.Value + p.nextToken() + } else { + break + } + } } else { return nil } diff --git a/parser/testdata/00030_alter_table/metadata.json b/parser/testdata/00030_alter_table/metadata.json index c04bc590a0..eb237b26b8 100644 --- a/parser/testdata/00030_alter_table/metadata.json +++ b/parser/testdata/00030_alter_table/metadata.json @@ -3,7 +3,6 @@ "stmt14": true, "stmt15": true, "stmt16": true, - "stmt19": true, "stmt20": true, "stmt9": true } diff --git a/parser/testdata/00061_merge_tree_alter/metadata.json b/parser/testdata/00061_merge_tree_alter/metadata.json index a8ce03a4e7..5640837dca 100644 --- a/parser/testdata/00061_merge_tree_alter/metadata.json +++ b/parser/testdata/00061_merge_tree_alter/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt33": true, - "stmt37": true, - "stmt41": true + "stmt37": true } } diff --git a/parser/testdata/00062_replicated_merge_tree_alter_zookeeper_long/metadata.json b/parser/testdata/00062_replicated_merge_tree_alter_zookeeper_long/metadata.json index 43f7a22336..a8ef33f487 100644 --- a/parser/testdata/00062_replicated_merge_tree_alter_zookeeper_long/metadata.json +++ b/parser/testdata/00062_replicated_merge_tree_alter_zookeeper_long/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt53": true, - "stmt59": true, - "stmt65": true + "stmt59": true } } diff --git a/parser/testdata/00147_alter_nested_default/metadata.json b/parser/testdata/00147_alter_nested_default/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/00147_alter_nested_default/metadata.json +++ b/parser/testdata/00147_alter_nested_default/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/00262_alter_alias/metadata.json b/parser/testdata/00262_alter_alias/metadata.json index a56c7cdb0b..0967ef424b 100644 --- a/parser/testdata/00262_alter_alias/metadata.json +++ b/parser/testdata/00262_alter_alias/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt12": true - } -} +{} diff --git a/parser/testdata/00392_enum_nested_alter/metadata.json b/parser/testdata/00392_enum_nested_alter/metadata.json index 6a047f388a..482ec8aca4 100644 --- a/parser/testdata/00392_enum_nested_alter/metadata.json +++ b/parser/testdata/00392_enum_nested_alter/metadata.json @@ -1,17 +1,9 @@ { "explain_todo": { - "stmt11": true, - "stmt13": true, - "stmt15": true, - "stmt17": true, "stmt21": true, - "stmt23": true, "stmt24": true, "stmt29": true, - "stmt31": true, "stmt4": true, - "stmt6": true, - "stmt7": true, - "stmt9": true + "stmt7": true } } diff --git a/parser/testdata/00576_nested_and_prewhere/metadata.json b/parser/testdata/00576_nested_and_prewhere/metadata.json index 943b275814..0967ef424b 100644 --- a/parser/testdata/00576_nested_and_prewhere/metadata.json +++ b/parser/testdata/00576_nested_and_prewhere/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt4": true, - "stmt6": true - } -} +{} diff --git a/parser/testdata/01576_alias_column_rewrite/metadata.json b/parser/testdata/01576_alias_column_rewrite/metadata.json index 2581609646..95cd2c2b48 100644 --- a/parser/testdata/01576_alias_column_rewrite/metadata.json +++ b/parser/testdata/01576_alias_column_rewrite/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt23": true, "stmt42": true } } diff --git a/parser/testdata/02559_nested_multiple_levels_default/metadata.json b/parser/testdata/02559_nested_multiple_levels_default/metadata.json index c2bbb632ab..0967ef424b 100644 --- a/parser/testdata/02559_nested_multiple_levels_default/metadata.json +++ b/parser/testdata/02559_nested_multiple_levels_default/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt13": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/02918_alter_temporary_table/metadata.json b/parser/testdata/02918_alter_temporary_table/metadata.json index 7f26f4a68c..07cb9f9814 100644 --- a/parser/testdata/02918_alter_temporary_table/metadata.json +++ b/parser/testdata/02918_alter_temporary_table/metadata.json @@ -3,7 +3,6 @@ "stmt13": true, "stmt14": true, "stmt15": true, - "stmt18": true, "stmt19": true, "stmt5": true, "stmt8": true diff --git a/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json b/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json index 2712c834b2..7512c65e31 100644 --- a/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json +++ b/parser/testdata/03628_subcolumns_of_columns_with_dot_in_name/metadata.json @@ -2,9 +2,7 @@ "explain_todo": { "stmt11": true, "stmt15": true, - "stmt20": true, "stmt24": true, - "stmt27": true, - "stmt31": true + "stmt27": true } } diff --git a/parser/testdata/03672_nested_array_nested_tuple/metadata.json b/parser/testdata/03672_nested_array_nested_tuple/metadata.json index 5fc7cc1cab..60106a3b25 100644 --- a/parser/testdata/03672_nested_array_nested_tuple/metadata.json +++ b/parser/testdata/03672_nested_array_nested_tuple/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { - "stmt10": true, "stmt3": true, - "stmt4": true, "stmt9": true } } From 3625405d49698f9b06e5727415aaa4f6dbcb8e7c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 22:10:43 +0000 Subject: [PATCH 07/20] Fix negative enum values and escape sequences in CAST type parameters - Handle UnaryExpr (e.g., -100) in formatBinaryExprForType - Add escapeStringForTypeParam for proper double-escaping of special characters in type parameters (backslash, tab, quotes, etc.) - Type parameters in DataType need extra escaping since they're embedded inside another string literal in EXPLAIN output This fixes CAST to Enum types with negative values and escape sequences. --- internal/explain/format.go | 45 ++++++++++++++++++- .../00298_enum_width_and_cast/metadata.json | 2 +- .../testdata/01064_arrayROCAUC/metadata.json | 9 +--- .../testdata/03272_arrayAUCPR/metadata.json | 6 +-- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/internal/explain/format.go b/internal/explain/format.go index 4de841329e..e52e8d18ba 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -74,6 +74,36 @@ func escapeStringLiteral(s string) string { return sb.String() } +// escapeStringForTypeParam escapes special characters for use in type parameters +// Uses extra escaping because type strings are embedded inside another string literal +func escapeStringForTypeParam(s string) string { + var sb strings.Builder + for i := 0; i < len(s); i++ { + b := s[i] + switch b { + case '\\': + sb.WriteString("\\\\\\\\\\\\\\\\") // backslash becomes 8 backslashes + case '\'': + sb.WriteString("\\\\\\\\\\'") // single quote becomes 5 backslashes + quote + case '\n': + sb.WriteString("\\\\\\\\n") // newline becomes \\\\n + case '\t': + sb.WriteString("\\\\\\\\t") // tab becomes \\\\t + case '\r': + sb.WriteString("\\\\\\\\r") // carriage return becomes \\\\r + case '\x00': + sb.WriteString("\\\\\\\\0") // null becomes \\\\0 + case '\b': + sb.WriteString("\\\\\\\\b") // backspace becomes \\\\b + case '\f': + sb.WriteString("\\\\\\\\f") // form feed becomes \\\\f + default: + sb.WriteByte(b) + } + } + return sb.String() +} + // FormatLiteral formats a literal value for EXPLAIN AST output func FormatLiteral(lit *ast.Literal) string { switch lit.Type { @@ -270,7 +300,9 @@ func formatBinaryExprForType(expr *ast.BinaryExpr) string { // Format left side if lit, ok := expr.Left.(*ast.Literal); ok { if lit.Type == ast.LiteralString { - left = fmt.Sprintf("\\\\\\'%s\\\\\\'", lit.Value) + // Use extra escaping for type parameters since they're embedded in another string literal + escaped := escapeStringForTypeParam(fmt.Sprintf("%v", lit.Value)) + left = fmt.Sprintf("\\\\\\'%s\\\\\\'", escaped) } else { left = fmt.Sprintf("%v", lit.Value) } @@ -285,6 +317,9 @@ func formatBinaryExprForType(expr *ast.BinaryExpr) string { right = fmt.Sprintf("%v", lit.Value) } else if ident, ok := expr.Right.(*ast.Identifier); ok { right = ident.Name() + } else if unary, ok := expr.Right.(*ast.UnaryExpr); ok { + // Handle unary expressions like -100 + right = formatUnaryExprForType(unary) } else { right = fmt.Sprintf("%v", expr.Right) } @@ -292,6 +327,14 @@ func formatBinaryExprForType(expr *ast.BinaryExpr) string { return left + " " + expr.Op + " " + right } +// formatUnaryExprForType formats a unary expression for use in type parameters (e.g., -100) +func formatUnaryExprForType(expr *ast.UnaryExpr) string { + if lit, ok := expr.Operand.(*ast.Literal); ok { + return expr.Op + fmt.Sprintf("%v", lit.Value) + } + return expr.Op + fmt.Sprintf("%v", expr.Operand) +} + // NormalizeFunctionName normalizes function names to match ClickHouse's EXPLAIN AST output func NormalizeFunctionName(name string) string { // ClickHouse normalizes certain function names in EXPLAIN AST diff --git a/parser/testdata/00298_enum_width_and_cast/metadata.json b/parser/testdata/00298_enum_width_and_cast/metadata.json index 92ffdfe8b5..0967ef424b 100644 --- a/parser/testdata/00298_enum_width_and_cast/metadata.json +++ b/parser/testdata/00298_enum_width_and_cast/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt10":true}} +{} diff --git a/parser/testdata/01064_arrayROCAUC/metadata.json b/parser/testdata/01064_arrayROCAUC/metadata.json index a48eb63337..0967ef424b 100644 --- a/parser/testdata/01064_arrayROCAUC/metadata.json +++ b/parser/testdata/01064_arrayROCAUC/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt21": true, - "stmt37": true, - "stmt5": true, - "stmt53": true - } -} +{} diff --git a/parser/testdata/03272_arrayAUCPR/metadata.json b/parser/testdata/03272_arrayAUCPR/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/03272_arrayAUCPR/metadata.json +++ b/parser/testdata/03272_arrayAUCPR/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} From ad2ffd882f4effac5c701f0a3b390dd2025b92ea Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 22:17:15 +0000 Subject: [PATCH 08/20] Handle function calls in aliased array literals for Function array format When an array literal with an alias contains function calls (e.g., [hex(number)] AS i), it should be rendered as "Function array" instead of "Literal Array_[...]". This aligns with ClickHouse's EXPLAIN AST output which expands array literals containing expressions. Added check for *ast.FunctionCall in explainAliasedExpr's array handling to trigger Function array format when arrays contain function calls. This fixes multiple tests including 00113_shard_group_array, 03447_window_functions_distinct, 03261_tuple_map_to_json_cast, and 03393_non_constant_second_argument_for_in. --- internal/explain/expressions.go | 5 +++++ parser/testdata/00113_shard_group_array/metadata.json | 2 +- parser/testdata/00255_array_concat_string/metadata.json | 9 +-------- parser/testdata/00348_tuples/metadata.json | 3 +-- .../00378_json_quote_64bit_integers/metadata.json | 2 +- parser/testdata/00855_join_with_array_join/metadata.json | 7 +------ parser/testdata/00945_bloom_filter_index/metadata.json | 6 +----- .../01414_low_cardinality_nullable/metadata.json | 6 +----- .../metadata.json | 7 +------ parser/testdata/02737_arrayJaccardIndex/metadata.json | 2 +- .../metadata.json | 1 - .../testdata/03240_array_element_or_null/metadata.json | 7 +------ .../testdata/03261_tuple_map_to_json_cast/metadata.json | 6 +----- .../metadata.json | 8 +------- .../03447_window_functions_distinct/metadata.json | 6 +----- 15 files changed, 18 insertions(+), 59 deletions(-) diff --git a/internal/explain/expressions.go b/internal/explain/expressions.go index a2717b38fc..f5aa8be482 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -463,6 +463,11 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { needsFunctionFormat = true break } + // Check for function calls - use Function array + if _, ok := expr.(*ast.FunctionCall); ok { + needsFunctionFormat = true + break + } } if needsFunctionFormat { // Render as Function array with alias diff --git a/parser/testdata/00113_shard_group_array/metadata.json b/parser/testdata/00113_shard_group_array/metadata.json index 7bfa5aa5a4..0967ef424b 100644 --- a/parser/testdata/00113_shard_group_array/metadata.json +++ b/parser/testdata/00113_shard_group_array/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt21":true,"stmt25":true}} +{} diff --git a/parser/testdata/00255_array_concat_string/metadata.json b/parser/testdata/00255_array_concat_string/metadata.json index 4a3785ad92..0967ef424b 100644 --- a/parser/testdata/00255_array_concat_string/metadata.json +++ b/parser/testdata/00255_array_concat_string/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt15": true, - "stmt16": true, - "stmt17": true, - "stmt18": true - } -} +{} diff --git a/parser/testdata/00348_tuples/metadata.json b/parser/testdata/00348_tuples/metadata.json index 114ee38611..c45b7602ba 100644 --- a/parser/testdata/00348_tuples/metadata.json +++ b/parser/testdata/00348_tuples/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt12": true, - "stmt7": true + "stmt12": true } } diff --git a/parser/testdata/00378_json_quote_64bit_integers/metadata.json b/parser/testdata/00378_json_quote_64bit_integers/metadata.json index 75e7fabe8b..0967ef424b 100644 --- a/parser/testdata/00378_json_quote_64bit_integers/metadata.json +++ b/parser/testdata/00378_json_quote_64bit_integers/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt10":true,"stmt11":true,"stmt12":true,"stmt6":true,"stmt7":true,"stmt8":true}} +{} diff --git a/parser/testdata/00855_join_with_array_join/metadata.json b/parser/testdata/00855_join_with_array_join/metadata.json index 42691a609c..0967ef424b 100644 --- a/parser/testdata/00855_join_with_array_join/metadata.json +++ b/parser/testdata/00855_join_with_array_join/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt22": true, - "stmt23": true - } -} +{} diff --git a/parser/testdata/00945_bloom_filter_index/metadata.json b/parser/testdata/00945_bloom_filter_index/metadata.json index 7c0763ffaa..0967ef424b 100644 --- a/parser/testdata/00945_bloom_filter_index/metadata.json +++ b/parser/testdata/00945_bloom_filter_index/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt152": true - } -} +{} diff --git a/parser/testdata/01414_low_cardinality_nullable/metadata.json b/parser/testdata/01414_low_cardinality_nullable/metadata.json index 7ad5569408..0967ef424b 100644 --- a/parser/testdata/01414_low_cardinality_nullable/metadata.json +++ b/parser/testdata/01414_low_cardinality_nullable/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt9": true - } -} +{} diff --git a/parser/testdata/01421_array_nullable_element_nullable_index/metadata.json b/parser/testdata/01421_array_nullable_element_nullable_index/metadata.json index e5e4780514..0967ef424b 100644 --- a/parser/testdata/01421_array_nullable_element_nullable_index/metadata.json +++ b/parser/testdata/01421_array_nullable_element_nullable_index/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt1": true, - "stmt3": true - } -} +{} diff --git a/parser/testdata/02737_arrayJaccardIndex/metadata.json b/parser/testdata/02737_arrayJaccardIndex/metadata.json index a133290734..0967ef424b 100644 --- a/parser/testdata/02737_arrayJaccardIndex/metadata.json +++ b/parser/testdata/02737_arrayJaccardIndex/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt8":true}} +{} diff --git a/parser/testdata/03033_analyzer_resolve_from_parent_scope/metadata.json b/parser/testdata/03033_analyzer_resolve_from_parent_scope/metadata.json index 9a8cc69c0b..b65b07d7a6 100644 --- a/parser/testdata/03033_analyzer_resolve_from_parent_scope/metadata.json +++ b/parser/testdata/03033_analyzer_resolve_from_parent_scope/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt2": true, "stmt4": true } } diff --git a/parser/testdata/03240_array_element_or_null/metadata.json b/parser/testdata/03240_array_element_or_null/metadata.json index 60e53ef924..0967ef424b 100644 --- a/parser/testdata/03240_array_element_or_null/metadata.json +++ b/parser/testdata/03240_array_element_or_null/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt45": true, - "stmt47": true - } -} +{} diff --git a/parser/testdata/03261_tuple_map_to_json_cast/metadata.json b/parser/testdata/03261_tuple_map_to_json_cast/metadata.json index f4c74e32be..0967ef424b 100644 --- a/parser/testdata/03261_tuple_map_to_json_cast/metadata.json +++ b/parser/testdata/03261_tuple_map_to_json_cast/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt10": true - } -} +{} diff --git a/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json b/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json index c89ada73f6..0967ef424b 100644 --- a/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json +++ b/parser/testdata/03393_non_constant_second_argument_for_in/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt43": true, - "stmt44": true, - "stmt45": true - } -} +{} diff --git a/parser/testdata/03447_window_functions_distinct/metadata.json b/parser/testdata/03447_window_functions_distinct/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/03447_window_functions_distinct/metadata.json +++ b/parser/testdata/03447_window_functions_distinct/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} From 2e536b181ee3dc79c40808e6b00de2f8b4e28dd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 22:27:33 +0000 Subject: [PATCH 09/20] Parse hexadecimal float literals as Float64 values Go 1.13+ supports parsing hex floats via strconv.ParseFloat. Updated the parser to properly parse hex float literals (e.g., 0x1.f7ced916872b0p-4) as Float64 values instead of storing them as strings. This fixes 01433_hex_float and 03747_float_parsing_subnormal tests. --- parser/expression.go | 14 ++++++++++---- parser/testdata/01433_hex_float/metadata.json | 6 +----- .../metadata.json | 1 - .../03747_float_parsing_subnormal/metadata.json | 8 +------- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/parser/expression.go b/parser/expression.go index fd6275abb3..26b8007f3f 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -843,10 +843,16 @@ func (p *Parser) parseNumber() ast.Expression { lit.Source = value // Preserve original source text (e.g., "0.0" vs "0") } } else if isHexFloat { - // Parse hex float (Go doesn't support this directly, approximate) - // For now, store as string - ClickHouse will interpret it - lit.Type = ast.LiteralString - lit.Value = value + // Parse hex float (Go 1.13+ supports this via ParseFloat) + f, err := strconv.ParseFloat(value, 64) + if err != nil { + lit.Type = ast.LiteralString + lit.Value = value + } else { + lit.Type = ast.LiteralFloat + lit.Value = f + lit.Source = value // Preserve original source text + } } else { // Determine the base for parsing // - 0x/0X: hex (base 16) diff --git a/parser/testdata/01433_hex_float/metadata.json b/parser/testdata/01433_hex_float/metadata.json index b65b07d7a6..0967ef424b 100644 --- a/parser/testdata/01433_hex_float/metadata.json +++ b/parser/testdata/01433_hex_float/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt4": true - } -} +{} diff --git a/parser/testdata/02354_numeric_literals_with_underscores/metadata.json b/parser/testdata/02354_numeric_literals_with_underscores/metadata.json index 0f293987f1..dbdbb76d4f 100644 --- a/parser/testdata/02354_numeric_literals_with_underscores/metadata.json +++ b/parser/testdata/02354_numeric_literals_with_underscores/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt5": true, "stmt6": true } } diff --git a/parser/testdata/03747_float_parsing_subnormal/metadata.json b/parser/testdata/03747_float_parsing_subnormal/metadata.json index e368f56fd8..0967ef424b 100644 --- a/parser/testdata/03747_float_parsing_subnormal/metadata.json +++ b/parser/testdata/03747_float_parsing_subnormal/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt12": true, - "stmt9": true - } -} +{} From fa3fdbd7f4b7d4ef29ac25090131a60c4a83de1e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 22:31:28 +0000 Subject: [PATCH 10/20] Fix empty tuple ExpressionList formatting without children count For empty tuples with aliases like `() AS a`, output `ExpressionList` without the `(children 0)` suffix to match ClickHouse's EXPLAIN AST. This fixes 02891_empty_tuple test. --- internal/explain/expressions.go | 7 ++++++- parser/testdata/02891_empty_tuple/metadata.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/explain/expressions.go b/internal/explain/expressions.go index f5aa8be482..330c330d5a 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -436,7 +436,12 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { 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)) + // For empty ExpressionList, don't include children count + if len(exprs) > 0 { + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(exprs)) + } else { + fmt.Fprintf(sb, "%s ExpressionList\n", indent) + } for _, expr := range exprs { Node(sb, expr, depth+2) } diff --git a/parser/testdata/02891_empty_tuple/metadata.json b/parser/testdata/02891_empty_tuple/metadata.json index af48d4c110..0967ef424b 100644 --- a/parser/testdata/02891_empty_tuple/metadata.json +++ b/parser/testdata/02891_empty_tuple/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt2":true}} +{} From 1fdce1bb4bee64e11dca0fd593ba375af6d781b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 22:38:56 +0000 Subject: [PATCH 11/20] Detect non-literal elements in nested arrays/tuples for Function format Add containsNonLiteralInNested helper that recursively checks nested arrays and tuples for identifiers and function calls, ensuring proper Function tuple/array output format instead of Literal format. --- internal/explain/expressions.go | 32 +++++++++++++++++++ .../01318_map_add_map_subtract/metadata.json | 6 +--- .../01318_map_populate_series/metadata.json | 6 +--- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/internal/explain/expressions.go b/internal/explain/expressions.go index 330c330d5a..bb380b2db9 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -235,6 +235,31 @@ func containsNonLiteralExpressions(exprs []ast.Expression) bool { return false } +// containsNonLiteralInNested checks if an array or tuple literal contains +// non-literal elements at any nesting level (identifiers, function calls, etc.) +func containsNonLiteralInNested(lit *ast.Literal) bool { + if lit.Type != ast.LiteralArray && lit.Type != ast.LiteralTuple { + return false + } + exprs, ok := lit.Value.([]ast.Expression) + if !ok { + return false + } + for _, e := range exprs { + // Check if this element is a non-literal (identifier, function call, etc.) + if _, isLit := e.(*ast.Literal); !isLit { + return true + } + // Recursively check nested arrays/tuples + if innerLit, ok := e.(*ast.Literal); ok { + if containsNonLiteralInNested(innerLit) { + return true + } + } + } + return false +} + // containsTuples checks if a slice of expressions contains any tuple literals func containsTuples(exprs []ast.Expression) bool { for _, e := range exprs { @@ -432,6 +457,13 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { needsFunctionFormat = true break } + // Also check if nested arrays/tuples contain non-literal elements + if lit, ok := expr.(*ast.Literal); ok { + if containsNonLiteralInNested(lit) { + needsFunctionFormat = true + break + } + } } if needsFunctionFormat { // Render as Function tuple with alias diff --git a/parser/testdata/01318_map_add_map_subtract/metadata.json b/parser/testdata/01318_map_add_map_subtract/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/01318_map_add_map_subtract/metadata.json +++ b/parser/testdata/01318_map_add_map_subtract/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/01318_map_populate_series/metadata.json b/parser/testdata/01318_map_populate_series/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/01318_map_populate_series/metadata.json +++ b/parser/testdata/01318_map_populate_series/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} From af476da89feeb5dd6442c229646015ebd3333d96 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 22:55:32 +0000 Subject: [PATCH 12/20] Fix nested parenthesized SELECT parsing at statement level The parseParenthesizedSelect function was skipping parenthesized content when it didn't find SELECT or WITH immediately after the opening paren. Now it also allows LPAREN to trigger proper recursive parsing of nested parenthesized SELECT statements. --- parser/parser.go | 6 +++--- parser/testdata/00622_select_in_parens/metadata.json | 3 +-- parser/testdata/01068_parens/metadata.json | 6 +----- .../02004_intersect_except_distinct_operators/metadata.json | 3 +-- .../testdata/02004_intersect_except_operators/metadata.json | 3 +-- 5 files changed, 7 insertions(+), 14 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index cce942f93d..0b40eefaa4 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -5658,9 +5658,9 @@ func (p *Parser) parseParenthesizedSelect() *ast.SelectWithUnionQuery { pos := p.current.Pos p.nextToken() // skip ( - // Check if this is actually a SELECT statement - if !p.currentIs(token.SELECT) && !p.currentIs(token.WITH) { - // Not a SELECT, just skip until we find closing paren + // Check if this is actually a SELECT statement or nested parentheses + if !p.currentIs(token.SELECT) && !p.currentIs(token.WITH) && !p.currentIs(token.LPAREN) { + // Not a SELECT and not nested parens, just skip until we find closing paren depth := 1 for depth > 0 && !p.currentIs(token.EOF) { if p.currentIs(token.LPAREN) { diff --git a/parser/testdata/00622_select_in_parens/metadata.json b/parser/testdata/00622_select_in_parens/metadata.json index bc141058a4..ef58f80315 100644 --- a/parser/testdata/00622_select_in_parens/metadata.json +++ b/parser/testdata/00622_select_in_parens/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt2": true, - "stmt3": true + "stmt2": true } } diff --git a/parser/testdata/01068_parens/metadata.json b/parser/testdata/01068_parens/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/01068_parens/metadata.json +++ b/parser/testdata/01068_parens/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/02004_intersect_except_distinct_operators/metadata.json b/parser/testdata/02004_intersect_except_distinct_operators/metadata.json index c80f910d88..ed99a48b08 100644 --- a/parser/testdata/02004_intersect_except_distinct_operators/metadata.json +++ b/parser/testdata/02004_intersect_except_distinct_operators/metadata.json @@ -12,7 +12,6 @@ "stmt36": true, "stmt38": true, "stmt39": true, - "stmt42": true, - "stmt45": true + "stmt42": true } } diff --git a/parser/testdata/02004_intersect_except_operators/metadata.json b/parser/testdata/02004_intersect_except_operators/metadata.json index a064b0addd..72b95f6c78 100644 --- a/parser/testdata/02004_intersect_except_operators/metadata.json +++ b/parser/testdata/02004_intersect_except_operators/metadata.json @@ -12,7 +12,6 @@ "stmt34": true, "stmt36": true, "stmt37": true, - "stmt40": true, - "stmt43": true + "stmt40": true } } From c01e6880dce5f1b009001371112078d38023e713 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 22:59:55 +0000 Subject: [PATCH 13/20] Normalize negated zero to UInt64_0 in EXPLAIN output ClickHouse normalizes -0 to UInt64_0 since negative zero equals zero. This fix ensures the explain output matches for negate(-0) expressions. --- internal/explain/expressions.go | 17 +++++++++++++++-- .../01566_negate_formatting/metadata.json | 6 +----- .../metadata.json | 9 +-------- .../metadata.json | 3 +-- .../03404_bfloat16_insert_values/metadata.json | 2 -- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/internal/explain/expressions.go b/internal/explain/expressions.go index bb380b2db9..a2d6c9c877 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -402,10 +402,23 @@ func explainUnaryExpr(sb *strings.Builder, n *ast.UnaryExpr, indent string, dept // Convert positive integer to negative switch val := lit.Value.(type) { case int64: - fmt.Fprintf(sb, "%sLiteral Int64_%d\n", indent, -val) + negVal := -val + // ClickHouse normalizes -0 to UInt64_0 + if negVal == 0 { + fmt.Fprintf(sb, "%sLiteral UInt64_0\n", indent) + } else if negVal > 0 { + fmt.Fprintf(sb, "%sLiteral UInt64_%d\n", indent, negVal) + } else { + fmt.Fprintf(sb, "%sLiteral Int64_%d\n", indent, negVal) + } return case uint64: - fmt.Fprintf(sb, "%sLiteral Int64_-%d\n", indent, val) + // ClickHouse normalizes -0 to UInt64_0 + if val == 0 { + fmt.Fprintf(sb, "%sLiteral UInt64_0\n", indent) + } else { + fmt.Fprintf(sb, "%sLiteral Int64_-%d\n", indent, val) + } return } case ast.LiteralFloat: diff --git a/parser/testdata/01566_negate_formatting/metadata.json b/parser/testdata/01566_negate_formatting/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/01566_negate_formatting/metadata.json +++ b/parser/testdata/01566_negate_formatting/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} diff --git a/parser/testdata/02782_inconsistent_formatting_and_constant_folding/metadata.json b/parser/testdata/02782_inconsistent_formatting_and_constant_folding/metadata.json index 2837de1e13..0967ef424b 100644 --- a/parser/testdata/02782_inconsistent_formatting_and_constant_folding/metadata.json +++ b/parser/testdata/02782_inconsistent_formatting_and_constant_folding/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt1": true, - "stmt6": true, - "stmt7": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/02911_cte_invalid_query_analysis/metadata.json b/parser/testdata/02911_cte_invalid_query_analysis/metadata.json index 968942c3e9..19830977ac 100644 --- a/parser/testdata/02911_cte_invalid_query_analysis/metadata.json +++ b/parser/testdata/02911_cte_invalid_query_analysis/metadata.json @@ -2,7 +2,6 @@ "explain_todo": { "stmt4": true, "stmt5": true, - "stmt6": true, - "stmt8": true + "stmt6": true } } diff --git a/parser/testdata/03404_bfloat16_insert_values/metadata.json b/parser/testdata/03404_bfloat16_insert_values/metadata.json index 7614c9f5b3..24c397911d 100644 --- a/parser/testdata/03404_bfloat16_insert_values/metadata.json +++ b/parser/testdata/03404_bfloat16_insert_values/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { - "stmt10": true, "stmt14": true, - "stmt16": true, "stmt8": true } } From 2064d4b43cbedce20c0cbdc26b7c9916a36e9f1c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 23:02:54 +0000 Subject: [PATCH 14/20] Add BFloat16 to isDataTypeName for proper type recognition When parsing type parameters like QBit(BFloat16, 3), BFloat16 needs to be recognized as a data type name so it's output as DataType instead of Identifier. --- parser/parser.go | 2 +- .../02354_vector_search_queries/metadata.json | 2 +- .../metadata.json | 6 +----- .../metadata.json | 9 +-------- .../metadata.json | 11 +---------- .../testdata/03364_qbit_negative/metadata.json | 2 +- .../metadata.json | 9 ++++++++- .../metadata.json | 2 +- .../03367_bfloat16_tuple_final/metadata.json | 6 +----- .../03368_qbit_subcolumns/metadata.json | 7 +------ .../testdata/03369_bfloat16_map/metadata.json | 6 +----- .../metadata.json | 18 +++++++++++++++++- .../metadata.json | 6 +----- .../03372_qbit_mergetree_1/metadata.json | 8 +------- .../03372_qbit_mergetree_2/metadata.json | 9 +-------- .../metadata.json | 7 ++++++- .../metadata.json | 2 +- 17 files changed, 45 insertions(+), 67 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index 0b40eefaa4..51b310d144 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -3618,7 +3618,7 @@ func (p *Parser) isDataTypeName(name string) bool { types := []string{ "INT", "INT8", "INT16", "INT32", "INT64", "INT128", "INT256", "UINT8", "UINT16", "UINT32", "UINT64", "UINT128", "UINT256", - "FLOAT32", "FLOAT64", "FLOAT", + "FLOAT32", "FLOAT64", "FLOAT", "BFLOAT16", "DECIMAL", "DECIMAL32", "DECIMAL64", "DECIMAL128", "DECIMAL256", "STRING", "FIXEDSTRING", "UUID", "DATE", "DATE32", "DATETIME", "DATETIME64", diff --git a/parser/testdata/02354_vector_search_queries/metadata.json b/parser/testdata/02354_vector_search_queries/metadata.json index e10d7d44e2..0967ef424b 100644 --- a/parser/testdata/02354_vector_search_queries/metadata.json +++ b/parser/testdata/02354_vector_search_queries/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt90":true}} +{} diff --git a/parser/testdata/02354_vector_search_reference_vector_types/metadata.json b/parser/testdata/02354_vector_search_reference_vector_types/metadata.json index ab9202e88e..0967ef424b 100644 --- a/parser/testdata/02354_vector_search_reference_vector_types/metadata.json +++ b/parser/testdata/02354_vector_search_reference_vector_types/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt11": true - } -} +{} diff --git a/parser/testdata/02354_vector_search_rescoring_distance_in_select_list/metadata.json b/parser/testdata/02354_vector_search_rescoring_distance_in_select_list/metadata.json index 25ecea4299..0967ef424b 100644 --- a/parser/testdata/02354_vector_search_rescoring_distance_in_select_list/metadata.json +++ b/parser/testdata/02354_vector_search_rescoring_distance_in_select_list/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt14": true, - "stmt16": true, - "stmt21": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/03363_qbit_create_insert_select/metadata.json b/parser/testdata/03363_qbit_create_insert_select/metadata.json index b023fcac0e..0967ef424b 100644 --- a/parser/testdata/03363_qbit_create_insert_select/metadata.json +++ b/parser/testdata/03363_qbit_create_insert_select/metadata.json @@ -1,10 +1 @@ -{ - "explain_todo": { - "stmt32": true, - "stmt5": true, - "stmt56": true, - "stmt73": true, - "stmt79": true, - "stmt82": true - } -} +{} diff --git a/parser/testdata/03364_qbit_negative/metadata.json b/parser/testdata/03364_qbit_negative/metadata.json index 51dfabe749..0967ef424b 100644 --- a/parser/testdata/03364_qbit_negative/metadata.json +++ b/parser/testdata/03364_qbit_negative/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt3":true}} +{} diff --git a/parser/testdata/03365_qbit_casts_as_from_array/metadata.json b/parser/testdata/03365_qbit_casts_as_from_array/metadata.json index 6947807c15..9d32d2ea78 100644 --- a/parser/testdata/03365_qbit_casts_as_from_array/metadata.json +++ b/parser/testdata/03365_qbit_casts_as_from_array/metadata.json @@ -1 +1,8 @@ -{"explain_todo":{"stmt13":true,"stmt14":true,"stmt15":true,"stmt20":true,"stmt21":true,"stmt22":true,"stmt23":true}} +{ + "explain_todo": { + "stmt20": true, + "stmt21": true, + "stmt22": true, + "stmt23": true + } +} diff --git a/parser/testdata/03366_qbit_array_map_populate/metadata.json b/parser/testdata/03366_qbit_array_map_populate/metadata.json index 3cca3b6166..0967ef424b 100644 --- a/parser/testdata/03366_qbit_array_map_populate/metadata.json +++ b/parser/testdata/03366_qbit_array_map_populate/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt14":true}} +{} diff --git a/parser/testdata/03367_bfloat16_tuple_final/metadata.json b/parser/testdata/03367_bfloat16_tuple_final/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/03367_bfloat16_tuple_final/metadata.json +++ b/parser/testdata/03367_bfloat16_tuple_final/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/03368_qbit_subcolumns/metadata.json b/parser/testdata/03368_qbit_subcolumns/metadata.json index 8606858586..0967ef424b 100644 --- a/parser/testdata/03368_qbit_subcolumns/metadata.json +++ b/parser/testdata/03368_qbit_subcolumns/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt16": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03369_bfloat16_map/metadata.json b/parser/testdata/03369_bfloat16_map/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/03369_bfloat16_map/metadata.json +++ b/parser/testdata/03369_bfloat16_map/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/03369_l2_distance_transposed_variadic/metadata.json b/parser/testdata/03369_l2_distance_transposed_variadic/metadata.json index c89079891b..2fc9d7d80a 100644 --- a/parser/testdata/03369_l2_distance_transposed_variadic/metadata.json +++ b/parser/testdata/03369_l2_distance_transposed_variadic/metadata.json @@ -1 +1,17 @@ -{"explain_todo":{"stmt10":true,"stmt17":true,"stmt18":true,"stmt19":true,"stmt26":true,"stmt27":true,"stmt28":true,"stmt33":true,"stmt34":true,"stmt35":true,"stmt37":true,"stmt4":true,"stmt8":true,"stmt9":true}} +{ + "explain_todo": { + "stmt10": true, + "stmt17": true, + "stmt18": true, + "stmt19": true, + "stmt26": true, + "stmt27": true, + "stmt28": true, + "stmt33": true, + "stmt34": true, + "stmt35": true, + "stmt37": true, + "stmt8": true, + "stmt9": true + } +} diff --git a/parser/testdata/03371_bfloat16_special_values/metadata.json b/parser/testdata/03371_bfloat16_special_values/metadata.json index 05f2588d5d..0967ef424b 100644 --- a/parser/testdata/03371_bfloat16_special_values/metadata.json +++ b/parser/testdata/03371_bfloat16_special_values/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt31": true - } -} +{} diff --git a/parser/testdata/03372_qbit_mergetree_1/metadata.json b/parser/testdata/03372_qbit_mergetree_1/metadata.json index 13a7459d03..0967ef424b 100644 --- a/parser/testdata/03372_qbit_mergetree_1/metadata.json +++ b/parser/testdata/03372_qbit_mergetree_1/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt13": true, - "stmt22": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03372_qbit_mergetree_2/metadata.json b/parser/testdata/03372_qbit_mergetree_2/metadata.json index af3c4a9de0..0967ef424b 100644 --- a/parser/testdata/03372_qbit_mergetree_2/metadata.json +++ b/parser/testdata/03372_qbit_mergetree_2/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt13": true, - "stmt22": true, - "stmt31": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03375_l2_distance_transposed_partial_reads_pass/metadata.json b/parser/testdata/03375_l2_distance_transposed_partial_reads_pass/metadata.json index 60ada59ad9..ef382ce51e 100644 --- a/parser/testdata/03375_l2_distance_transposed_partial_reads_pass/metadata.json +++ b/parser/testdata/03375_l2_distance_transposed_partial_reads_pass/metadata.json @@ -1 +1,6 @@ -{"explain_todo":{"stmt2":true,"stmt4":true,"stmt5":true}} +{ + "explain_todo": { + "stmt4": true, + "stmt5": true + } +} diff --git a/parser/testdata/03566_low_cardinality_nan_unique/metadata.json b/parser/testdata/03566_low_cardinality_nan_unique/metadata.json index 60f8ea1f08..0967ef424b 100644 --- a/parser/testdata/03566_low_cardinality_nan_unique/metadata.json +++ b/parser/testdata/03566_low_cardinality_nan_unique/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt11":true}} +{} From 1a09672d7314e59b0cae8576c303d3d1a5b477f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 23:08:38 +0000 Subject: [PATCH 15/20] Add standalone UPDATE statement support ClickHouse supports standalone UPDATE statements as syntactic sugar for ALTER TABLE ... UPDATE. This adds: - UpdateQuery AST node - Parser for UPDATE table SET col=expr WHERE condition - Explain output for UpdateQuery and Assignment nodes This fixes many lightweight UPDATE (lwu) tests. --- ast/ast.go | 14 +++++ internal/explain/explain.go | 4 ++ internal/explain/statements.go | 42 ++++++++++++++ parser/parser.go | 57 +++++++++++++++++++ .../03100_lwu_01_basics/metadata.json | 3 +- .../03100_lwu_02_basics/metadata.json | 7 +-- .../testdata/03100_lwu_03_join/metadata.json | 4 +- .../03100_lwu_04_prewhere/metadata.json | 7 +-- .../03100_lwu_05_basics/metadata.json | 6 +- .../03100_lwu_06_apply_patches/metadata.json | 2 - .../03100_lwu_07_merge_patches/metadata.json | 7 +-- .../metadata.json | 6 +- .../metadata.json | 3 - .../metadata.json | 4 -- .../metadata.json | 3 +- .../03100_lwu_18_sequence/metadata.json | 6 +- .../03100_lwu_19_nullable/metadata.json | 10 +--- .../metadata.json | 8 +-- .../metadata.json | 3 - .../03100_lwu_23_apply_patches/metadata.json | 8 +-- .../03100_lwu_26_subcolumns/metadata.json | 7 +-- .../metadata.json | 7 +-- .../03100_lwu_30_join_cache/metadata.json | 1 - .../metadata.json | 1 - .../03100_lwu_32_on_fly_filter/metadata.json | 7 +-- .../03100_lwu_33_add_column/metadata.json | 7 +-- .../metadata.json | 8 +-- .../metadata.json | 6 +- .../metadata.json | 6 +- .../metadata.json | 4 +- .../03100_lwu_41_bytes_limits/metadata.json | 11 +--- .../metadata.json | 6 +- .../metadata.json | 7 +-- .../metadata.json | 1 - .../03100_lwu_deletes_1/metadata.json | 1 - .../03100_lwu_deletes_3/metadata.json | 3 - .../metadata.json | 3 +- .../metadata.json | 7 +-- 38 files changed, 142 insertions(+), 155 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index e8c72ea028..3c9d542ec4 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -514,6 +514,20 @@ func (u *UndropQuery) Pos() token.Position { return u.Position } func (u *UndropQuery) End() token.Position { return u.Position } func (u *UndropQuery) statementNode() {} +// UpdateQuery represents a standalone UPDATE statement. +// In ClickHouse, UPDATE is syntactic sugar for ALTER TABLE ... UPDATE +type UpdateQuery struct { + Position token.Position `json:"-"` + Database string `json:"database,omitempty"` + Table string `json:"table"` + Assignments []*Assignment `json:"assignments"` + Where Expression `json:"where,omitempty"` +} + +func (u *UpdateQuery) Pos() token.Position { return u.Position } +func (u *UpdateQuery) End() token.Position { return u.Position } +func (u *UpdateQuery) statementNode() {} + // AlterQuery represents an ALTER statement. type AlterQuery struct { Position token.Position `json:"-"` diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 02e76350eb..b915330d53 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -240,6 +240,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) { explainCheckQuery(sb, n, indent) case *ast.CreateIndexQuery: explainCreateIndexQuery(sb, n, indent, depth) + case *ast.UpdateQuery: + explainUpdateQuery(sb, n, indent, depth) // Types case *ast.DataType: @@ -266,6 +268,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) { explainDictionaryLayout(sb, n, indent, depth) case *ast.DictionaryRange: explainDictionaryRange(sb, n, indent, depth) + case *ast.Assignment: + explainAssignment(sb, n, indent, depth) default: // For unhandled types, just print the type name diff --git a/internal/explain/statements.go b/internal/explain/statements.go index a7a3c6974b..ae9f9e48be 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -1589,3 +1589,45 @@ func explainCreateIndexQuery(sb *strings.Builder, n *ast.CreateIndexQuery, inden // Child 3: Table name fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table) } + +func explainAssignment(sb *strings.Builder, n *ast.Assignment, indent string, depth int) { + if n == nil { + return + } + // Assignment col_name (children 1) + fmt.Fprintf(sb, "%sAssignment %s (children 1)\n", indent, n.Column) + if n.Value != nil { + Node(sb, n.Value, depth+1) + } +} + +func explainUpdateQuery(sb *strings.Builder, n *ast.UpdateQuery, indent string, depth int) { + if n == nil { + fmt.Fprintf(sb, "%s*ast.UpdateQuery\n", indent) + return + } + + // Count children: always 3 (identifier, where condition, assignments) + children := 3 + + // UpdateQuery with two spaces before table name + if n.Database != "" { + fmt.Fprintf(sb, "%sUpdateQuery %s %s (children %d)\n", indent, n.Database, n.Table, children) + } else { + fmt.Fprintf(sb, "%sUpdateQuery %s (children %d)\n", indent, n.Table, children) + } + + // Child 1: Table identifier + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Table) + + // Child 2: WHERE condition + if n.Where != nil { + Node(sb, n.Where, depth+1) + } + + // Child 3: Assignments wrapped in ExpressionList + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Assignments)) + for _, assign := range n.Assignments { + Node(sb, assign, depth+2) + } +} diff --git a/parser/parser.go b/parser/parser.go index 51b310d144..c1957458b3 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -200,6 +200,8 @@ func (p *Parser) parseStatement() ast.Statement { return p.parseSetRole() } return p.parseSet() + case token.UPDATE: + return p.parseUpdate() case token.OPTIMIZE: return p.parseOptimize() case token.SYSTEM: @@ -4663,6 +4665,61 @@ func (p *Parser) parseUndrop() *ast.UndropQuery { return undrop } +func (p *Parser) parseUpdate() *ast.UpdateQuery { + update := &ast.UpdateQuery{ + Position: p.current.Pos, + } + + p.nextToken() // skip UPDATE + + // Parse table name (can be database.table) + tableName := p.parseIdentifierName() + if tableName != "" { + if p.currentIs(token.DOT) { + p.nextToken() + update.Database = tableName + update.Table = p.parseIdentifierName() + } else { + update.Table = tableName + } + } + + // Expect SET keyword + if !p.currentIs(token.SET) { + return update + } + p.nextToken() // skip SET + + // Parse assignments: col = expr, col = expr, ... + for { + if !p.currentIs(token.IDENT) && !p.current.Token.IsKeyword() { + break + } + assign := &ast.Assignment{ + Position: p.current.Pos, + Column: p.current.Value, + } + p.nextToken() // skip column name + if p.currentIs(token.EQ) { + p.nextToken() // skip = + assign.Value = p.parseExpression(LOWEST) + } + update.Assignments = append(update.Assignments, assign) + if !p.currentIs(token.COMMA) { + break + } + p.nextToken() // skip comma + } + + // Parse WHERE clause + if p.currentIs(token.WHERE) { + p.nextToken() // skip WHERE + update.Where = p.parseExpression(LOWEST) + } + + return update +} + func (p *Parser) parseUse() *ast.UseQuery { use := &ast.UseQuery{ Position: p.current.Pos, diff --git a/parser/testdata/03100_lwu_01_basics/metadata.json b/parser/testdata/03100_lwu_01_basics/metadata.json index bd82208299..7b4455cd5f 100644 --- a/parser/testdata/03100_lwu_01_basics/metadata.json +++ b/parser/testdata/03100_lwu_01_basics/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt16": true, - "stmt9": true + "stmt16": true } } diff --git a/parser/testdata/03100_lwu_02_basics/metadata.json b/parser/testdata/03100_lwu_02_basics/metadata.json index afaaa4b0a6..0967ef424b 100644 --- a/parser/testdata/03100_lwu_02_basics/metadata.json +++ b/parser/testdata/03100_lwu_02_basics/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt7": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/03100_lwu_03_join/metadata.json b/parser/testdata/03100_lwu_03_join/metadata.json index f50a0f21b7..62b81668c3 100644 --- a/parser/testdata/03100_lwu_03_join/metadata.json +++ b/parser/testdata/03100_lwu_03_join/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt10": true, - "stmt13": true, - "stmt6": true + "stmt13": true } } diff --git a/parser/testdata/03100_lwu_04_prewhere/metadata.json b/parser/testdata/03100_lwu_04_prewhere/metadata.json index 0f293987f1..0967ef424b 100644 --- a/parser/testdata/03100_lwu_04_prewhere/metadata.json +++ b/parser/testdata/03100_lwu_04_prewhere/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt5": true, - "stmt6": true - } -} +{} diff --git a/parser/testdata/03100_lwu_05_basics/metadata.json b/parser/testdata/03100_lwu_05_basics/metadata.json index dbdbb76d4f..0967ef424b 100644 --- a/parser/testdata/03100_lwu_05_basics/metadata.json +++ b/parser/testdata/03100_lwu_05_basics/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt6": true - } -} +{} diff --git a/parser/testdata/03100_lwu_06_apply_patches/metadata.json b/parser/testdata/03100_lwu_06_apply_patches/metadata.json index 7db8f80ee1..36d996a78f 100644 --- a/parser/testdata/03100_lwu_06_apply_patches/metadata.json +++ b/parser/testdata/03100_lwu_06_apply_patches/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { - "stmt10": true, "stmt18": true, - "stmt7": true, "stmt9": true } } diff --git a/parser/testdata/03100_lwu_07_merge_patches/metadata.json b/parser/testdata/03100_lwu_07_merge_patches/metadata.json index 1f8757bb32..c45b7602ba 100644 --- a/parser/testdata/03100_lwu_07_merge_patches/metadata.json +++ b/parser/testdata/03100_lwu_07_merge_patches/metadata.json @@ -1,10 +1,5 @@ { "explain_todo": { - "stmt12": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, - "stmt9": true + "stmt12": true } } diff --git a/parser/testdata/03100_lwu_08_multiple_blocks/metadata.json b/parser/testdata/03100_lwu_08_multiple_blocks/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/03100_lwu_08_multiple_blocks/metadata.json +++ b/parser/testdata/03100_lwu_08_multiple_blocks/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} diff --git a/parser/testdata/03100_lwu_09_different_structure/metadata.json b/parser/testdata/03100_lwu_09_different_structure/metadata.json index ebb98b5b75..b67140f418 100644 --- a/parser/testdata/03100_lwu_09_different_structure/metadata.json +++ b/parser/testdata/03100_lwu_09_different_structure/metadata.json @@ -2,9 +2,6 @@ "explain_todo": { "stmt10": true, "stmt11": true, - "stmt6": true, - "stmt7": true, - "stmt8": true, "stmt9": true } } diff --git a/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json b/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json index f216958458..e3311cecdb 100644 --- a/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json +++ b/parser/testdata/03100_lwu_10_apply_on_merges/metadata.json @@ -1,12 +1,8 @@ { "explain_todo": { "stmt11": true, - "stmt14": true, - "stmt16": true, "stmt18": true, "stmt19": true, - "stmt5": true, - "stmt6": true, "stmt8": true } } diff --git a/parser/testdata/03100_lwu_12_sequential_consistency/metadata.json b/parser/testdata/03100_lwu_12_sequential_consistency/metadata.json index 15223e732d..ab9202e88e 100644 --- a/parser/testdata/03100_lwu_12_sequential_consistency/metadata.json +++ b/parser/testdata/03100_lwu_12_sequential_consistency/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt11": true, - "stmt9": true + "stmt11": true } } diff --git a/parser/testdata/03100_lwu_18_sequence/metadata.json b/parser/testdata/03100_lwu_18_sequence/metadata.json index 7401011030..f650e24ee6 100644 --- a/parser/testdata/03100_lwu_18_sequence/metadata.json +++ b/parser/testdata/03100_lwu_18_sequence/metadata.json @@ -1,10 +1,6 @@ { "explain_todo": { "stmt12": true, - "stmt16": true, - "stmt5": true, - "stmt6": true, - "stmt7": true, - "stmt8": true + "stmt16": true } } diff --git a/parser/testdata/03100_lwu_19_nullable/metadata.json b/parser/testdata/03100_lwu_19_nullable/metadata.json index 8555a3ee52..0967ef424b 100644 --- a/parser/testdata/03100_lwu_19_nullable/metadata.json +++ b/parser/testdata/03100_lwu_19_nullable/metadata.json @@ -1,9 +1 @@ -{ - "explain_todo": { - "stmt12": true, - "stmt16": true, - "stmt18": true, - "stmt21": true, - "stmt5": true - } -} +{} diff --git a/parser/testdata/03100_lwu_20_different_structure/metadata.json b/parser/testdata/03100_lwu_20_different_structure/metadata.json index 703138bb80..0967ef424b 100644 --- a/parser/testdata/03100_lwu_20_different_structure/metadata.json +++ b/parser/testdata/03100_lwu_20_different_structure/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt12": true, - "stmt16": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/03100_lwu_22_detach_attach_patches/metadata.json b/parser/testdata/03100_lwu_22_detach_attach_patches/metadata.json index 19e8872514..29a0cd7cc2 100644 --- a/parser/testdata/03100_lwu_22_detach_attach_patches/metadata.json +++ b/parser/testdata/03100_lwu_22_detach_attach_patches/metadata.json @@ -1,8 +1,5 @@ { "explain_todo": { - "stmt15": true, - "stmt16": true, - "stmt17": true, "stmt21": true, "stmt23": true, "stmt24": true, diff --git a/parser/testdata/03100_lwu_23_apply_patches/metadata.json b/parser/testdata/03100_lwu_23_apply_patches/metadata.json index 52cd57d2ce..9aa0f7414d 100644 --- a/parser/testdata/03100_lwu_23_apply_patches/metadata.json +++ b/parser/testdata/03100_lwu_23_apply_patches/metadata.json @@ -1,12 +1,6 @@ { "explain_todo": { "stmt11": true, - "stmt18": true, - "stmt19": true, - "stmt20": true, - "stmt22": true, - "stmt7": true, - "stmt8": true, - "stmt9": true + "stmt22": true } } diff --git a/parser/testdata/03100_lwu_26_subcolumns/metadata.json b/parser/testdata/03100_lwu_26_subcolumns/metadata.json index ff0eba6904..0967ef424b 100644 --- a/parser/testdata/03100_lwu_26_subcolumns/metadata.json +++ b/parser/testdata/03100_lwu_26_subcolumns/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt6": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/03100_lwu_27_update_after_on_fly_mutations/metadata.json b/parser/testdata/03100_lwu_27_update_after_on_fly_mutations/metadata.json index 2cf11720b5..0967ef424b 100644 --- a/parser/testdata/03100_lwu_27_update_after_on_fly_mutations/metadata.json +++ b/parser/testdata/03100_lwu_27_update_after_on_fly_mutations/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt10": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/03100_lwu_30_join_cache/metadata.json b/parser/testdata/03100_lwu_30_join_cache/metadata.json index 0f293987f1..dbdbb76d4f 100644 --- a/parser/testdata/03100_lwu_30_join_cache/metadata.json +++ b/parser/testdata/03100_lwu_30_join_cache/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt5": true, "stmt6": true } } diff --git a/parser/testdata/03100_lwu_31_merge_memory_usage/metadata.json b/parser/testdata/03100_lwu_31_merge_memory_usage/metadata.json index ff0eba6904..b563327205 100644 --- a/parser/testdata/03100_lwu_31_merge_memory_usage/metadata.json +++ b/parser/testdata/03100_lwu_31_merge_memory_usage/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt6": true, "stmt7": true } } diff --git a/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json b/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json index 92efb02376..0967ef424b 100644 --- a/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json +++ b/parser/testdata/03100_lwu_32_on_fly_filter/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt6": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/03100_lwu_33_add_column/metadata.json b/parser/testdata/03100_lwu_33_add_column/metadata.json index 92efb02376..0967ef424b 100644 --- a/parser/testdata/03100_lwu_33_add_column/metadata.json +++ b/parser/testdata/03100_lwu_33_add_column/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt6": true, - "stmt8": true - } -} +{} diff --git a/parser/testdata/03100_lwu_34_multistep_prewhere/metadata.json b/parser/testdata/03100_lwu_34_multistep_prewhere/metadata.json index 661bded8e9..0967ef424b 100644 --- a/parser/testdata/03100_lwu_34_multistep_prewhere/metadata.json +++ b/parser/testdata/03100_lwu_34_multistep_prewhere/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt7": true, - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/03100_lwu_36_json_skip_indexes/metadata.json b/parser/testdata/03100_lwu_36_json_skip_indexes/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/03100_lwu_36_json_skip_indexes/metadata.json +++ b/parser/testdata/03100_lwu_36_json_skip_indexes/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/03100_lwu_37_update_all_columns/metadata.json b/parser/testdata/03100_lwu_37_update_all_columns/metadata.json index 3a06a4a1ac..0967ef424b 100644 --- a/parser/testdata/03100_lwu_37_update_all_columns/metadata.json +++ b/parser/testdata/03100_lwu_37_update_all_columns/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt5": true - } -} +{} diff --git a/parser/testdata/03100_lwu_39_after_replace_partition/metadata.json b/parser/testdata/03100_lwu_39_after_replace_partition/metadata.json index 58e9a6ae97..15d79beb93 100644 --- a/parser/testdata/03100_lwu_39_after_replace_partition/metadata.json +++ b/parser/testdata/03100_lwu_39_after_replace_partition/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { "stmt11": true, - "stmt12": true, - "stmt5": true, - "stmt6": true + "stmt5": true } } diff --git a/parser/testdata/03100_lwu_41_bytes_limits/metadata.json b/parser/testdata/03100_lwu_41_bytes_limits/metadata.json index 478109d1e7..0967ef424b 100644 --- a/parser/testdata/03100_lwu_41_bytes_limits/metadata.json +++ b/parser/testdata/03100_lwu_41_bytes_limits/metadata.json @@ -1,10 +1 @@ -{ - "explain_todo": { - "stmt13": true, - "stmt14": true, - "stmt15": true, - "stmt5": true, - "stmt6": true, - "stmt7": true - } -} +{} diff --git a/parser/testdata/03100_lwu_43_subquery_from_rmt/metadata.json b/parser/testdata/03100_lwu_43_subquery_from_rmt/metadata.json index 342b3ff5b4..0967ef424b 100644 --- a/parser/testdata/03100_lwu_43_subquery_from_rmt/metadata.json +++ b/parser/testdata/03100_lwu_43_subquery_from_rmt/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt8": true - } -} +{} diff --git a/parser/testdata/03100_lwu_44_missing_default/metadata.json b/parser/testdata/03100_lwu_44_missing_default/metadata.json index 8162ad6436..0967ef424b 100644 --- a/parser/testdata/03100_lwu_44_missing_default/metadata.json +++ b/parser/testdata/03100_lwu_44_missing_default/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt21": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/03100_lwu_45_query_condition_cache/metadata.json b/parser/testdata/03100_lwu_45_query_condition_cache/metadata.json index 661bded8e9..afaaa4b0a6 100644 --- a/parser/testdata/03100_lwu_45_query_condition_cache/metadata.json +++ b/parser/testdata/03100_lwu_45_query_condition_cache/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt7": true, - "stmt8": true, "stmt9": true } } diff --git a/parser/testdata/03100_lwu_deletes_1/metadata.json b/parser/testdata/03100_lwu_deletes_1/metadata.json index b470c2b73c..ccc75f74d5 100644 --- a/parser/testdata/03100_lwu_deletes_1/metadata.json +++ b/parser/testdata/03100_lwu_deletes_1/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt12": true, "stmt15": true, "stmt9": true } diff --git a/parser/testdata/03100_lwu_deletes_3/metadata.json b/parser/testdata/03100_lwu_deletes_3/metadata.json index d94c5b9752..7392d8b45e 100644 --- a/parser/testdata/03100_lwu_deletes_3/metadata.json +++ b/parser/testdata/03100_lwu_deletes_3/metadata.json @@ -1,8 +1,5 @@ { "explain_todo": { - "stmt10": true, - "stmt11": true, - "stmt12": true, "stmt13": true, "stmt14": true, "stmt15": true, diff --git a/parser/testdata/03604_test_merge_tree_min_read_task_size_is_zero/metadata.json b/parser/testdata/03604_test_merge_tree_min_read_task_size_is_zero/metadata.json index ff0eba6904..dbdbb76d4f 100644 --- a/parser/testdata/03604_test_merge_tree_min_read_task_size_is_zero/metadata.json +++ b/parser/testdata/03604_test_merge_tree_min_read_task_size_is_zero/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt6": true, - "stmt7": true + "stmt6": true } } diff --git a/parser/testdata/03715_empty_tuple_functions_conversion/metadata.json b/parser/testdata/03715_empty_tuple_functions_conversion/metadata.json index 73520390f9..0967ef424b 100644 --- a/parser/testdata/03715_empty_tuple_functions_conversion/metadata.json +++ b/parser/testdata/03715_empty_tuple_functions_conversion/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt12": true, - "stmt14": true - } -} +{} From 0a4435a20ab595981f790330f1f5f386960dbf5a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 23:17:52 +0000 Subject: [PATCH 16/20] Add alias support for EXISTS expressions in EXPLAIN output Add explainExistsExprWithAlias function to handle EXISTS expressions that have an alias (e.g., EXISTS (SELECT 1) AS mycheck). The alias is now correctly included in the Function exists output. --- internal/explain/expressions.go | 3 +++ internal/explain/functions.go | 10 +++++++++- .../testdata/03115_alias_exists_column/metadata.json | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/explain/expressions.go b/internal/explain/expressions.go index a2d6c9c877..2d37b0867d 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -632,6 +632,9 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { case *ast.CaseExpr: // CASE expressions with alias explainCaseExprWithAlias(sb, e, n.Alias, indent, depth) + case *ast.ExistsExpr: + // EXISTS expressions with alias + explainExistsExprWithAlias(sb, e, n.Alias, indent, depth) default: // For other types, recursively explain and add alias info Node(sb, n.Expr, depth) diff --git a/internal/explain/functions.go b/internal/explain/functions.go index 4da28af3f0..88e139f983 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -1311,8 +1311,16 @@ func parseIntervalString(s string) (value string, unit string) { } func explainExistsExpr(sb *strings.Builder, n *ast.ExistsExpr, indent string, depth int) { + explainExistsExprWithAlias(sb, n, "", indent, depth) +} + +func explainExistsExprWithAlias(sb *strings.Builder, n *ast.ExistsExpr, alias string, indent string, depth int) { // EXISTS is represented as Function exists - fmt.Fprintf(sb, "%sFunction exists (children %d)\n", indent, 1) + if alias != "" { + fmt.Fprintf(sb, "%sFunction exists (alias %s) (children %d)\n", indent, alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction exists (children %d)\n", indent, 1) + } fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) fmt.Fprintf(sb, "%s Subquery (children %d)\n", indent, 1) Node(sb, n.Query, depth+3) diff --git a/parser/testdata/03115_alias_exists_column/metadata.json b/parser/testdata/03115_alias_exists_column/metadata.json index af48d4c110..0967ef424b 100644 --- a/parser/testdata/03115_alias_exists_column/metadata.json +++ b/parser/testdata/03115_alias_exists_column/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt2":true}} +{} From aa552bb5968eca66644f05f19de4d369f1bf18bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 23:27:12 +0000 Subject: [PATCH 17/20] Add INTERPOLATE clause support for ORDER BY ... WITH FILL Implements parsing and EXPLAIN output for the INTERPOLATE clause used with ORDER BY ... WITH FILL. The INTERPOLATE clause allows specifying how values should be filled for interpolated rows. Changes: - Add INTERPOLATE token to token.go - Add InterpolateElement struct to ast/ast.go - Add Interpolate field to SelectQuery - Add parseInterpolateList in parser.go - Add explainInterpolateElement and update explainSelectQuery --- ast/ast.go | 14 ++++- internal/explain/explain.go | 2 + internal/explain/select.go | 26 +++++++++ parser/parser.go | 54 +++++++++++++++++++ .../metadata.json | 9 +--- .../03033_with_fill_interpolate/metadata.json | 6 +-- .../03155_analyzer_interpolate/metadata.json | 8 +-- .../03366_with_fill_dag/metadata.json | 6 +-- token/token.go | 2 + 9 files changed, 101 insertions(+), 26 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 3c9d542ec4..8a77407bb1 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -72,7 +72,8 @@ type SelectQuery struct { Having Expression `json:"having,omitempty"` Qualify Expression `json:"qualify,omitempty"` Window []*WindowDefinition `json:"window,omitempty"` - OrderBy []*OrderByElement `json:"order_by,omitempty"` + OrderBy []*OrderByElement `json:"order_by,omitempty"` + Interpolate []*InterpolateElement `json:"interpolate,omitempty"` Limit Expression `json:"limit,omitempty"` LimitBy []Expression `json:"limit_by,omitempty"` LimitByLimit Expression `json:"limit_by_limit,omitempty"` // LIMIT value before BY (e.g., LIMIT 1 BY x LIMIT 3) @@ -212,6 +213,17 @@ type OrderByElement struct { func (o *OrderByElement) Pos() token.Position { return o.Position } func (o *OrderByElement) End() token.Position { return o.Position } +// InterpolateElement represents a single column interpolation in INTERPOLATE clause. +// Example: INTERPOLATE (value AS value + 1) +type InterpolateElement struct { + Position token.Position `json:"-"` + Column string `json:"column"` + Value Expression `json:"value,omitempty"` // nil if just column name +} + +func (i *InterpolateElement) Pos() token.Position { return i.Position } +func (i *InterpolateElement) End() token.Position { return i.Position } + // SettingExpr represents a setting expression. type SettingExpr struct { Position token.Position `json:"-"` diff --git a/internal/explain/explain.go b/internal/explain/explain.go index b915330d53..5b37927157 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -61,6 +61,8 @@ func Node(sb *strings.Builder, node interface{}, depth int) { // Expressions case *ast.OrderByElement: explainOrderByElement(sb, n, indent, depth) + case *ast.InterpolateElement: + explainInterpolateElement(sb, n, indent, depth) case *ast.Identifier: explainIdentifier(sb, n, indent) case *ast.Literal: diff --git a/internal/explain/select.go b/internal/explain/select.go index c867495c91..84776a14af 100644 --- a/internal/explain/select.go +++ b/internal/explain/select.go @@ -149,6 +149,13 @@ func explainSelectQuery(sb *strings.Builder, n *ast.SelectQuery, indent string, Node(sb, o, depth+2) } } + // INTERPOLATE + if len(n.Interpolate) > 0 { + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Interpolate)) + for _, i := range n.Interpolate { + Node(sb, i, depth+2) + } + } // OFFSET (ClickHouse outputs offset before limit in EXPLAIN AST) if n.Offset != nil { Node(sb, n.Offset, depth+1) @@ -259,6 +266,22 @@ func explainOrderByElement(sb *strings.Builder, n *ast.OrderByElement, indent st } } +// explainInterpolateElement explains an INTERPOLATE element. +// Format: InterpolateElement (column colname) (children N) +func explainInterpolateElement(sb *strings.Builder, n *ast.InterpolateElement, indent string, depth int) { + children := 0 + if n.Value != nil { + children = 1 + } + + if children > 0 { + fmt.Fprintf(sb, "%sInterpolateElement (column %s) (children %d)\n", indent, n.Column, children) + Node(sb, n.Value, depth+1) + } else { + fmt.Fprintf(sb, "%sInterpolateElement (column %s)\n", indent, n.Column) + } +} + // isComplexExpr checks if an expression is complex (not a simple literal) func isComplexExpr(expr ast.Expression) bool { if expr == nil { @@ -348,6 +371,9 @@ func countSelectQueryChildren(n *ast.SelectQuery) int { if len(n.OrderBy) > 0 { count++ } + if len(n.Interpolate) > 0 { + count++ + } if n.LimitByLimit != nil { count++ // LIMIT n in "LIMIT n BY x LIMIT m" } diff --git a/parser/parser.go b/parser/parser.go index c1957458b3..edfe31dd3e 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -735,6 +735,12 @@ func (p *Parser) parseSelect() *ast.SelectQuery { sel.OrderBy = p.parseOrderByList() } + // Parse INTERPOLATE clause (comes after ORDER BY ... WITH FILL) + if p.currentIs(token.INTERPOLATE) { + p.nextToken() + sel.Interpolate = p.parseInterpolateList() + } + // Parse LIMIT clause if p.currentIs(token.LIMIT) { p.nextToken() @@ -1368,6 +1374,54 @@ func (p *Parser) parseOrderByList() []*ast.OrderByElement { return elements } +// parseInterpolateList parses INTERPOLATE (col1 AS expr1, col2, col3 AS expr3) +func (p *Parser) parseInterpolateList() []*ast.InterpolateElement { + var elements []*ast.InterpolateElement + + // Expect opening parenthesis + if !p.currentIs(token.LPAREN) { + return elements + } + p.nextToken() + + for { + if p.currentIs(token.RPAREN) { + break + } + + // Column name + if !p.currentIs(token.IDENT) && !p.current.Token.IsKeyword() { + break + } + + elem := &ast.InterpolateElement{ + Position: p.current.Pos, + Column: p.current.Value, + } + p.nextToken() + + // Optional AS expression + if p.currentIs(token.AS) { + p.nextToken() + elem.Value = p.parseExpression(LOWEST) + } + + elements = append(elements, elem) + + if !p.currentIs(token.COMMA) { + break + } + p.nextToken() + } + + // Expect closing parenthesis + if p.currentIs(token.RPAREN) { + p.nextToken() + } + + return elements +} + func (p *Parser) parseSettingsList() []*ast.SettingExpr { var settings []*ast.SettingExpr diff --git a/parser/testdata/02730_with_fill_by_sorting_prefix/metadata.json b/parser/testdata/02730_with_fill_by_sorting_prefix/metadata.json index 10d115b1ef..4ccbd7dd43 100644 --- a/parser/testdata/02730_with_fill_by_sorting_prefix/metadata.json +++ b/parser/testdata/02730_with_fill_by_sorting_prefix/metadata.json @@ -1,17 +1,10 @@ { "explain_todo": { - "stmt20": true, "stmt21": true, - "stmt22": true, "stmt23": true, - "stmt24": true, "stmt25": true, - "stmt26": true, "stmt27": true, - "stmt28": true, "stmt29": true, - "stmt30": true, - "stmt31": true, - "stmt32": true + "stmt31": true } } diff --git a/parser/testdata/03033_with_fill_interpolate/metadata.json b/parser/testdata/03033_with_fill_interpolate/metadata.json index b65b07d7a6..0967ef424b 100644 --- a/parser/testdata/03033_with_fill_interpolate/metadata.json +++ b/parser/testdata/03033_with_fill_interpolate/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt4": true - } -} +{} diff --git a/parser/testdata/03155_analyzer_interpolate/metadata.json b/parser/testdata/03155_analyzer_interpolate/metadata.json index fffcb7d38b..0967ef424b 100644 --- a/parser/testdata/03155_analyzer_interpolate/metadata.json +++ b/parser/testdata/03155_analyzer_interpolate/metadata.json @@ -1,7 +1 @@ -{ - "explain_todo": { - "stmt2": true, - "stmt3": true, - "stmt4": true - } -} +{} diff --git a/parser/testdata/03366_with_fill_dag/metadata.json b/parser/testdata/03366_with_fill_dag/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/03366_with_fill_dag/metadata.json +++ b/parser/testdata/03366_with_fill_dag/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/token/token.go b/token/token.go index 05bf17cbe7..38ae3888d0 100644 --- a/token/token.go +++ b/token/token.go @@ -123,6 +123,7 @@ const ( INNER INSERT INTERSECT + INTERPOLATE INTERVAL INTO IS @@ -320,6 +321,7 @@ var tokens = [...]string{ INNER: "INNER", INSERT: "INSERT", INTERSECT: "INTERSECT", + INTERPOLATE: "INTERPOLATE", INTERVAL: "INTERVAL", INTO: "INTO", IS: "IS", From bb6a1c66df7dcaa24986367bb2d499ec1af475b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 23:39:10 +0000 Subject: [PATCH 18/20] Fix INTERSECT/EXCEPT operator precedence in parser - Always create SelectIntersectExceptQuery for all EXCEPT/INTERSECT variants (including ALL modifier) - Handle ALL modifier in operator string storage - Use HasPrefix for detecting EXCEPT/INTERSECT variants in explain code This fixes EXCEPT ALL and INTERSECT ALL queries that were previously being parsed as UNION-style flattened queries instead of using the proper SelectIntersectExceptQuery wrapper. --- internal/explain/select.go | 2 +- parser/parser.go | 27 +++++++++---------- .../metadata.json | 9 +------ .../metadata.json | 7 +---- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/internal/explain/select.go b/internal/explain/select.go index 84776a14af..c9a56cd3cc 100644 --- a/internal/explain/select.go +++ b/internal/explain/select.go @@ -13,7 +13,7 @@ func explainSelectIntersectExceptQuery(sb *strings.Builder, n *ast.SelectInterse // ClickHouse wraps first operand in SelectWithUnionQuery when EXCEPT is present hasExcept := false for _, op := range n.Operators { - if op == "EXCEPT" { + if strings.HasPrefix(op, "EXCEPT") { hasExcept = true break } diff --git a/parser/parser.go b/parser/parser.go index edfe31dd3e..c7b09e5fc3 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -286,8 +286,11 @@ func (p *Parser) parseSelectWithUnion() *ast.SelectWithUnionQuery { } p.nextToken() // skip INTERSECT/EXCEPT - // Handle DISTINCT if present (ALL case is handled in the loop condition) - if p.currentIs(token.DISTINCT) { + // Handle ALL or DISTINCT if present + if p.currentIs(token.ALL) { + op += " ALL" + p.nextToken() + } else if p.currentIs(token.DISTINCT) { op += " DISTINCT" p.nextToken() } @@ -403,8 +406,11 @@ func (p *Parser) parseIntersectExceptWithFirstOperand(unionQuery *ast.SelectWith } p.nextToken() // skip INTERSECT/EXCEPT - // Handle DISTINCT if present - if p.currentIs(token.DISTINCT) { + // Handle ALL or DISTINCT if present + if p.currentIs(token.ALL) { + op += " ALL" + p.nextToken() + } else if p.currentIs(token.DISTINCT) { op += " DISTINCT" p.nextToken() } @@ -500,21 +506,14 @@ func (p *Parser) parseSelectWithUnionOnly() ast.Statement { // isIntersectExceptWithWrapper checks if the current token is INTERSECT or EXCEPT // that should use a SelectIntersectExceptQuery wrapper. -// Only INTERSECT ALL and EXCEPT ALL are flattened (no wrapper). -// INTERSECT DISTINCT, INTERSECT, EXCEPT DISTINCT, and EXCEPT all use the wrapper. +// All INTERSECT and EXCEPT variants (including ALL and DISTINCT) use the wrapper. func (p *Parser) isIntersectExceptWithWrapper() bool { - if !p.currentIs(token.EXCEPT) && !p.currentIs(token.INTERSECT) { - return false - } - // INTERSECT ALL and EXCEPT ALL are flattened (no wrapper) - // All other cases (DISTINCT or no modifier) use the wrapper - nextTok := p.peek.Token - return nextTok != token.ALL + return p.currentIs(token.EXCEPT) || p.currentIs(token.INTERSECT) } // isIntersectOp checks if the operator is an INTERSECT variant (not EXCEPT) func isIntersectOp(op string) bool { - return op == "INTERSECT" || op == "INTERSECT DISTINCT" + return strings.HasPrefix(op, "INTERSECT") } // buildIntersectExceptTree builds the AST tree respecting operator precedence. diff --git a/parser/testdata/02004_intersect_except_const_column/metadata.json b/parser/testdata/02004_intersect_except_const_column/metadata.json index 12d7fda64a..0967ef424b 100644 --- a/parser/testdata/02004_intersect_except_const_column/metadata.json +++ b/parser/testdata/02004_intersect_except_const_column/metadata.json @@ -1,8 +1 @@ -{ - "explain_todo": { - "stmt12": true, - "stmt13": true, - "stmt14": true, - "stmt2": true - } -} +{} diff --git a/parser/testdata/03312_analyzer_unused_projection_fix/metadata.json b/parser/testdata/03312_analyzer_unused_projection_fix/metadata.json index 682bda1cbc..0967ef424b 100644 --- a/parser/testdata/03312_analyzer_unused_projection_fix/metadata.json +++ b/parser/testdata/03312_analyzer_unused_projection_fix/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt1": true, - "stmt2": true - } -} +{} From 730f9d2007175c1c33d078c4cd36b24737dcfd10 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 23:50:44 +0000 Subject: [PATCH 19/20] Add ALTER TABLE MODIFY COMMENT support Implement parsing and explain output for ALTER TABLE ... MODIFY COMMENT statements: - Add AlterModifyComment command type to AST - Add parser handling in MODIFY case for COMMENT token - Add explain output handling for MODIFY_COMMENT command type - Add child count handling for comment literal This fixes 14 statements across 4 tests: - 02111_modify_table_comment: stmt6, stmt10 - 02155_dictionary_comment: stmt13 - 02792_alter_table_modify_comment: stmt4, stmt9, stmt14, stmt19, stmt24, stmt29, stmt34, stmt39, stmt44, stmt49 - 03142_alter_comment_parameterized_view: stmt3 --- ast/ast.go | 1 + internal/explain/statements.go | 8 ++++++++ parser/parser.go | 8 ++++++++ .../02111_modify_table_comment/metadata.json | 2 -- .../testdata/02155_dictionary_comment/metadata.json | 6 +----- .../02792_alter_table_modify_comment/metadata.json | 12 +----------- .../metadata.json | 3 +-- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/ast/ast.go b/ast/ast.go index 8a77407bb1..44e1209e07 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -653,6 +653,7 @@ const ( AlterDropStatistics AlterCommandType = "DROP_STATISTICS" AlterClearStatistics AlterCommandType = "CLEAR_STATISTICS" AlterMaterializeStatistics AlterCommandType = "MATERIALIZE_STATISTICS" + AlterModifyComment AlterCommandType = "MODIFY_COMMENT" ) // TruncateQuery represents a TRUNCATE statement. diff --git a/internal/explain/statements.go b/internal/explain/statements.go index ae9f9e48be..1dce30ecf8 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -1210,6 +1210,10 @@ func explainAlterCommand(sb *strings.Builder, cmd *ast.AlterCommand, indent stri if cmd.Comment != "" { fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, escapeStringLiteral(cmd.Comment)) } + case ast.AlterModifyComment: + if cmd.Comment != "" { + fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, escapeStringLiteral(cmd.Comment)) + } case ast.AlterAddIndex, ast.AlterDropIndex, ast.AlterClearIndex, ast.AlterMaterializeIndex: if cmd.Index != "" { fmt.Fprintf(sb, "%s Identifier %s\n", indent, cmd.Index) @@ -1392,6 +1396,10 @@ func countAlterCommandChildren(cmd *ast.AlterCommand) int { if cmd.Comment != "" { children++ } + case ast.AlterModifyComment: + if cmd.Comment != "" { + children++ + } case ast.AlterRenameColumn: if cmd.ColumnName != "" { children++ diff --git a/parser/parser.go b/parser/parser.go index c7b09e5fc3..9e8cda19ed 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -4495,6 +4495,14 @@ func (p *Parser) parseAlterCommand() *ast.AlterCommand { p.nextToken() cmd.StatisticsTypes = p.parseStatisticsTypeList() } + } else if p.currentIs(token.COMMENT) { + // MODIFY COMMENT 'comment string' + cmd.Type = ast.AlterModifyComment + p.nextToken() + if p.currentIs(token.STRING) { + cmd.Comment = p.current.Value + p.nextToken() + } } case token.RENAME: p.nextToken() diff --git a/parser/testdata/02111_modify_table_comment/metadata.json b/parser/testdata/02111_modify_table_comment/metadata.json index e0284eb884..05aa6dfc72 100644 --- a/parser/testdata/02111_modify_table_comment/metadata.json +++ b/parser/testdata/02111_modify_table_comment/metadata.json @@ -1,8 +1,6 @@ { "explain_todo": { - "stmt10": true, "stmt4": true, - "stmt6": true, "stmt8": true } } diff --git a/parser/testdata/02155_dictionary_comment/metadata.json b/parser/testdata/02155_dictionary_comment/metadata.json index 62b81668c3..0967ef424b 100644 --- a/parser/testdata/02155_dictionary_comment/metadata.json +++ b/parser/testdata/02155_dictionary_comment/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt13": true - } -} +{} diff --git a/parser/testdata/02792_alter_table_modify_comment/metadata.json b/parser/testdata/02792_alter_table_modify_comment/metadata.json index 398f0169e7..1e1d731c05 100644 --- a/parser/testdata/02792_alter_table_modify_comment/metadata.json +++ b/parser/testdata/02792_alter_table_modify_comment/metadata.json @@ -1,16 +1,6 @@ { "explain_todo": { - "stmt14": true, - "stmt19": true, "stmt22": true, - "stmt24": true, - "stmt29": true, - "stmt34": true, - "stmt39": true, - "stmt4": true, - "stmt44": true, - "stmt49": true, - "stmt7": true, - "stmt9": true + "stmt7": true } } diff --git a/parser/testdata/03142_alter_comment_parameterized_view/metadata.json b/parser/testdata/03142_alter_comment_parameterized_view/metadata.json index bc141058a4..ef58f80315 100644 --- a/parser/testdata/03142_alter_comment_parameterized_view/metadata.json +++ b/parser/testdata/03142_alter_comment_parameterized_view/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt2": true, - "stmt3": true + "stmt2": true } } From 29346af48886a8d304d986226b9eed4e4b5f1ba7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 23:59:59 +0000 Subject: [PATCH 20/20] Always show SAMPLE BY in CREATE TABLE EXPLAIN output Previously, SAMPLE BY was only shown when it was a function (not an identifier) and when different from ORDER BY. ClickHouse's actual EXPLAIN AST output always shows SAMPLE BY when present. This fixes 27 statements across 19 tests including: - 02559_add_parts - 00578_merge_table_sampling - 02380_analyzer_join_sample - 03227_test_sample_n - And 15 more SAMPLE BY related tests --- internal/explain/statements.go | 38 ++----------------- .../metadata.json | 3 +- .../00578_merge_table_sampling/metadata.json | 7 +--- .../metadata.json | 2 +- .../metadata.json | 7 +--- .../metadata.json | 6 +-- .../metadata.json | 6 +-- .../metadata.json | 2 +- .../metadata.json | 6 +-- .../metadata.json | 6 +-- .../02097_remove_sample_by/metadata.json | 3 -- .../02184_default_table_engine/metadata.json | 1 - .../02380_analyzer_join_sample/metadata.json | 2 - .../02381_analyzer_join_final/metadata.json | 4 +- .../metadata.json | 2 +- .../metadata.json | 1 - parser/testdata/02559_add_parts/metadata.json | 6 +-- .../03002_sample_factor_where/metadata.json | 2 +- .../metadata.json | 6 +-- .../03227_test_sample_n/metadata.json | 6 +-- 20 files changed, 19 insertions(+), 97 deletions(-) diff --git a/internal/explain/statements.go b/internal/explain/statements.go index 1dce30ecf8..3b14cbc2ca 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -282,24 +282,9 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, if len(n.PrimaryKey) > 0 { storageChildren++ } - // SAMPLE BY is only shown in EXPLAIN AST when it's a function (not a simple identifier) - // and when it's different from ORDER BY + // SAMPLE BY is always shown in EXPLAIN AST when present if n.SampleBy != nil { - if _, isIdent := n.SampleBy.(*ast.Identifier); !isIdent { - // Check if SAMPLE BY equals ORDER BY - if so, don't show it - showSampleBy := true - if len(n.OrderBy) == 1 { - var orderBySb, sampleBySb strings.Builder - Node(&orderBySb, n.OrderBy[0], 0) - Node(&sampleBySb, n.SampleBy, 0) - if orderBySb.String() == sampleBySb.String() { - showSampleBy = false - } - } - if showSampleBy { - storageChildren++ - } - } + storageChildren++ } if n.TTL != nil { storageChildren++ @@ -395,24 +380,9 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, } } } - // SAMPLE BY is only shown in EXPLAIN AST when it's a function (not a simple identifier) - // and when it's different from ORDER BY + // SAMPLE BY is always shown in EXPLAIN AST when present if n.SampleBy != nil { - if _, isIdent := n.SampleBy.(*ast.Identifier); !isIdent { - // Check if SAMPLE BY equals ORDER BY - if so, don't show it - showSampleBy := true - if len(n.OrderBy) == 1 { - var orderBySb, sampleBySb strings.Builder - Node(&orderBySb, n.OrderBy[0], 0) - Node(&sampleBySb, n.SampleBy, 0) - if orderBySb.String() == sampleBySb.String() { - showSampleBy = false - } - } - if showSampleBy { - Node(sb, n.SampleBy, storageChildDepth) - } - } + Node(sb, n.SampleBy, storageChildDepth) } if n.TTL != nil { fmt.Fprintf(sb, "%s ExpressionList (children 1)\n", storageIndent) diff --git a/parser/testdata/00509_extended_storage_definition_syntax_zookeeper/metadata.json b/parser/testdata/00509_extended_storage_definition_syntax_zookeeper/metadata.json index 969184549d..9be7220609 100644 --- a/parser/testdata/00509_extended_storage_definition_syntax_zookeeper/metadata.json +++ b/parser/testdata/00509_extended_storage_definition_syntax_zookeeper/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt22": true, - "stmt4": true + "stmt22": true } } diff --git a/parser/testdata/00578_merge_table_sampling/metadata.json b/parser/testdata/00578_merge_table_sampling/metadata.json index 92e84e943a..0967ef424b 100644 --- a/parser/testdata/00578_merge_table_sampling/metadata.json +++ b/parser/testdata/00578_merge_table_sampling/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt8": true, - "stmt9": true - } -} +{} diff --git a/parser/testdata/00712_prewhere_with_sampling/metadata.json b/parser/testdata/00712_prewhere_with_sampling/metadata.json index 2bca590175..0967ef424b 100644 --- a/parser/testdata/00712_prewhere_with_sampling/metadata.json +++ b/parser/testdata/00712_prewhere_with_sampling/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt2":true,"stmt7":true}} +{} diff --git a/parser/testdata/00753_system_columns_and_system_tables_long/metadata.json b/parser/testdata/00753_system_columns_and_system_tables_long/metadata.json index 6eea4ac173..0967ef424b 100644 --- a/parser/testdata/00753_system_columns_and_system_tables_long/metadata.json +++ b/parser/testdata/00753_system_columns_and_system_tables_long/metadata.json @@ -1,6 +1 @@ -{ - "explain_todo": { - "stmt3": true, - "stmt74": true - } -} +{} diff --git a/parser/testdata/00983_summing_merge_tree_not_an_identifier/metadata.json b/parser/testdata/00983_summing_merge_tree_not_an_identifier/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/00983_summing_merge_tree_not_an_identifier/metadata.json +++ b/parser/testdata/00983_summing_merge_tree_not_an_identifier/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} diff --git a/parser/testdata/01034_prewhere_max_parallel_replicas_distributed/metadata.json b/parser/testdata/01034_prewhere_max_parallel_replicas_distributed/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/01034_prewhere_max_parallel_replicas_distributed/metadata.json +++ b/parser/testdata/01034_prewhere_max_parallel_replicas_distributed/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/01557_max_parallel_replicas_no_sample/metadata.json b/parser/testdata/01557_max_parallel_replicas_no_sample/metadata.json index 60f8ea1f08..0967ef424b 100644 --- a/parser/testdata/01557_max_parallel_replicas_no_sample/metadata.json +++ b/parser/testdata/01557_max_parallel_replicas_no_sample/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt11":true}} +{} diff --git a/parser/testdata/01942_create_table_with_sample/metadata.json b/parser/testdata/01942_create_table_with_sample/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/01942_create_table_with_sample/metadata.json +++ b/parser/testdata/01942_create_table_with_sample/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} diff --git a/parser/testdata/02002_sampling_and_unknown_column_bug/metadata.json b/parser/testdata/02002_sampling_and_unknown_column_bug/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/02002_sampling_and_unknown_column_bug/metadata.json +++ b/parser/testdata/02002_sampling_and_unknown_column_bug/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/02097_remove_sample_by/metadata.json b/parser/testdata/02097_remove_sample_by/metadata.json index 47373b0cff..9b2d5596a3 100644 --- a/parser/testdata/02097_remove_sample_by/metadata.json +++ b/parser/testdata/02097_remove_sample_by/metadata.json @@ -1,13 +1,10 @@ { "explain_todo": { "stmt13": true, - "stmt15": true, "stmt16": true, - "stmt2": true, "stmt21": true, "stmt3": true, "stmt5": true, - "stmt8": true, "stmt9": true } } diff --git a/parser/testdata/02184_default_table_engine/metadata.json b/parser/testdata/02184_default_table_engine/metadata.json index f251b24f78..482c754937 100644 --- a/parser/testdata/02184_default_table_engine/metadata.json +++ b/parser/testdata/02184_default_table_engine/metadata.json @@ -1,7 +1,6 @@ { "explain_todo": { "stmt107": true, - "stmt26": true, "stmt56": true, "stmt61": true, "stmt73": true diff --git a/parser/testdata/02380_analyzer_join_sample/metadata.json b/parser/testdata/02380_analyzer_join_sample/metadata.json index 08d2ef4168..342b3ff5b4 100644 --- a/parser/testdata/02380_analyzer_join_sample/metadata.json +++ b/parser/testdata/02380_analyzer_join_sample/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt3": true, - "stmt6": true, "stmt8": true } } diff --git a/parser/testdata/02381_analyzer_join_final/metadata.json b/parser/testdata/02381_analyzer_join_final/metadata.json index 1a3f93296f..c45b7602ba 100644 --- a/parser/testdata/02381_analyzer_join_final/metadata.json +++ b/parser/testdata/02381_analyzer_join_final/metadata.json @@ -1,7 +1,5 @@ { "explain_todo": { - "stmt12": true, - "stmt3": true, - "stmt8": true + "stmt12": true } } diff --git a/parser/testdata/02481_merge_array_join_sample_by/metadata.json b/parser/testdata/02481_merge_array_join_sample_by/metadata.json index 51dfabe749..0967ef424b 100644 --- a/parser/testdata/02481_merge_array_join_sample_by/metadata.json +++ b/parser/testdata/02481_merge_array_join_sample_by/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt3":true}} +{} diff --git a/parser/testdata/02484_substitute_udf_storage_args/metadata.json b/parser/testdata/02484_substitute_udf_storage_args/metadata.json index 74d1fd2782..62b81668c3 100644 --- a/parser/testdata/02484_substitute_udf_storage_args/metadata.json +++ b/parser/testdata/02484_substitute_udf_storage_args/metadata.json @@ -1,6 +1,5 @@ { "explain_todo": { - "stmt10": true, "stmt13": true } } diff --git a/parser/testdata/02559_add_parts/metadata.json b/parser/testdata/02559_add_parts/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/02559_add_parts/metadata.json +++ b/parser/testdata/02559_add_parts/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{} diff --git a/parser/testdata/03002_sample_factor_where/metadata.json b/parser/testdata/03002_sample_factor_where/metadata.json index af48d4c110..0967ef424b 100644 --- a/parser/testdata/03002_sample_factor_where/metadata.json +++ b/parser/testdata/03002_sample_factor_where/metadata.json @@ -1 +1 @@ -{"explain_todo":{"stmt2":true}} +{} diff --git a/parser/testdata/03080_analyzer_prefer_column_name_to_alias__virtual_columns/metadata.json b/parser/testdata/03080_analyzer_prefer_column_name_to_alias__virtual_columns/metadata.json index ef58f80315..0967ef424b 100644 --- a/parser/testdata/03080_analyzer_prefer_column_name_to_alias__virtual_columns/metadata.json +++ b/parser/testdata/03080_analyzer_prefer_column_name_to_alias__virtual_columns/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt2": true - } -} +{} diff --git a/parser/testdata/03227_test_sample_n/metadata.json b/parser/testdata/03227_test_sample_n/metadata.json index e9d6e46171..0967ef424b 100644 --- a/parser/testdata/03227_test_sample_n/metadata.json +++ b/parser/testdata/03227_test_sample_n/metadata.json @@ -1,5 +1 @@ -{ - "explain_todo": { - "stmt1": true - } -} +{}