Skip to content

fix: support ClickHouse C-style ternary operator (? :) (#2436) - #2466

Merged
manticore-projects merged 1 commit into
JSQLParser:masterfrom
fudianchn:fix/clickhouse-ternary-operator-2436
Aug 14, 2026
Merged

fix: support ClickHouse C-style ternary operator (? :) (#2436)#2466
manticore-projects merged 1 commit into
JSQLParser:masterfrom
fudianchn:fix/clickhouse-ternary-operator-2436

Conversation

@fudianchn

Copy link
Copy Markdown
Contributor

What

Support the ClickHouse C-style ternary conditional operator cond ? then : else (an alias for if(cond, then, else)), fixing #2436:

SELECT x > 0 ? 'y' : 'n' FROM t
SELECT * FROM t WHERE a OR b ? c : d
SELECT abs(x > 0 ? 1 : 2) FROM t

JSQLParser currently rejects these with ParseException: Encountered: "?".

Why / Root cause

? is already double-booked in the grammar: as a JDBC parameter atom (JdbcParameter) and as the PostgreSQL JSON existence operator (RegularConditionRHS consumes expr ? 'key'). The ternary needs the third reading: ? after a complete boolean expression, closed by a :. Neither existing path can express that, so x > 0 ? 'y' : 'n' fails at the ?.

How

The ternary is integrated into the existing Pratt boolean loop prattExpressionRest, at the same precedence level as OR (C semantics: binds looser than AND/OR, right-associative):

  • a pending ? is routed to the ternary via a new isTernaryAhead() scan (same pattern as isUnparenthesizedSelectAhead): it is a ternary when a standalone : closes the then-branch at the same nesting depth before any expression boundary (,, ;, ), EOF, clause keyword). Otherwise the ? keeps its current meaning as the JSON operator — isConditionSuffixAhead()/isComparisonOperatorAhead() decline it only when it is a ternary.
  • the : separator collided with the JSON path operator (b : path in PrimaryExpression/JsonExpression); inside a then-branch a top-level : now closes the ternary instead (guarded by a ternaryThenBranchDepth counter, LOOKAHEAD(2, { ... })).

AST: new TernaryExpression(condition, thenExpression, elseExpression) wired into ExpressionVisitor, ExpressionVisitorAdapter, TablesNamesFinder, ExpressionDeParser and ExpressionValidator, following the pattern of RangeExpression.

Scope

Works in every expression context (select items, WHERE, GROUP BY/HAVING, ORDER BY, function arguments, join ON, UPDATE SET, INSERT VALUES, subqueries, nested ternaries in both branches, JDBC parameters as branches). The PostgreSQL JSON operators (?, ?|, ?&, ->, : paths) and JDBC parameters (?, ?5, LIMIT ?) keep parsing unchanged; a JSON ? followed later by a top-level : at depth 0 is inherently ambiguous across dialects and stays a JSON operator only when no : closes it first.

Testing

TernaryExpressionTest (32 tests): the issue case with AST shape assertions, 16 context round-trips (parse + deparse), precedence/associativity AST assertions (a OR b ? c : d groups as (a OR b) ? c : d, a ? b : c ? d : e nests right), and 11 unaffected-syntax guards for JSON operators, JSON paths, JDBC parameters and array ranges.

Local: spotlessApply, checkstyleMain/Test, spotbugsMain, pmdMain clean; full test suite green (4737 tests, 0 failures).

Performance

gradle jmh, JSQLParserBenchmark.parseSQLStatements on performance.sql, version=latest, 10 forks × 10 iterations (100 samples) on a 32-core host:

build ms/op
master 3.714 ± 0.021
this change 3.730 ± 0.023

The +0.43% delta lies within the 99.9% confidence intervals (the CIs overlap from 3.707 to 3.735): no regression. When no ? is present the only added cost is one string comparison per prattExpressionRest iteration.

Verification of the original issue

SELECT x > 0 ? 'y' : 'n' FROM t now parses and round-trips:

SELECT x > 0 ? 'y' : 'n' FROM t
condition      : x > 0 (GreaterThan)
thenExpression : 'y'
elseExpression : 'n'

…JSQLParser#2436)

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>
@fudianchn
fudianchn force-pushed the fix/clickhouse-ternary-operator-2436 branch from 10b8465 to 9d0e057 Compare August 14, 2026 12:38
@manticore-projects

Copy link
Copy Markdown
Contributor

Excellent work! Thank your for your contribution!

@manticore-projects
manticore-projects merged commit 60b0875 into JSQLParser:master Aug 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants