Skip to content

Commit 73b21ea

Browse files
committed
Add alias support in CAST, SUBSTRING, and TRIM functions
- Handle CAST(expr AS alias AS Type) and CAST(expr alias AS Type) patterns - Support aliases on both expression and type in comma-style CAST - Update SUBSTRING to handle aliases on all arguments in FROM/FOR and comma styles - Update TRIM to handle aliases on trimChars and FROM expression - Add trimBoth function name for BOTH modifier - Update wrapWithAlias to replace existing aliases instead of double-wrapping This enables the parser to correctly handle ClickHouse's special operator alias syntax where expressions inside function arguments can have aliases.
1 parent f1157ad commit 73b21ea

File tree

7 files changed

+291
-53
lines changed

7 files changed

+291
-53
lines changed

parser/expression.go

Lines changed: 287 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,19 +1065,162 @@ func (p *Parser) parseCast() ast.Expression {
10651065
expr.Expr = p.parseExpression(ALIAS_PREC)
10661066

10671067
// Handle both CAST(x AS Type) and CAST(x, 'Type') or CAST(x, expr) syntax
1068+
// Also handle CAST(x AS alias AS Type) and CAST(x alias AS Type) where alias is for the expression
1069+
// And CAST(x AS alias, 'Type') and CAST(x alias, 'Type') for comma-style with aliased expression
10681070
if p.currentIs(token.AS) {
1069-
p.nextToken()
1071+
p.nextToken() // skip AS
1072+
1073+
// Check what comes after the identifier
1074+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1075+
if p.peekIs(token.AS) {
1076+
// "AS alias AS Type" pattern
1077+
alias := p.current.Value
1078+
p.nextToken() // skip alias
1079+
p.nextToken() // skip AS
1080+
expr.Expr = p.wrapWithAlias(expr.Expr, alias)
1081+
expr.Type = p.parseDataType()
1082+
expr.UsedASSyntax = true
1083+
} else if p.peekIs(token.COMMA) {
1084+
// "AS alias, 'Type'" pattern - comma-style with aliased expression
1085+
alias := p.current.Value
1086+
p.nextToken() // skip alias
1087+
p.nextToken() // skip comma
1088+
expr.Expr = p.wrapWithAlias(expr.Expr, alias)
1089+
// Parse type (which may also have an alias)
1090+
if p.currentIs(token.STRING) {
1091+
typeStr := p.current.Value
1092+
typePos := p.current.Pos
1093+
p.nextToken()
1094+
// Check for alias on the type string
1095+
if p.currentIs(token.AS) {
1096+
p.nextToken()
1097+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1098+
typeAlias := p.current.Value
1099+
p.nextToken()
1100+
expr.TypeExpr = &ast.AliasedExpr{
1101+
Position: typePos,
1102+
Expr: &ast.Literal{Position: typePos, Type: ast.LiteralString, Value: typeStr},
1103+
Alias: typeAlias,
1104+
}
1105+
} else {
1106+
expr.Type = &ast.DataType{Position: typePos, Name: typeStr}
1107+
}
1108+
} else if p.currentIs(token.IDENT) && !p.peekIs(token.LPAREN) && !p.peekIs(token.COMMA) {
1109+
// Implicit alias: cast('1234' AS lhs, 'UInt32' rhs)
1110+
typeAlias := p.current.Value
1111+
p.nextToken()
1112+
expr.TypeExpr = &ast.AliasedExpr{
1113+
Position: typePos,
1114+
Expr: &ast.Literal{Position: typePos, Type: ast.LiteralString, Value: typeStr},
1115+
Alias: typeAlias,
1116+
}
1117+
} else {
1118+
expr.Type = &ast.DataType{Position: typePos, Name: typeStr}
1119+
}
1120+
} else {
1121+
expr.TypeExpr = p.parseExpression(LOWEST)
1122+
}
1123+
} else {
1124+
// Just "AS Type"
1125+
expr.Type = p.parseDataType()
1126+
expr.UsedASSyntax = true
1127+
}
1128+
} else {
1129+
// Just "AS Type"
1130+
expr.Type = p.parseDataType()
1131+
expr.UsedASSyntax = true
1132+
}
1133+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && p.peekIs(token.AS) {
1134+
// Handle "expr alias AS Type" pattern (alias without AS keyword)
1135+
alias := p.current.Value
1136+
p.nextToken() // skip alias
1137+
p.nextToken() // skip AS
1138+
expr.Expr = p.wrapWithAlias(expr.Expr, alias)
10701139
expr.Type = p.parseDataType()
10711140
expr.UsedASSyntax = true
1141+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && p.peekIs(token.COMMA) {
1142+
// Handle "expr alias, 'Type'" pattern (alias without AS keyword, comma-style)
1143+
alias := p.current.Value
1144+
p.nextToken() // skip alias
1145+
p.nextToken() // skip comma
1146+
expr.Expr = p.wrapWithAlias(expr.Expr, alias)
1147+
// Parse type (which may also have an alias)
1148+
if p.currentIs(token.STRING) {
1149+
typeStr := p.current.Value
1150+
typePos := p.current.Pos
1151+
p.nextToken()
1152+
// Check for alias on the type string
1153+
if p.currentIs(token.AS) {
1154+
p.nextToken()
1155+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1156+
typeAlias := p.current.Value
1157+
p.nextToken()
1158+
expr.TypeExpr = &ast.AliasedExpr{
1159+
Position: typePos,
1160+
Expr: &ast.Literal{Position: typePos, Type: ast.LiteralString, Value: typeStr},
1161+
Alias: typeAlias,
1162+
}
1163+
} else {
1164+
expr.Type = &ast.DataType{Position: typePos, Name: typeStr}
1165+
}
1166+
} else if p.currentIs(token.IDENT) && !p.peekIs(token.LPAREN) && !p.peekIs(token.COMMA) {
1167+
// Implicit alias: cast('1234' lhs, 'UInt32' rhs)
1168+
typeAlias := p.current.Value
1169+
p.nextToken()
1170+
expr.TypeExpr = &ast.AliasedExpr{
1171+
Position: typePos,
1172+
Expr: &ast.Literal{Position: typePos, Type: ast.LiteralString, Value: typeStr},
1173+
Alias: typeAlias,
1174+
}
1175+
} else {
1176+
expr.Type = &ast.DataType{Position: typePos, Name: typeStr}
1177+
}
1178+
} else {
1179+
expr.TypeExpr = p.parseExpression(LOWEST)
1180+
}
10721181
} else if p.currentIs(token.COMMA) {
10731182
p.nextToken()
10741183
// Type can be given as a string literal or an expression (e.g., if(cond, 'Type1', 'Type2'))
1184+
// It can also have an alias like: cast('1234', 'UInt32' AS rhs)
10751185
if p.currentIs(token.STRING) {
1076-
expr.Type = &ast.DataType{
1077-
Position: p.current.Pos,
1078-
Name: p.current.Value,
1079-
}
1186+
typeStr := p.current.Value
1187+
typePos := p.current.Pos
10801188
p.nextToken()
1189+
// Check for alias on the type string
1190+
if p.currentIs(token.AS) {
1191+
p.nextToken()
1192+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1193+
alias := p.current.Value
1194+
p.nextToken()
1195+
// Store as aliased literal in TypeExpr
1196+
expr.TypeExpr = &ast.AliasedExpr{
1197+
Position: typePos,
1198+
Expr: &ast.Literal{
1199+
Position: typePos,
1200+
Type: ast.LiteralString,
1201+
Value: typeStr,
1202+
},
1203+
Alias: alias,
1204+
}
1205+
} else {
1206+
expr.Type = &ast.DataType{Position: typePos, Name: typeStr}
1207+
}
1208+
} else if p.currentIs(token.IDENT) && !p.peekIs(token.LPAREN) && !p.peekIs(token.COMMA) {
1209+
// Implicit alias (no AS keyword): cast('1234', 'UInt32' rhs)
1210+
alias := p.current.Value
1211+
p.nextToken()
1212+
expr.TypeExpr = &ast.AliasedExpr{
1213+
Position: typePos,
1214+
Expr: &ast.Literal{
1215+
Position: typePos,
1216+
Type: ast.LiteralString,
1217+
Value: typeStr,
1218+
},
1219+
Alias: alias,
1220+
}
1221+
} else {
1222+
expr.Type = &ast.DataType{Position: typePos, Name: typeStr}
1223+
}
10811224
} else {
10821225
// Parse as expression for dynamic type casting
10831226
expr.TypeExpr = p.parseExpression(LOWEST)
@@ -1089,6 +1232,29 @@ func (p *Parser) parseCast() ast.Expression {
10891232
return expr
10901233
}
10911234

1235+
// wrapWithAlias wraps an expression with an alias, handling different expression types appropriately
1236+
// If the expression already has an alias (e.g., AliasedExpr), the new alias replaces/overrides it
1237+
func (p *Parser) wrapWithAlias(expr ast.Expression, alias string) ast.Expression {
1238+
switch e := expr.(type) {
1239+
case *ast.Identifier:
1240+
e.Alias = alias
1241+
return e
1242+
case *ast.FunctionCall:
1243+
e.Alias = alias
1244+
return e
1245+
case *ast.AliasedExpr:
1246+
// Replace the alias instead of double-wrapping
1247+
e.Alias = alias
1248+
return e
1249+
default:
1250+
return &ast.AliasedExpr{
1251+
Position: expr.Pos(),
1252+
Expr: expr,
1253+
Alias: alias,
1254+
}
1255+
}
1256+
}
1257+
10921258
func (p *Parser) parseExtract() ast.Expression {
10931259
pos := p.current.Pos
10941260
p.nextToken() // skip EXTRACT
@@ -1234,24 +1400,101 @@ func (p *Parser) parseSubstring() ast.Expression {
12341400
return nil
12351401
}
12361402

1237-
args := []ast.Expression{p.parseExpression(LOWEST)}
1403+
// Parse first argument (source string) - may have alias before FROM
1404+
// Use ALIAS_PREC to not consume AS
1405+
firstArg := p.parseExpression(ALIAS_PREC)
12381406

1239-
// Handle FROM
1407+
// Check for alias on first argument (AS alias or just alias before FROM)
1408+
if p.currentIs(token.AS) {
1409+
p.nextToken()
1410+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1411+
alias := p.current.Value
1412+
p.nextToken()
1413+
firstArg = p.wrapWithAlias(firstArg, alias)
1414+
}
1415+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && (p.peekIs(token.FROM) || p.peekIs(token.COMMA)) {
1416+
// Implicit alias before FROM or COMMA
1417+
alias := p.current.Value
1418+
p.nextToken()
1419+
firstArg = p.wrapWithAlias(firstArg, alias)
1420+
}
1421+
1422+
args := []ast.Expression{firstArg}
1423+
1424+
// Handle FROM or COMMA for second argument
12401425
if p.currentIs(token.FROM) {
12411426
p.nextToken()
1242-
args = append(args, p.parseExpression(LOWEST))
1427+
// Parse start position - may have alias before FOR or )
1428+
startArg := p.parseExpression(ALIAS_PREC)
1429+
// Check for alias
1430+
if p.currentIs(token.AS) {
1431+
p.nextToken()
1432+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1433+
alias := p.current.Value
1434+
p.nextToken()
1435+
startArg = p.wrapWithAlias(startArg, alias)
1436+
}
1437+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && (p.peekIs(token.FOR) || p.peekIs(token.RPAREN)) {
1438+
alias := p.current.Value
1439+
p.nextToken()
1440+
startArg = p.wrapWithAlias(startArg, alias)
1441+
}
1442+
args = append(args, startArg)
12431443
} else if p.currentIs(token.COMMA) {
12441444
p.nextToken()
1245-
args = append(args, p.parseExpression(LOWEST))
1445+
// Parse second argument with possible alias
1446+
startArg := p.parseExpression(ALIAS_PREC)
1447+
if p.currentIs(token.AS) {
1448+
p.nextToken()
1449+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1450+
alias := p.current.Value
1451+
p.nextToken()
1452+
startArg = p.wrapWithAlias(startArg, alias)
1453+
}
1454+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && (p.peekIs(token.COMMA) || p.peekIs(token.RPAREN)) {
1455+
alias := p.current.Value
1456+
p.nextToken()
1457+
startArg = p.wrapWithAlias(startArg, alias)
1458+
}
1459+
args = append(args, startArg)
12461460
}
12471461

1248-
// Handle FOR
1462+
// Handle FOR or COMMA for third argument
12491463
if p.currentIs(token.FOR) {
12501464
p.nextToken()
1251-
args = append(args, p.parseExpression(LOWEST))
1465+
// Parse length - may have alias before )
1466+
lenArg := p.parseExpression(ALIAS_PREC)
1467+
// Check for alias
1468+
if p.currentIs(token.AS) {
1469+
p.nextToken()
1470+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1471+
alias := p.current.Value
1472+
p.nextToken()
1473+
lenArg = p.wrapWithAlias(lenArg, alias)
1474+
}
1475+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && p.peekIs(token.RPAREN) {
1476+
alias := p.current.Value
1477+
p.nextToken()
1478+
lenArg = p.wrapWithAlias(lenArg, alias)
1479+
}
1480+
args = append(args, lenArg)
12521481
} else if p.currentIs(token.COMMA) {
12531482
p.nextToken()
1254-
args = append(args, p.parseExpression(LOWEST))
1483+
// Parse third argument with possible alias
1484+
lenArg := p.parseExpression(ALIAS_PREC)
1485+
if p.currentIs(token.AS) {
1486+
p.nextToken()
1487+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1488+
alias := p.current.Value
1489+
p.nextToken()
1490+
lenArg = p.wrapWithAlias(lenArg, alias)
1491+
}
1492+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && p.peekIs(token.RPAREN) {
1493+
alias := p.current.Value
1494+
p.nextToken()
1495+
lenArg = p.wrapWithAlias(lenArg, alias)
1496+
}
1497+
args = append(args, lenArg)
12551498
}
12561499

12571500
p.expect(token.RPAREN)
@@ -1287,15 +1530,43 @@ func (p *Parser) parseTrim() ast.Expression {
12871530
}
12881531

12891532
// Parse characters to trim (if specified)
1533+
// Use ALIAS_PREC to not consume AS as alias
12901534
if !p.currentIs(token.FROM) && !p.currentIs(token.RPAREN) {
1291-
trimChars = p.parseExpression(LOWEST)
1535+
trimChars = p.parseExpression(ALIAS_PREC)
1536+
// Check for alias on trimChars
1537+
if p.currentIs(token.AS) {
1538+
p.nextToken()
1539+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1540+
alias := p.current.Value
1541+
p.nextToken()
1542+
trimChars = p.wrapWithAlias(trimChars, alias)
1543+
}
1544+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && p.peekIs(token.FROM) {
1545+
alias := p.current.Value
1546+
p.nextToken()
1547+
trimChars = p.wrapWithAlias(trimChars, alias)
1548+
}
12921549
}
12931550

12941551
// FROM clause
12951552
var expr ast.Expression
12961553
if p.currentIs(token.FROM) {
12971554
p.nextToken()
1298-
expr = p.parseExpression(LOWEST)
1555+
// Parse expression with possible alias
1556+
expr = p.parseExpression(ALIAS_PREC)
1557+
// Check for alias
1558+
if p.currentIs(token.AS) {
1559+
p.nextToken()
1560+
if p.currentIs(token.IDENT) || p.current.Token.IsKeyword() {
1561+
alias := p.current.Value
1562+
p.nextToken()
1563+
expr = p.wrapWithAlias(expr, alias)
1564+
}
1565+
} else if (p.currentIs(token.IDENT) || p.current.Token.IsKeyword()) && p.peekIs(token.RPAREN) {
1566+
alias := p.current.Value
1567+
p.nextToken()
1568+
expr = p.wrapWithAlias(expr, alias)
1569+
}
12991570
} else {
13001571
expr = trimChars
13011572
trimChars = nil
@@ -1310,6 +1581,8 @@ func (p *Parser) parseTrim() ast.Expression {
13101581
fnName = "trimLeft"
13111582
case "TRAILING":
13121583
fnName = "trimRight"
1584+
case "BOTH":
1585+
fnName = "trimBoth"
13131586
}
13141587

13151588
args := []ast.Expression{expr}

parser/testdata/00765_sql_compatibility_aliases/metadata.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"stmt10": true,
44
"stmt18": true,
55
"stmt2": true,
6-
"stmt23": true,
76
"stmt25": true,
87
"stmt26": true,
98
"stmt27": true,
Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt10": true,
4-
"stmt12": true,
5-
"stmt3": true
6-
}
7-
}
1+
{}

parser/testdata/02160_special_functions/metadata.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"explain_todo": {
3-
"stmt10": true,
43
"stmt11": true,
54
"stmt14": true,
65
"stmt16": true,

0 commit comments

Comments
 (0)