diff --git a/ast/ast.go b/ast/ast.go index 5c3111d1f..626e458e5 100644 --- a/ast/ast.go +++ b/ast/ast.go @@ -258,6 +258,7 @@ type CreateQuery struct { TTL *TTLClause `json:"ttl,omitempty"` Settings []*SettingExpr `json:"settings,omitempty"` AsSelect Statement `json:"as_select,omitempty"` + AsTableFunction Expression `json:"as_table_function,omitempty"` // AS table_function(...) in CREATE TABLE Comment string `json:"comment,omitempty"` OnCluster string `json:"on_cluster,omitempty"` CreateDatabase bool `json:"create_database,omitempty"` diff --git a/internal/explain/explain.go b/internal/explain/explain.go index 7b1fbdb18..ce2bc8985 100644 --- a/internal/explain/explain.go +++ b/internal/explain/explain.go @@ -8,6 +8,10 @@ import ( "github.com/sqlc-dev/doubleclick/ast" ) +// inSubqueryContext is a package-level flag to track when we're inside a Subquery +// This affects how negated literals with aliases are formatted +var inSubqueryContext 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/expressions.go b/internal/explain/expressions.go index 8db7ea7ec..dd058b158 100644 --- a/internal/explain/expressions.go +++ b/internal/explain/expressions.go @@ -314,7 +314,12 @@ func explainSubquery(sb *strings.Builder, n *ast.Subquery, indent string, depth } else { fmt.Fprintf(sb, "%sSubquery (children %d)\n", indent, children) } + // Set context flag before recursing into subquery content + // This affects how negated literals with aliases are formatted + prevContext := inSubqueryContext + inSubqueryContext = true Node(sb, n.Query, depth+1) + inSubqueryContext = prevContext } func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { @@ -398,6 +403,28 @@ func explainAliasedExpr(sb *strings.Builder, n *ast.AliasedExpr, depth int) { Node(sb, e.Right, depth+2) } case *ast.UnaryExpr: + // When inside a Subquery context, negated numeric literals should be output as Literal Int64_-N + // Otherwise, output as Function negate + if inSubqueryContext && e.Op == "-" { + if lit, ok := e.Operand.(*ast.Literal); ok { + switch lit.Type { + case ast.LiteralInteger: + switch val := lit.Value.(type) { + case int64: + fmt.Fprintf(sb, "%sLiteral Int64_%d (alias %s)\n", indent, -val, n.Alias) + return + case uint64: + fmt.Fprintf(sb, "%sLiteral Int64_-%d (alias %s)\n", indent, val, n.Alias) + return + } + case ast.LiteralFloat: + val := lit.Value.(float64) + s := FormatFloat(-val) + fmt.Fprintf(sb, "%sLiteral Float64_%s (alias %s)\n", indent, s, n.Alias) + return + } + } + } // Unary expressions become functions with alias fnName := UnaryOperatorToFunction(e.Op) fmt.Fprintf(sb, "%sFunction %s (alias %s) (children %d)\n", indent, fnName, n.Alias, 1) diff --git a/internal/explain/functions.go b/internal/explain/functions.go index e8f83bc56..028126a4b 100644 --- a/internal/explain/functions.go +++ b/internal/explain/functions.go @@ -42,9 +42,13 @@ func explainFunctionCallWithAlias(sb *strings.Builder, n *ast.FunctionCall, alia fmt.Fprintln(sb) for _, arg := range n.Arguments { // For view() table function, unwrap Subquery wrapper + // Also reset the subquery context since view() SELECT is not in a Subquery node if strings.ToLower(n.Name) == "view" { if sq, ok := arg.(*ast.Subquery); ok { + prevContext := inSubqueryContext + inSubqueryContext = false Node(sb, sq.Query, depth+2) + inSubqueryContext = prevContext continue } } diff --git a/internal/explain/statements.go b/internal/explain/statements.go index f70b7461c..83c8bf627 100644 --- a/internal/explain/statements.go +++ b/internal/explain/statements.go @@ -97,6 +97,9 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, if n.AsSelect != nil { children++ } + if n.AsTableFunction != nil { + children++ + } // ClickHouse adds an extra space before (children N) for CREATE DATABASE if n.CreateDatabase { fmt.Fprintf(sb, "%sCreateQuery %s (children %d)\n", indent, name, children) @@ -112,6 +115,16 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, if len(n.Indexes) > 0 { childrenCount++ } + // Check for PRIMARY KEY constraints in column declarations + var primaryKeyColumns []string + for _, col := range n.Columns { + if col.PrimaryKey { + primaryKeyColumns = append(primaryKeyColumns, col.Name) + } + } + if len(primaryKeyColumns) > 0 { + childrenCount++ // Add for Function tuple containing PRIMARY KEY columns + } fmt.Fprintf(sb, "%s Columns definition (children %d)\n", indent, childrenCount) if len(n.Columns) > 0 { fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(n.Columns)) @@ -125,6 +138,14 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, Index(sb, idx, depth+3) } } + // Output PRIMARY KEY columns as Function tuple + if len(primaryKeyColumns) > 0 { + fmt.Fprintf(sb, "%s Function tuple (children %d)\n", indent, 1) + fmt.Fprintf(sb, "%s ExpressionList (children %d)\n", indent, len(primaryKeyColumns)) + for _, colName := range primaryKeyColumns { + fmt.Fprintf(sb, "%s Identifier %s\n", indent, colName) + } + } } if n.Engine != nil || len(n.OrderBy) > 0 || len(n.PrimaryKey) > 0 || n.PartitionBy != nil || len(n.Settings) > 0 { storageChildren := 0 @@ -228,6 +249,10 @@ func explainCreateQuery(sb *strings.Builder, n *ast.CreateQuery, indent string, // AS SELECT is output directly without Subquery wrapper Node(sb, n.AsSelect, depth+1) } + if n.AsTableFunction != nil { + // AS table_function(...) is output directly + Node(sb, n.AsTableFunction, depth+1) + } } func explainDropQuery(sb *strings.Builder, n *ast.DropQuery, indent string, depth int) { @@ -337,6 +362,27 @@ func explainShowQuery(sb *strings.Builder, n *ast.ShowQuery, indent string) { if showType == "Settings" || showType == "Databases" { showType = "Tables" } + + // SHOW CREATE TABLE has special output format with database and table identifiers + if n.ShowType == ast.ShowCreate && (n.Database != "" || n.From != "") { + // Format: ShowCreateTableQuery database table (children 2) + name := n.From + if n.Database != "" && n.From != "" { + fmt.Fprintf(sb, "%sShowCreateTableQuery %s %s (children 2)\n", indent, n.Database, n.From) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.From) + } else if n.From != "" { + fmt.Fprintf(sb, "%sShowCreateTableQuery %s (children 1)\n", indent, name) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, name) + } else if n.Database != "" { + fmt.Fprintf(sb, "%sShowCreateTableQuery %s (children 1)\n", indent, n.Database) + fmt.Fprintf(sb, "%s Identifier %s\n", indent, n.Database) + } else { + fmt.Fprintf(sb, "%sShow%s\n", indent, showType) + } + return + } + fmt.Fprintf(sb, "%sShow%s\n", indent, showType) } diff --git a/internal/explain/tables.go b/internal/explain/tables.go index b17111d90..9347672bf 100644 --- a/internal/explain/tables.go +++ b/internal/explain/tables.go @@ -41,7 +41,11 @@ func explainTableExpression(sb *strings.Builder, n *ast.TableExpression, indent 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) + // Set context flag for subquery - affects how negated literals with aliases are formatted + prevContext := inSubqueryContext + inSubqueryContext = true Node(sb, subq.Query, depth+2) + inSubqueryContext = prevContext } else { Node(sb, n.Table, depth+1) } diff --git a/parser/expression.go b/parser/expression.go index 79698675b..1734c7ab0 100644 --- a/parser/expression.go +++ b/parser/expression.go @@ -506,7 +506,7 @@ func (p *Parser) parseFunctionCall(name string, pos token.Position) *ast.Functio } // Handle view() and similar functions that take a subquery as argument - // view(SELECT ...) should parse SELECT as a subquery, not expression + // view(SELECT ...) should parse SELECT as a subquery if strings.ToLower(name) == "view" && (p.currentIs(token.SELECT) || p.currentIs(token.WITH)) { subquery := p.parseSelectWithUnion() fn.Arguments = []ast.Expression{&ast.Subquery{Position: pos, Query: subquery}} diff --git a/parser/parser.go b/parser/parser.go index 4a70be3f6..7b162fe20 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -1500,17 +1500,16 @@ done_table_options: p.nextToken() p.parseIdentifierName() } else if p.currentIs(token.LPAREN) { - // AS function(...) - skip the function call - depth := 1 - p.nextToken() - for depth > 0 && !p.currentIs(token.EOF) { - if p.currentIs(token.LPAREN) { - depth++ - } else if p.currentIs(token.RPAREN) { - depth-- - } + // AS function(...) - parse as a function call + fn := &ast.FunctionCall{Name: name} + p.nextToken() // skip ( + if !p.currentIs(token.RPAREN) { + fn.Arguments = p.parseExpressionList() + } + if p.currentIs(token.RPAREN) { p.nextToken() } + create.AsTableFunction = fn } _ = name // Use name for future AS table support } @@ -1595,8 +1594,13 @@ func (p *Parser) parseCreateView(create *ast.CreateQuery) { } } - // Handle TO (target table for materialized views) + // Handle TO (target table for materialized views only) + // TO clause is not valid for regular views - only for MATERIALIZED VIEW if p.currentIs(token.TO) { + if !create.Materialized { + p.errors = append(p.errors, fmt.Errorf("TO clause is only valid for MATERIALIZED VIEW, not VIEW")) + return + } p.nextToken() create.To = p.parseIdentifierName() } diff --git a/parser/testdata/00098_j_union_all/metadata.json b/parser/testdata/00098_j_union_all/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/00098_j_union_all/metadata.json +++ b/parser/testdata/00098_j_union_all/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/01668_avg_weighted_ubsan/metadata.json b/parser/testdata/01668_avg_weighted_ubsan/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/01668_avg_weighted_ubsan/metadata.json +++ b/parser/testdata/01668_avg_weighted_ubsan/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/01761_cast_to_enum_nullable/metadata.json b/parser/testdata/01761_cast_to_enum_nullable/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/01761_cast_to_enum_nullable/metadata.json +++ b/parser/testdata/01761_cast_to_enum_nullable/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/01818_case_float_value_fangyc/metadata.json b/parser/testdata/01818_case_float_value_fangyc/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/01818_case_float_value_fangyc/metadata.json +++ b/parser/testdata/01818_case_float_value_fangyc/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/02118_show_create_table_rocksdb/metadata.json b/parser/testdata/02118_show_create_table_rocksdb/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/02118_show_create_table_rocksdb/metadata.json +++ b/parser/testdata/02118_show_create_table_rocksdb/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/02189_join_type_conversion/metadata.json b/parser/testdata/02189_join_type_conversion/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/02189_join_type_conversion/metadata.json +++ b/parser/testdata/02189_join_type_conversion/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/02554_invalid_create_view_syntax/metadata.json b/parser/testdata/02554_invalid_create_view_syntax/metadata.json index d10cf5963..fb6ca20c9 100644 --- a/parser/testdata/02554_invalid_create_view_syntax/metadata.json +++ b/parser/testdata/02554_invalid_create_view_syntax/metadata.json @@ -1 +1 @@ -{"todo": true, "parse_error": true} \ No newline at end of file +{"parse_error": true} diff --git a/parser/testdata/03290_limit_by_segv/metadata.json b/parser/testdata/03290_limit_by_segv/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/03290_limit_by_segv/metadata.json +++ b/parser/testdata/03290_limit_by_segv/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/03293_forbid_cluster_table_engine/metadata.json b/parser/testdata/03293_forbid_cluster_table_engine/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/03293_forbid_cluster_table_engine/metadata.json +++ b/parser/testdata/03293_forbid_cluster_table_engine/metadata.json @@ -1 +1 @@ -{"todo": true} +{} diff --git a/parser/testdata/03532_redis_empty_variant_key/metadata.json b/parser/testdata/03532_redis_empty_variant_key/metadata.json index ef120d978..0967ef424 100644 --- a/parser/testdata/03532_redis_empty_variant_key/metadata.json +++ b/parser/testdata/03532_redis_empty_variant_key/metadata.json @@ -1 +1 @@ -{"todo": true} +{}