Skip to content

Commit 6751071

Browse files
committed
Support trailing commas in expression lists
ClickHouse allows trailing commas before clauses like FROM, WHERE, etc. For example: SELECT a, b, FROM table Add isClauseKeyword function to detect when a token is a clause keyword that should terminate an expression list, vs when it's being used as an identifier (which ClickHouse allows for many keywords). The detection is context-aware - keywords followed by (, [, or = are treated as expression continuations rather than clause terminators.
1 parent 2e1c16b commit 6751071

File tree

21 files changed

+47
-120
lines changed

21 files changed

+47
-120
lines changed

parser/expression.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ func (p *Parser) parseExpressionList() []ast.Expression {
9999

100100
for p.currentIs(token.COMMA) {
101101
p.nextToken()
102+
// Handle trailing commas by checking if next token is a clause keyword
103+
if p.isClauseKeyword() {
104+
break
105+
}
102106
expr := p.parseExpression(LOWEST)
103107
if expr != nil {
104108
// Handle implicit alias (identifier without AS)
@@ -110,6 +114,29 @@ func (p *Parser) parseExpressionList() []ast.Expression {
110114
return exprs
111115
}
112116

117+
// isClauseKeyword returns true if the current token is a SQL clause keyword
118+
// that should terminate an expression list (used for trailing comma support)
119+
func (p *Parser) isClauseKeyword() bool {
120+
switch p.current.Token {
121+
// Only these tokens are unambiguously clause terminators
122+
case token.RPAREN, token.SEMICOLON, token.EOF:
123+
return true
124+
// FROM is a clause keyword unless followed by ( or [ (function/index access)
125+
case token.FROM:
126+
return !p.peekIs(token.LPAREN) && !p.peekIs(token.LBRACKET)
127+
// These keywords can be used as identifiers in ClickHouse
128+
// Only treat as clause keywords if NOT followed by expression-like tokens
129+
case token.WHERE, token.GROUP, token.HAVING, token.ORDER, token.LIMIT:
130+
// If followed by comma, it's likely an identifier in a list
131+
return !p.peekIs(token.LPAREN) && !p.peekIs(token.LBRACKET) && !p.peekIs(token.COMMA) && !p.peekIs(token.RPAREN)
132+
case token.INTO, token.SETTINGS, token.FORMAT:
133+
return !p.peekIs(token.LPAREN) && !p.peekIs(token.LBRACKET) && !p.peekIs(token.EQ) && !p.peekIs(token.COMMA) && !p.peekIs(token.RPAREN)
134+
// UNION, EXCEPT, INTERSECT, OFFSET are often used as identifiers
135+
// Don't treat them as clause terminators to avoid breaking valid code
136+
}
137+
return false
138+
}
139+
113140
// parseGroupingSets parses GROUPING SETS ((a), (b), (a, b))
114141
func (p *Parser) parseGroupingSets() []ast.Expression {
115142
var exprs []ast.Expression
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 & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt136": true,
4-
"stmt141": true
5-
}
6-
}
1+
{}
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt28": true,
4-
"stmt42": true
5-
}
6-
}
1+
{}
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt9": 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-
"stmt3": true
4-
}
5-
}
1+
{}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"explain_todo": {
3-
"stmt24": true,
4-
"stmt30": true
3+
"stmt24": true
54
}
65
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
{
22
"explain_todo": {
3-
"stmt25": true,
4-
"stmt32": true
3+
"stmt25": true
54
}
65
}
Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt36": true,
4-
"stmt58": true
5-
}
6-
}
1+
{}
Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1 @@
1-
{
2-
"explain_todo": {
3-
"stmt11": true,
4-
"stmt12": true,
5-
"stmt13": true,
6-
"stmt4": true,
7-
"stmt5": true,
8-
"stmt6": true
9-
}
10-
}
1+
{}

0 commit comments

Comments
 (0)