Skip to content

Commit 7a62b45

Browse files
kyleconroyclaude
andcommitted
Support binary expression WITH clauses like (SELECT ...) + (SELECT ...) AS name
- Remove special case for `(SELECT ...) AS name` in parseWithClause, letting it go through the expression parser which handles binary expressions - Add ScalarWith flag to WithElement to distinguish between: - "name AS (SELECT ...)" - standard CTE syntax - "(SELECT ...) AS name" - ClickHouse scalar WITH syntax - Update EXPLAIN output to use correct format based on ScalarWith flag Fixes 03212_variant_dynamic_cast_or_default/stmt51. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a273941 commit 7a62b45

File tree

4 files changed

+26
-37
lines changed

4 files changed

+26
-37
lines changed

ast/ast.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,9 +1593,10 @@ func (s *Subquery) expressionNode() {}
15931593

15941594
// WithElement represents a WITH element (CTE).
15951595
type WithElement struct {
1596-
Position token.Position `json:"-"`
1597-
Name string `json:"name"`
1598-
Query Expression `json:"query"` // Subquery or Expression
1596+
Position token.Position `json:"-"`
1597+
Name string `json:"name"`
1598+
Query Expression `json:"query"` // Subquery or Expression
1599+
ScalarWith bool `json:"scalar_with"` // True for "(expr) AS name" syntax, false for "name AS (SELECT ...)"
15991600
}
16001601

16011602
func (w *WithElement) Pos() token.Position { return w.Position }

internal/explain/expressions.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,14 +1195,23 @@ func explainWithElement(sb *strings.Builder, n *ast.WithElement, indent string,
11951195
Node(sb, e.Right, depth+2)
11961196
}
11971197
case *ast.Subquery:
1198-
// Check if this is "(subquery) AS alias" syntax vs "name AS (subquery)" syntax
1199-
if e.Alias != "" {
1200-
// "(subquery) AS alias" syntax: output Subquery with alias directly
1201-
fmt.Fprintf(sb, "%sSubquery (alias %s) (children 1)\n", indent, e.Alias)
1198+
// Output format depends on the WITH syntax:
1199+
// - "name AS (SELECT ...)": Standard CTE - output WithElement wrapping Subquery (no alias)
1200+
// - "(SELECT ...) AS name": Scalar WITH - output Subquery with alias
1201+
if n.ScalarWith {
1202+
// Scalar WITH: show alias on Subquery
1203+
alias := n.Name
1204+
if alias == "" {
1205+
alias = e.Alias
1206+
}
1207+
if alias != "" {
1208+
fmt.Fprintf(sb, "%sSubquery (alias %s) (children 1)\n", indent, alias)
1209+
} else {
1210+
fmt.Fprintf(sb, "%sSubquery (children 1)\n", indent)
1211+
}
12021212
Node(sb, e.Query, depth+1)
12031213
} else {
1204-
// "name AS (subquery)" syntax: output WithElement wrapping the Subquery
1205-
// The alias/name is not shown in the EXPLAIN AST output
1214+
// Standard CTE: wrap in WithElement without alias
12061215
fmt.Fprintf(sb, "%sWithElement (children 1)\n", indent)
12071216
fmt.Fprintf(sb, "%s Subquery (children 1)\n", indent)
12081217
Node(sb, e.Query, depth+2)

parser/parser.go

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,33 +1447,16 @@ func (p *Parser) parseWithClause() []ast.Expression {
14471447
elem.Name = name
14481448
elem.Query = &ast.Identifier{Position: pos, Parts: []string{name}}
14491449
}
1450-
} else if p.currentIs(token.LPAREN) && (p.peekIs(token.SELECT) || p.peekIs(token.WITH)) {
1451-
// Subquery: (SELECT ...) AS name or (WITH ... SELECT ...) AS name
1452-
// In this syntax, the alias goes on the Subquery, not on WithElement
1453-
p.nextToken()
1454-
subquery := p.parseSelectWithUnion()
1455-
if !p.expect(token.RPAREN) {
1456-
return nil
1457-
}
1458-
sq := &ast.Subquery{Query: subquery}
1459-
1460-
if !p.expect(token.AS) {
1461-
return nil
1462-
}
1463-
1464-
// Alias can be IDENT or certain keywords (VALUES, KEY, etc.)
1465-
// Set alias on the Subquery for "(subquery) AS name" syntax
1466-
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1467-
sq.Alias = p.current.Value
1468-
p.nextToken()
1469-
}
1470-
elem.Query = sq
14711450
} else {
14721451
// Scalar WITH: expr AS name (ClickHouse style)
1473-
// Examples: WITH 1 AS x, WITH 'hello' AS s, WITH func() AS f
1474-
// Also handles lambda: WITH x -> toString(x) AS lambda_1
1452+
// This handles various forms:
1453+
// - WITH 1 AS x, WITH 'hello' AS s, WITH func() AS f
1454+
// - WITH (SELECT ...) AS name (subquery expression)
1455+
// - WITH (SELECT ...) + (SELECT ...) AS name (binary expression of subqueries)
1456+
// - WITH x -> toString(x) AS lambda_1 (lambda expression)
14751457
// Arrow has OR_PREC precedence, so it gets parsed with ALIAS_PREC
14761458
// Note: AS name is optional in ClickHouse, e.g., WITH 1 SELECT 1 is valid
1459+
elem.ScalarWith = true
14771460
elem.Query = p.parseExpression(ALIAS_PREC) // Use ALIAS_PREC to stop before AS
14781461

14791462
// AS name is optional
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt51": true
4-
}
5-
}
1+
{}

0 commit comments

Comments
 (0)