Skip to content

Commit 60b0875

Browse files
authored
fix: support ClickHouse C-style ternary operator (? :) in expressions (#2436) (#2466)
Parse cond ? then : else as a TernaryExpression at the boolean-operator level in prattExpressionRest, keeping C-style precedence (binds looser than AND/OR) and right-associativity. A pending ? is disambiguated between this ternary and the PostgreSQL JSON operator via isTernaryAhead, and inside a then-branch a top-level : closes the ternary instead of starting a JSON path, so both dialects keep working. Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 2989cb1 commit 60b0875

8 files changed

Lines changed: 332 additions & 3 deletions

File tree

src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,12 @@ default void visit(RangeExpression rangeExpression) {
737737
this.visit(rangeExpression, null);
738738
}
739739

740+
<S> T visit(TernaryExpression ternaryExpression, S context);
741+
742+
default void visit(TernaryExpression ternaryExpression) {
743+
this.visit(ternaryExpression, null);
744+
}
745+
740746
<S> T visit(TSQLLeftJoin tsqlLeftJoin, S context);
741747

742748
default void visit(TSQLLeftJoin tsqlLeftJoin) {

src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,12 @@ public <S> T visit(RangeExpression rangeExpression, S context) {
822822
rangeExpression.getEndExpression());
823823
}
824824

825+
@Override
826+
public <S> T visit(TernaryExpression ternaryExpression, S context) {
827+
return visitExpressions(ternaryExpression, context, ternaryExpression.getCondition(),
828+
ternaryExpression.getThenExpression(), ternaryExpression.getElseExpression());
829+
}
830+
825831
@Override
826832
public <S> T visit(TSQLLeftJoin tsqlLeftJoin, S context) {
827833
return visitBinaryExpression(tsqlLeftJoin, context);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2025 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.expression;
11+
12+
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
13+
14+
/**
15+
* The C-style ternary conditional operator {@code condition ? thenExpression : elseExpression},
16+
* supported for instance by ClickHouse as an alias for {@code if(condition, then, else)}.
17+
*/
18+
public class TernaryExpression extends ASTNodeAccessImpl implements Expression {
19+
private Expression condition;
20+
private Expression thenExpression;
21+
private Expression elseExpression;
22+
23+
public TernaryExpression() {}
24+
25+
public TernaryExpression(Expression condition, Expression thenExpression,
26+
Expression elseExpression) {
27+
this.condition = condition;
28+
this.thenExpression = thenExpression;
29+
this.elseExpression = elseExpression;
30+
}
31+
32+
public Expression getCondition() {
33+
return condition;
34+
}
35+
36+
public TernaryExpression setCondition(Expression condition) {
37+
this.condition = condition;
38+
return this;
39+
}
40+
41+
public Expression getThenExpression() {
42+
return thenExpression;
43+
}
44+
45+
public TernaryExpression setThenExpression(Expression thenExpression) {
46+
this.thenExpression = thenExpression;
47+
return this;
48+
}
49+
50+
public Expression getElseExpression() {
51+
return elseExpression;
52+
}
53+
54+
public TernaryExpression setElseExpression(Expression elseExpression) {
55+
this.elseExpression = elseExpression;
56+
return this;
57+
}
58+
59+
@Override
60+
public String toString() {
61+
return condition + " ? " + thenExpression + " : " + elseExpression;
62+
}
63+
64+
@Override
65+
public <T, S> T accept(ExpressionVisitor<T> expressionVisitor, S context) {
66+
return expressionVisitor.visit(this, context);
67+
}
68+
}

src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,14 @@ public <S> Void visit(RangeExpression rangeExpression, S context) {
249249
return null;
250250
}
251251

252+
@Override
253+
public <S> Void visit(TernaryExpression ternaryExpression, S context) {
254+
ternaryExpression.getCondition().accept(this, context);
255+
ternaryExpression.getThenExpression().accept(this, context);
256+
ternaryExpression.getElseExpression().accept(this, context);
257+
return null;
258+
}
259+
252260
/**
253261
* Main entry for this Tool class. A list of found tables is returned.
254262
*/

src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import net.sf.jsqlparser.expression.OverlapsCondition;
6363
import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter;
6464
import net.sf.jsqlparser.expression.RangeExpression;
65+
import net.sf.jsqlparser.expression.TernaryExpression;
6566
import net.sf.jsqlparser.expression.RowConstructor;
6667
import net.sf.jsqlparser.expression.RowGetExpression;
6768
import net.sf.jsqlparser.expression.SignedExpression;
@@ -818,6 +819,16 @@ public <S> StringBuilder visit(RangeExpression rangeExpression, S context) {
818819
return builder;
819820
}
820821

822+
@Override
823+
public <S> StringBuilder visit(TernaryExpression ternaryExpression, S context) {
824+
ternaryExpression.getCondition().accept(this, context);
825+
builder.append(" ? ");
826+
ternaryExpression.getThenExpression().accept(this, context);
827+
builder.append(" : ");
828+
ternaryExpression.getElseExpression().accept(this, context);
829+
return builder;
830+
}
831+
821832
@Override
822833
public <S> StringBuilder visit(Column tableColumn, S context) {
823834
final Table table = tableColumn.getTable();
@@ -1045,6 +1056,10 @@ public void visit(RangeExpression rangeExpression) {
10451056
visit(rangeExpression, null);
10461057
}
10471058

1059+
public void visit(TernaryExpression ternaryExpression) {
1060+
visit(ternaryExpression, null);
1061+
}
1062+
10481063
public void visit(Column tableColumn) {
10491064
visit(tableColumn, null);
10501065
}

src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import net.sf.jsqlparser.expression.OverlapsCondition;
5757
import net.sf.jsqlparser.expression.PostgresNamedFunctionParameter;
5858
import net.sf.jsqlparser.expression.RangeExpression;
59+
import net.sf.jsqlparser.expression.TernaryExpression;
5960
import net.sf.jsqlparser.expression.RowConstructor;
6061
import net.sf.jsqlparser.expression.RowGetExpression;
6162
import net.sf.jsqlparser.expression.SignedExpression;
@@ -1153,6 +1154,14 @@ public <S> Void visit(RangeExpression rangeExpression, S context) {
11531154
return null;
11541155
}
11551156

1157+
@Override
1158+
public <S> Void visit(TernaryExpression ternaryExpression, S context) {
1159+
ternaryExpression.getCondition().accept(this, context);
1160+
ternaryExpression.getThenExpression().accept(this, context);
1161+
ternaryExpression.getElseExpression().accept(this, context);
1162+
return null;
1163+
}
1164+
11561165
@Override
11571166
public <S> Void visit(TSQLLeftJoin tsqlLeftJoin, S context) {
11581167
tsqlLeftJoin.getLeftExpression().accept(this, context);
@@ -1310,6 +1319,10 @@ public void visit(RangeExpression rangeExpression) {
13101319
visit(rangeExpression, null);
13111320
}
13121321

1322+
public void visit(TernaryExpression ternaryExpression) {
1323+
visit(ternaryExpression, null);
1324+
}
1325+
13131326
public void visit(TSQLLeftJoin tsqlLeftJoin) {
13141327
visit(tsqlLeftJoin, null);
13151328
}

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
101101
return this;
102102
}
103103

104+
// depth of open ClickHouse-style ternary then-branches: a ":" directly after the
105+
// then-branch closes the ternary and must not be taken as the JSON path operator
106+
private int ternaryThenBranchDepth = 0;
107+
104108
private void linkAST(ASTNodeAccess access, Node node) {
105109
access.setASTNode(node);
106110
node.jjtSetValue(access);
@@ -182,6 +186,11 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
182186
// comparison operator follows after "(" "+" ")"
183187
return isComparisonOperator(getToken(4));
184188
}
189+
if ("?".equals(token.image) && isTernaryAhead()) {
190+
// ClickHouse ternary `cond ? then : else` — the `?` must not be taken
191+
// as the PostgreSQL JSON operator here, it is consumed by prattExpressionRest
192+
return false;
193+
}
185194
return isComparisonOperator(token);
186195
} catch (Exception e) {
187196
return false;
@@ -212,6 +221,52 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
212221
}
213222
}
214223

224+
/**
225+
* True when the pending "?" starts a ClickHouse-style ternary conditional
226+
* {@code cond ? then : else} rather than the PostgreSQL JSON operator:
227+
* a standalone ":" closes the then-branch at the same nesting depth before
228+
* any expression boundary (",", ";", EOF, an unbalanced closing bracket or a
229+
* clause keyword such as FROM/WHERE).
230+
*/
231+
protected boolean isTernaryAhead() {
232+
try {
233+
int depth = 0;
234+
for (int i = 2; ; i++) {
235+
Token t = getToken(i);
236+
if (t == null || t.kind == EOF) {
237+
return false;
238+
}
239+
String image = t.image;
240+
if ("(".equals(image) || "[".equals(image)) {
241+
depth++;
242+
} else if (")".equals(image) || "]".equals(image)) {
243+
if (depth == 0) {
244+
return false;
245+
}
246+
depth--;
247+
} else if (depth == 0) {
248+
if (":".equals(image)) {
249+
return true;
250+
}
251+
if (",".equals(image) || ";".equals(image)) {
252+
return false;
253+
}
254+
switch (t.kind) {
255+
case K_SELECT: case K_FROM: case K_WHERE: case K_GROUP:
256+
case K_HAVING: case K_ORDER: case K_LIMIT: case K_UNION:
257+
case K_INTERSECT: case K_EXCEPT: case K_MINUS: case K_INTO:
258+
case K_VALUES: case K_FETCH: case K_OFFSET:
259+
return false;
260+
default:
261+
break;
262+
}
263+
}
264+
}
265+
} catch (Exception e) {
266+
return false;
267+
}
268+
}
269+
215270

216271
/**
217272
* Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch.
@@ -328,12 +383,35 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
328383
*/
329384
protected Expression prattExpressionRest(Expression left, int minPrec) throws ParseException {
330385
while (!interrupted) {
331-
int op = getToken(1).kind;
386+
Token t = getToken(1);
387+
int op = t.kind;
388+
boolean ternary = "?".equals(t.image) && isTernaryAhead();
332389
int prec;
333390
if (op == K_AND || op == OP_DOUBLEAND) prec = 4;
334391
else if (op == K_XOR || op == K_OR) prec = 2;
392+
else if (ternary) prec = 2;
335393
else break;
336394
if (prec < minPrec) break;
395+
if (ternary) {
396+
// ClickHouse-style ternary conditional: cond ? then : else
397+
// Right-associative: both branches parse at the full expression
398+
// level (prec 2), nested ternaries are absorbed by the else-branch.
399+
jj_consume_token(op, t.image);
400+
Expression thenExpression;
401+
ternaryThenBranchDepth++;
402+
try {
403+
thenExpression = prattExpressionRest(Condition(), 2);
404+
} finally {
405+
ternaryThenBranchDepth--;
406+
}
407+
if (!":".equals(getToken(1).image)) {
408+
throw new ParseException("Expected ':' closing the ternary conditional operator");
409+
}
410+
jj_consume_token(getToken(1).kind, getToken(1).image);
411+
Expression elseExpression = prattExpressionRest(Condition(), 2);
412+
left = new TernaryExpression(left, thenExpression, elseExpression);
413+
continue;
414+
}
337415
jj_consume_token(op, getToken(1).image);
338416
// +1 makes OR/AND/XOR left-associative
339417
Expression right = prattExpressionRest(Condition(), prec + 1);
@@ -920,6 +998,9 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
920998
if (t.kind == OPENING_BRACKET && getToken(2).image.equals("+")) {
921999
return isComparisonOperator(getToken(4));
9221000
}
1001+
// ClickHouse ternary `cond ? then : else` — the `?` must not be taken as the
1002+
// PostgreSQL JSON operator, it is consumed by prattExpressionRest instead
1003+
if ("?".equals(t.image) && isTernaryAhead()) return false;
9231004
if (isComparisonOperator(t)) return true;
9241005
switch (t.kind) {
9251006
// Each suffix's start token:
@@ -7980,7 +8061,7 @@ Expression PrimaryExpression() #PrimaryExpression:
79808061

79818062
// Check for JSON operands
79828063
[
7983-
LOOKAHEAD(2) (
8064+
LOOKAHEAD(2, { ternaryThenBranchDepth == 0 || !":".equals(getToken(1).image) }) (
79848065
LOOKAHEAD(2) (
79858066
token="->"
79868067
|
@@ -8310,7 +8391,7 @@ JsonExpression JsonExpression(Expression expr, List<Map.Entry<Expression, String
83108391
}
83118392

83128393
(
8313-
LOOKAHEAD(2) (
8394+
LOOKAHEAD(2, { ternaryThenBranchDepth == 0 || !":".equals(getToken(1).image) }) (
83148395
token="->"
83158396
|
83168397
token=":"

0 commit comments

Comments
 (0)