@@ -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