Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
4 changes: 4 additions & 0 deletions internal/explain/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions internal/explain/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions internal/explain/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
46 changes: 46 additions & 0 deletions internal/explain/statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down
4 changes: 4 additions & 0 deletions internal/explain/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion parser/expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down
24 changes: 14 additions & 10 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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()
}
Expand Down
2 changes: 1 addition & 1 deletion parser/testdata/00098_j_union_all/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
2 changes: 1 addition & 1 deletion parser/testdata/01668_avg_weighted_ubsan/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
2 changes: 1 addition & 1 deletion parser/testdata/01761_cast_to_enum_nullable/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
2 changes: 1 addition & 1 deletion parser/testdata/02189_join_type_conversion/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true, "parse_error": true}
{"parse_error": true}
2 changes: 1 addition & 1 deletion parser/testdata/03290_limit_by_segv/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"todo": true}
{}