diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 68a8afb14..000000000 --- a/PLAN.md +++ /dev/null @@ -1,276 +0,0 @@ -# Comprehensive Plan: Fix Remaining Tests - -## Current Status -- **Tests passing:** 6,005 (88.0%) -- **Tests skipped:** 819 (12.0%) - - Parser failures: 173 tests - - Explain mismatches: 331 tests - - Other (metadata skip/explain=false): ~315 tests - -## Phase 1: Parser Fixes (High Impact) - -### 1.1 `view()` Table Function (~50 tests) -**Problem:** The `view(SELECT ...)` table function with inline subquery fails to parse. -```sql -SELECT * FROM view(SELECT 1 as id); -``` -**Files:** `parser/parser.go` (parseTableExpression, parseFunctionCall) -**Solution:** When parsing a function call and the function name is `view`, check if the first argument starts with SELECT/WITH and parse it as a subquery instead of expression list. - -### 1.2 Complex Type Casts with Named Parameters (~30 tests) -**Problem:** `::Tuple(a UInt32, b String)` with named fields fails -```sql -SELECT tuple(42, 42)::Tuple(a UInt32, b UInt32); -``` -**Files:** `parser/expression.go` (parseDataType) -**Solution:** Extend parseDataType to handle named parameters in type constructors like `Tuple(name Type, ...)`. - -### 1.3 DESCRIBE on Table Functions (~20 tests) -**Problem:** `desc format()`, `desc url()`, `desc s3Cluster()` fail -```sql -desc format(CSV, '"value"'); -``` -**Files:** `parser/parser.go` (parseDescribe) -**Solution:** Handle table function after DESC/DESCRIBE by calling parseTableExpression. - -### 1.4 INSERT INTO FUNCTION (~15 tests) -**Problem:** INSERT INTO FUNCTION with file paths and settings fails -```sql -insert into function file(02458_data.jsonl) select * settings engine_file_truncate_on_insert=1; -``` -**Files:** `parser/parser.go` (parseInsert) -**Solution:** Handle TABLE FUNCTION keyword and parse function call with settings. - -### 1.5 CREATE USER / FUNCTION / DICTIONARY (~10 tests) -**Problem:** These CREATE variants are not supported -```sql -CREATE USER test_user GRANTEES ...; -CREATE DICTIONARY d0 (c1 UInt64) PRIMARY KEY c1; -``` -**Files:** `parser/parser.go` (parseCreate) -**Solution:** Add cases for USER, FUNCTION, DICTIONARY in parseCreate switch. - -### 1.6 SHOW SETTINGS (~5 tests) -**Problem:** SHOW SETTINGS LIKE syntax not supported -```sql -show settings like 'send_timeout'; -``` -**Files:** `parser/parser.go` (parseShow) -**Solution:** Handle SETTINGS keyword after SHOW. - -### 1.7 PASTE JOIN (~3 tests) -**Problem:** PASTE JOIN is not recognized -```sql -SELECT * FROM t1 PASTE JOIN t2; -``` -**Files:** `parser/parser.go` (parseTableExpression or join parsing) -**Solution:** Add PASTE as a valid join type. - -### 1.8 `any()` Subquery Syntax (~2 tests) -**Problem:** `== any (SELECT ...)` syntax not supported -```sql -select 1 == any (select number from numbers(10)); -``` -**Files:** `parser/expression.go` -**Solution:** Handle `any(subquery)` as a special expression form after comparison operators. - ---- - -## Phase 2: Explain Layer Fixes (Medium Impact) - -### 2.1 INDEX Clause in CREATE TABLE (~50 tests) -**Problem:** INDEX definitions are skipped but should produce explain output -```sql -CREATE TABLE t (x UInt8, INDEX i x TYPE hypothesis GRANULARITY 100); -``` -**Files:** `parser/parser.go` (parseCreateTable), `internal/explain/statements.go` -**Solution:** -1. Parse INDEX into an ast.IndexDefinition struct -2. Add explain output for index definitions - -### 2.2 SETTINGS Inside Function Arguments (~40 tests) -**Problem:** SETTINGS in table functions should create a Set child -```sql -SELECT * FROM icebergS3(s3_conn, SETTINGS key='value'); -``` -**Files:** `parser/expression.go` (parseFunctionCall), `internal/explain/functions.go` -**Solution:** Capture SETTINGS as a Set node attached to the function call, output in explain. - -### 2.3 WITH FILL Clause (~30 tests) -**Problem:** ORDER BY ... WITH FILL is not captured -```sql -SELECT nan ORDER BY 1 WITH FILL; -``` -**Files:** `parser/parser.go` (parseOrderByItem), `internal/explain/select.go` -**Solution:** Add WithFill field to OrderItem, parse WITH FILL, output in explain. - -### 2.4 Column CODEC Clause (~20 tests) -**Problem:** CODEC(GCD, LZ4) in columns not captured -```sql -CREATE TABLE t (col UInt32 CODEC(GCD, LZ4)); -``` -**Files:** `parser/parser.go` (parseColumnDeclaration), `internal/explain/statements.go` -**Solution:** Parse CODEC clause into ColumnDeclaration, output in explain. - -### 2.5 Column EPHEMERAL Modifier (~15 tests) -**Problem:** EPHEMERAL keyword not captured -```sql -CREATE TABLE t (a Int EPHEMERAL); -``` -**Files:** `parser/parser.go` (parseColumnDeclaration) -**Solution:** Add Ephemeral field to ColumnDeclaration, parse and explain. - -### 2.6 CREATE TABLE ... AS function() (~15 tests) -**Problem:** CREATE TABLE AS s3Cluster(...) should have Function child -```sql -CREATE TABLE test AS s3Cluster('cluster', 'url'); -``` -**Files:** `parser/parser.go` (parseCreateTable), `internal/explain/statements.go` -**Solution:** Parse AS clause when followed by function call, store as TableFunction field. - -### 2.7 WithElement Wrapper for CTEs (~20 tests) -**Problem:** Some CTEs need WithElement wrapper in output -```sql -WITH sub AS (SELECT ...) SELECT ...; -``` -**Files:** `internal/explain/select.go` -**Solution:** Output WithElement wrapper when appropriate for CTE definitions. - -### 2.8 Float Scientific Notation (~15 tests) -**Problem:** Very small/large floats should use scientific notation -```sql -SELECT 2.2250738585072014e-308; -``` -**Files:** `internal/explain/format.go` -**Solution:** Format floats using scientific notation when appropriate. - -### 2.9 Negative Literals in Arrays (~10 tests) -**Problem:** Arrays with negatives may output Function instead of Literal -```sql -SELECT [-10000, 5750]; -``` -**Files:** `internal/explain/expressions.go` -**Solution:** Properly detect and format negative integer literals in arrays. - -### 2.10 Parameterized View Placeholders (~10 tests) -**Problem:** `{name:Type}` parameters in views -```sql -create view v as select number where number%2={parity:Int8}; -``` -**Files:** `internal/explain/expressions.go` -**Solution:** Output Parameter nodes correctly with type info. - -### 2.11 Column TTL (~10 tests) -**Problem:** TTL expression on columns not captured -```sql -CREATE TABLE t (c Int TTL expr()); -``` -**Files:** `parser/parser.go` (parseColumnDeclaration) -**Solution:** Parse TTL clause into ColumnDeclaration. - ---- - -## Phase 3: Lower Priority Fixes - -### 3.1 GROUPING SETS (~5 tests) -```sql -SELECT ... GROUP BY GROUPING SETS ((a), (b)); -``` - -### 3.2 QUALIFY Clause (~5 tests) -```sql -SELECT x QUALIFY row_number() OVER () = 1; -``` - -### 3.3 INTO OUTFILE TRUNCATE (~3 tests) -```sql -SELECT 1 INTO OUTFILE '/dev/null' TRUNCATE FORMAT Npy; -``` - -### 3.4 INTERVAL with Dynamic Type (~3 tests) -```sql -SELECT INTERVAL c0::Dynamic DAY; -``` - -### 3.5 ALTER TABLE with Multiple Operations (~3 tests) -```sql -ALTER TABLE t (DELETE WHERE ...), (UPDATE ... WHERE ...); -``` - -### 3.6 EXPLAIN SYNTAX for SYSTEM commands (~2 tests) -```sql -explain syntax system drop schema cache for hdfs; -``` - ---- - -## Implementation Order (Recommended) - -1. **Week 1: Parser Fundamentals** - - 1.2 Complex Type Casts (unlocks many tests) - - 1.1 view() Table Function (high impact) - - 1.3 DESCRIBE on Table Functions - -2. **Week 2: Parser Completeness** - - 1.4 INSERT INTO FUNCTION - - 1.5 CREATE USER/FUNCTION/DICTIONARY - - 1.6 SHOW SETTINGS - - 1.7 PASTE JOIN - - 1.8 any() Subquery - -3. **Week 3: Explain Layer - CREATE TABLE** - - 2.1 INDEX Clause - - 2.4 CODEC Clause - - 2.5 EPHEMERAL Modifier - - 2.6 CREATE TABLE AS function() - - 2.11 Column TTL - -4. **Week 4: Explain Layer - SELECT** - - 2.2 SETTINGS in Functions - - 2.3 WITH FILL - - 2.7 WithElement for CTEs - - 2.10 Parameterized View Placeholders - -5. **Week 5: Explain Layer - Formatting** - - 2.8 Float Scientific Notation - - 2.9 Negative Literals in Arrays - -6. **Week 6: Remaining Items** - - Phase 3 lower priority items - ---- - -## Estimated Impact - -| Phase | Tests Fixed | New Pass Rate | -|-------|-------------|---------------| -| 1.1-1.4 | ~115 | ~90% | -| 1.5-1.8 | ~20 | ~90.5% | -| 2.1-2.6 | ~140 | ~93% | -| 2.7-2.11 | ~65 | ~94% | -| Phase 3 | ~20 | ~94.5% | - ---- - -## Files to Modify - -### Parser Layer -- `parser/parser.go` - Main parser (CREATE, INSERT, DESCRIBE, SHOW, joins) -- `parser/expression.go` - Expression parsing (type casts, functions, special syntax) -- `ast/ast.go` - AST node definitions (IndexDefinition, new fields) - -### Explain Layer -- `internal/explain/statements.go` - CREATE TABLE explain -- `internal/explain/select.go` - SELECT explain (WITH FILL, CTEs) -- `internal/explain/functions.go` - Function explain (SETTINGS) -- `internal/explain/expressions.go` - Expression explain (literals, parameters) -- `internal/explain/format.go` - Output formatting (scientific notation) - ---- - -## Testing Strategy - -1. Run tests frequently: `go test ./parser -timeout 5s` -2. After each fix, verify no regressions: compare PASS count -3. Check specific test cases: `go test ./parser -v -run "TestParser/test_name"` -4. Monitor for infinite loops (timeout protection already in place) diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 2822f3fbe..43d527534 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -92,7 +92,7 @@ func Node(sb *strings.Builder, node interface{}, depth int) { case *ast.CaseExpr: explainCaseExpr(sb, n, indent, depth) case *ast.IntervalExpr: - explainIntervalExpr(sb, n, indent, depth) + explainIntervalExpr(sb, n, "", indent, depth) case *ast.ExistsExpr: explainExistsExpr(sb, n, indent, depth) case *ast.ExtractExpr: @@ -166,7 +166,9 @@ func Column(sb *strings.Builder, col *ast.ColumnDeclaration, depth int) { if col.Type != nil { children++ } - if col.Default != nil { + // EPHEMERAL columns without explicit default get defaultValueOfTypeName + hasEphemeralDefault := col.DefaultKind == "EPHEMERAL" && col.Default == nil + if col.Default != nil || hasEphemeralDefault { children++ } fmt.Fprintf(sb, "%sColumnDeclaration %s (children %d)\n", indent, col.Name, children) @@ -175,6 +177,9 @@ func Column(sb *strings.Builder, col *ast.ColumnDeclaration, depth int) { } if col.Default != nil { Node(sb, col.Default, depth+1) + } else if hasEphemeralDefault { + // EPHEMERAL columns without explicit default value show defaultValueOfTypeName function + fmt.Fprintf(sb, "%s Function defaultValueOfTypeName\n", indent) } } diff --git a/internal/explain/expressions.go b/internal/explain/expressions.go index c73f4fc98..d18384055 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -8,7 +8,7 @@ import ( ) func explainIdentifier(sb *strings.Builder, n *ast.Identifier, indent string) { - name := n.Name() + name := formatIdentifierName(n) if n.Alias != "" { fmt.Fprintf(sb, "%sIdentifier %s (alias %s)\n", indent, name, n.Alias) } else { @@ -16,6 +16,26 @@ func explainIdentifier(sb *strings.Builder, n *ast.Identifier, indent string) { } } +// formatIdentifierName formats an identifier name, handling JSON path notation +func formatIdentifierName(n *ast.Identifier) string { + if len(n.Parts) == 0 { + return "" + } + if len(n.Parts) == 1 { + return n.Parts[0] + } + result := n.Parts[0] + for _, p := range n.Parts[1:] { + // JSON path notation: ^fieldname should be formatted as ^`fieldname` + if strings.HasPrefix(p, "^") { + result += ".^`" + p[1:] + "`" + } else { + result += "." + p + } + } + return result +} + func explainLiteral(sb *strings.Builder, n *ast.Literal, indent string, depth int) { // Check if this is a tuple - either with expressions or empty if n.Type == ast.LiteralTuple { @@ -63,9 +83,7 @@ func explainLiteral(sb *strings.Builder, n *ast.Literal, indent string, depth in } hasComplexExpr := false for _, e := range exprs { - lit, isLit := e.(*ast.Literal) - // Non-literals or tuple/array literals count as complex - if !isLit || (isLit && (lit.Type == ast.LiteralTuple || lit.Type == ast.LiteralArray)) { + if !isSimpleLiteralOrNegation(e) { hasComplexExpr = true break } @@ -89,6 +107,23 @@ func explainLiteral(sb *strings.Builder, n *ast.Literal, indent string, depth in fmt.Fprintf(sb, "%sLiteral %s\n", indent, FormatLiteral(n)) } +// isSimpleLiteralOrNegation checks if an expression is a simple literal +// or a unary negation of a numeric literal (for array elements) +func isSimpleLiteralOrNegation(e ast.Expression) bool { + // Direct literal check + if lit, ok := e.(*ast.Literal); ok { + // Nested arrays/tuples are complex + return lit.Type != ast.LiteralTuple && lit.Type != ast.LiteralArray + } + // Unary minus of a literal integer/float is also simple (negative number) + if unary, ok := e.(*ast.UnaryExpr); ok && unary.Op == "-" { + if lit, ok := unary.Operand.(*ast.Literal); ok { + return lit.Type == ast.LiteralInteger || lit.Type == ast.LiteralFloat + } + } + return false +} + func explainBinaryExpr(sb *strings.Builder, n *ast.BinaryExpr, indent string, depth int) { // Convert operator to function name fnName := OperatorToFunction(n.Op) @@ -224,6 +259,16 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { case *ast.Identifier: // Identifiers with alias fmt.Fprintf(sb, "%sIdentifier %s (alias %s)\n", indent, e.Name(), n.Alias) + case *ast.IntervalExpr: + // Interval expressions with alias + explainIntervalExpr(sb, e, n.Alias, indent, depth) + case *ast.TernaryExpr: + // Ternary expressions become if functions with alias + fmt.Fprintf(sb, "%sFunction if (alias %s) (children %d)\n", indent, n.Alias, 1) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 3) + Node(sb, e.Condition, depth+2) + Node(sb, e.Then, depth+2) + Node(sb, e.Else, depth+2) default: // For other types, recursively explain and add alias info Node(sb, n.Expr, depth) diff --git a/internal/explain/format.go b/internal/explain/format.go index 268687211..73bb760e8 100644 --- a/internal/explain/format.go +++ b/internal/explain/format.go @@ -93,6 +93,27 @@ func formatArrayLiteral(val interface{}) string { for _, e := range exprs { if lit, ok := e.(*ast.Literal); ok { parts = append(parts, FormatLiteral(lit)) + } else if unary, ok := e.(*ast.UnaryExpr); ok && unary.Op == "-" { + // Handle negation of numeric literals + if lit, ok := unary.Operand.(*ast.Literal); ok { + if lit.Type == ast.LiteralInteger { + switch val := lit.Value.(type) { + case int64: + parts = append(parts, fmt.Sprintf("Int64_%d", -val)) + case uint64: + parts = append(parts, fmt.Sprintf("Int64_-%d", val)) + default: + parts = append(parts, fmt.Sprintf("Int64_-%v", lit.Value)) + } + } else if lit.Type == ast.LiteralFloat { + val := lit.Value.(float64) + parts = append(parts, fmt.Sprintf("Float64_%s", FormatFloat(-val))) + } else { + parts = append(parts, formatExprAsString(e)) + } + } else { + parts = append(parts, formatExprAsString(e)) + } } else if ident, ok := e.(*ast.Identifier); ok { parts = append(parts, ident.Name()) } else { @@ -333,11 +354,12 @@ func formatElementAsString(expr ast.Expression) string { case ast.LiteralFloat: return fmt.Sprintf("%v", e.Value) case ast.LiteralString: - // Quote strings with single quotes + // Quote strings with single quotes, triple-escape for nested context + // Expected output format is \\\' (three backslashes + quote) s := e.Value.(string) - // Escape single quotes in the string - s = strings.ReplaceAll(s, "'", "\\'") - return "\\'" + s + "\\'" + // Triple-escape single quotes for nested string literal context + s = strings.ReplaceAll(s, "'", "\\\\\\'") + return "\\\\\\'" + s + "\\\\\\'" case ast.LiteralBoolean: if e.Value.(bool) { return "true" diff --git a/internal/explain/functions.go b/internal/explain/functions.go index c522dc7f0..606a0ad5d 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -80,7 +80,11 @@ func explainLambda(sb *strings.Builder, n *ast.Lambda, indent string, depth int) func explainCastExpr(sb *strings.Builder, n *ast.CastExpr, indent string, depth int) { // CAST is represented as Function CAST with expr and type as arguments - fmt.Fprintf(sb, "%sFunction CAST (children %d)\n", indent, 1) + if n.Alias != "" { + fmt.Fprintf(sb, "%sFunction CAST (alias %s) (children %d)\n", indent, n.Alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction CAST (children %d)\n", indent, 1) + } fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 2) // For :: operator syntax with simple literals, format as string literal // For function syntax or complex expressions, use normal AST node @@ -281,7 +285,7 @@ func explainCaseExpr(sb *strings.Builder, n *ast.CaseExpr, indent string, depth } } -func explainIntervalExpr(sb *strings.Builder, n *ast.IntervalExpr, indent string, depth int) { +func explainIntervalExpr(sb *strings.Builder, n *ast.IntervalExpr, alias string, indent string, depth int) { // INTERVAL is represented as Function toInterval // Unit needs to be title-cased (e.g., YEAR -> Year) unit := n.Unit @@ -289,7 +293,11 @@ func explainIntervalExpr(sb *strings.Builder, n *ast.IntervalExpr, indent string unit = strings.ToUpper(unit[:1]) + strings.ToLower(unit[1:]) } fnName := "toInterval" + unit - fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1) + if alias != "" { + fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, alias, 1) + } else { + fmt.Fprintf(sb, "%sFunction %s (children %d)\n", indent, fnName, 1) + } fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 1) Node(sb, n.Value, depth+2) } diff --git a/internal/explain/select.go b/internal/explain/select.go index 5c0d30420..6e4d1d6f7 100644 --- a/internal/explain/select.go +++ b/internal/explain/select.go @@ -80,6 +80,17 @@ func explainSelectQuery(sb *strings.Builder, n *ast.SelectQuery, indent string, if n.Having != nil { Node(sb, n.Having, depth+1) } + // QUALIFY + if n.Qualify != nil { + Node(sb, n.Qualify, depth+1) + } + // WINDOW clause (named window definitions) + if len(n.Window) > 0 { + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Window)) + for range n.Window { + fmt.Fprintf(sb, "%s WindowListElement\n", indent) + } + } // ORDER BY if len(n.OrderBy) > 0 { fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.OrderBy)) @@ -188,6 +199,12 @@ func countSelectQueryChildren(n *ast.SelectQuery) int { if n.Having != nil { count++ } + if n.Qualify != nil { + count++ + } + if len(n.Window) > 0 { + count++ + } if len(n.OrderBy) > 0 { count++ } diff --git a/internal/explain/statements.go b/internal/explain/statements.go index 3a4cfa4f3..eaceeac36 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -46,8 +46,9 @@ func explainInsertQuery(sb *strings.Builder, n *ast.InsertQuery, indent string, func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, depth int) { // Handle special CREATE types if n.CreateFunction { - children := 1 // lambda + children := 2 // identifier + lambda fmt.Fprintf(sb, "%sCreateFunctionQuery %s (children %d)\n", indent, n.FunctionName, children) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.FunctionName) if n.FunctionBody != nil { Node(sb, n.FunctionBody, depth+1) } diff --git a/internal/explain/tables.go b/internal/explain/tables.go index 30cde1b5f..951285a6e 100644 --- a/internal/explain/tables.go +++ b/internal/explain/tables.go @@ -32,9 +32,16 @@ func explainTableExpression(sb *strings.Builder, n *ast.TableExpression, indent children := 1 // table fmt.Fprintf(sb, "%sTableExpression (children %d)\n", indent, children) // If there's a subquery with an alias, pass the alias to the subquery output - if subq, ok := n.Table.(*ast.Subquery); ok && n.Alias != "" { - fmt.Fprintf(sb, "%s Subquery (alias %s) (children %d)\n", indent, n.Alias, 1) - Node(sb, subq.Query, depth+2) + if subq, ok := n.Table.(*ast.Subquery); ok { + // Check if subquery contains an EXPLAIN query - convert to viewExplain function + if explainQ, ok := subq.Query.(*ast.ExplainQuery); ok { + explainViewExplain(sb, explainQ, n.Alias, indent+" ", depth+1) + } else if n.Alias != "" { + fmt.Fprintf(sb, "%s Subquery (alias %s) (children %d)\n", indent, n.Alias, 1) + Node(sb, subq.Query, depth+2) + } else { + Node(sb, n.Table, depth+1) + } } else if fn, ok := n.Table.(*ast.FunctionCall); ok && n.Alias != "" { // Table function with alias explainFunctionCallWithAlias(sb, fn, n.Alias, indent+" ", depth+1) @@ -46,6 +53,25 @@ func explainTableExpression(sb *strings.Builder, n *ast.TableExpression, indent } } +// explainViewExplain handles EXPLAIN queries used as table sources, converting to viewExplain function +func explainViewExplain(sb *strings.Builder, n *ast.ExplainQuery, alias string, indent string, depth int) { + // When EXPLAIN is used as a table source, it becomes viewExplain function + // Arguments: 'EXPLAIN', 'options', subquery + fmt.Fprintf(sb, "%sFunction viewExplain (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, 3) + // First argument: 'EXPLAIN' literal + fmt.Fprintf(sb, "%s Literal \\'EXPLAIN\\'\n", indent) + // Second argument: options string (empty for now since we don't track detailed options) + options := string(n.ExplainType) + if options == "PLAN" { + options = "" + } + fmt.Fprintf(sb, "%s Literal \\'%s\\'\n", indent, options) + // Third argument: the subquery being explained + fmt.Fprintf(sb, "%s Subquery (children %d)\n", indent, 1) + Node(sb, n.Statement, depth+3) +} + func explainTableIdentifierWithAlias(sb *strings.Builder, n *ast.TableIdentifier, alias string, indent string) { name := n.Table if n.Database != "" { diff --git a/parser/expression.go b/parser/expression.go index f12c02e9c..e26a69b39 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -194,6 +194,24 @@ func (p *Parser) parseImplicitAlias(expr ast.Expression) ast.Expression { case *ast.Subquery: e.Alias = alias return e + case *ast.CastExpr: + // Only set alias on CastExpr if using :: operator syntax + // Function-style CAST() aliases go to AliasedExpr + if e.OperatorSyntax { + e.Alias = alias + return e + } + return &ast.AliasedExpr{ + Position: expr.Pos(), + Expr: expr, + Alias: alias, + } + case *ast.CaseExpr: + e.Alias = alias + return e + case *ast.ExtractExpr: + e.Alias = alias + return e default: return &ast.AliasedExpr{ Position: expr.Pos(), @@ -1546,6 +1564,12 @@ func (p *Parser) parseAlias(left ast.Expression) ast.Expression { case *ast.Subquery: e.Alias = alias return e + case *ast.CaseExpr: + e.Alias = alias + return e + case *ast.ExtractExpr: + e.Alias = alias + return e default: return &ast.AliasedExpr{ Position: left.Pos(),