Skip to content

Commit 04d68a5

Browse files
kyleconroyclaude
andcommitted
Preserve function name case from SQL source in EXPLAIN AST output
ClickHouse EXPLAIN AST preserves the original case of function names as written in the SQL source (e.g., CEIL stays CEIL, COALESCE stays COALESCE). Only a few functions are normalized to specific canonical forms (e.g., DATEDIFF→dateDiff, POSITION→position, SUBSTRING→substring). This fixes issues where special parser functions (parseIfFunction, parseExtract, parseSubstring, parseArrayConstructor) were hardcoding lowercase names instead of preserving the original case from the token. Also fixes parseKeywordAsFunction which was incorrectly lowercasing all keyword-based function names. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 050d0b2 commit 04d68a5

File tree

8 files changed

+34
-52
lines changed

8 files changed

+34
-52
lines changed

internal/explain/format.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -448,24 +448,22 @@ func formatExprForType(expr ast.Expression) string {
448448
// NormalizeFunctionName normalizes function names to match ClickHouse's EXPLAIN AST output
449449
func NormalizeFunctionName(name string) string {
450450
// ClickHouse normalizes certain function names in EXPLAIN AST
451-
// Note: lcase, ucase, mid are preserved as-is by ClickHouse EXPLAIN AST
451+
// Most functions preserve their original case from the SQL source.
452+
// Only a few are normalized to specific canonical forms.
452453
normalized := map[string]string{
453-
"trim": "trimBoth",
454-
"ltrim": "trimLeft",
455-
"rtrim": "trimRight",
456-
"ceiling": "ceil",
457-
"log10": "log10",
458-
"log2": "log2",
459-
"rand": "rand",
460-
"ifnull": "ifNull",
461-
"nullif": "nullIf",
462-
"coalesce": "coalesce",
463-
"greatest": "greatest",
464-
"least": "least",
454+
// TRIM functions are normalized to trimBoth/trimLeft/trimRight
455+
"trim": "trimBoth",
456+
"ltrim": "trimLeft",
457+
"rtrim": "trimRight",
458+
// CONCAT_WS is normalized to concat
465459
"concat_ws": "concat",
460+
// Position is normalized to lowercase
466461
"position": "position",
467-
"date_diff": "dateDiff",
468-
"datediff": "dateDiff",
462+
// SUBSTRING is normalized to lowercase (but SUBSTR preserves case)
463+
"substring": "substring",
464+
// DateDiff variants are normalized to camelCase
465+
"date_diff": "dateDiff",
466+
"datediff": "dateDiff",
469467
// SQL standard ANY/ALL subquery operators - simple cases
470468
"anyequals": "in",
471469
"allnotequals": "notIn",

parser/expression.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1596,7 +1596,8 @@ func (p *Parser) wrapWithAlias(expr ast.Expression, alias string) ast.Expression
15961596

15971597
func (p *Parser) parseExtract() ast.Expression {
15981598
pos := p.current.Pos
1599-
p.nextToken() // skip EXTRACT
1599+
name := p.current.Value // preserve original case
1600+
p.nextToken() // skip EXTRACT
16001601

16011602
if !p.expect(token.LPAREN) {
16021603
return nil
@@ -1646,7 +1647,7 @@ func (p *Parser) parseExtract() ast.Expression {
16461647
p.expect(token.RPAREN)
16471648
return &ast.FunctionCall{
16481649
Position: pos,
1649-
Name: "extract",
1650+
Name: name,
16501651
Arguments: args,
16511652
}
16521653
}
@@ -1670,7 +1671,7 @@ func (p *Parser) parseExtract() ast.Expression {
16701671

16711672
return &ast.FunctionCall{
16721673
Position: pos,
1673-
Name: "extract",
1674+
Name: name,
16741675
Arguments: args,
16751676
}
16761677
}
@@ -1764,7 +1765,8 @@ func (p *Parser) parsePositionalParameter() ast.Expression {
17641765

17651766
func (p *Parser) parseSubstring() ast.Expression {
17661767
pos := p.current.Pos
1767-
p.nextToken() // skip SUBSTRING
1768+
name := p.current.Value // preserve original case
1769+
p.nextToken() // skip SUBSTRING
17681770

17691771
if !p.expect(token.LPAREN) {
17701772
return nil
@@ -1871,7 +1873,7 @@ func (p *Parser) parseSubstring() ast.Expression {
18711873

18721874
return &ast.FunctionCall{
18731875
Position: pos,
1874-
Name: "substring",
1876+
Name: name,
18751877
Arguments: args,
18761878
}
18771879
}
@@ -2705,7 +2707,8 @@ func (p *Parser) parseQualifiedColumnsMatcher(qualifier string, pos token.Positi
27052707

27062708
func (p *Parser) parseArrayConstructor() ast.Expression {
27072709
pos := p.current.Pos
2708-
p.nextToken() // skip ARRAY
2710+
name := p.current.Value // preserve original case
2711+
p.nextToken() // skip ARRAY
27092712

27102713
if !p.expect(token.LPAREN) {
27112714
return nil
@@ -2720,14 +2723,15 @@ func (p *Parser) parseArrayConstructor() ast.Expression {
27202723

27212724
return &ast.FunctionCall{
27222725
Position: pos,
2723-
Name: "array",
2726+
Name: name,
27242727
Arguments: args,
27252728
}
27262729
}
27272730

27282731
func (p *Parser) parseIfFunction() ast.Expression {
27292732
pos := p.current.Pos
2730-
p.nextToken() // skip IF
2733+
name := p.current.Value // preserve original case
2734+
p.nextToken() // skip IF
27312735

27322736
if !p.expect(token.LPAREN) {
27332737
return nil
@@ -2742,15 +2746,15 @@ func (p *Parser) parseIfFunction() ast.Expression {
27422746

27432747
return &ast.FunctionCall{
27442748
Position: pos,
2745-
Name: "if",
2749+
Name: name,
27462750
Arguments: args,
27472751
}
27482752
}
27492753

27502754
func (p *Parser) parseKeywordAsFunction() ast.Expression {
27512755
pos := p.current.Pos
2752-
name := strings.ToLower(p.current.Value)
2753-
p.nextToken() // skip keyword
2756+
name := p.current.Value // preserve original case
2757+
p.nextToken() // skip keyword
27542758

27552759
if !p.expect(token.LPAREN) {
27562760
return nil
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"explain_todo":{"stmt2":true}}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt33": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt21": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt21": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt22": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt58": true
4-
}
5-
}
1+
{}

0 commit comments

Comments
 (0)