Skip to content

fix: do not treat a named parameter ':' as the ternary else separator (#2475) - #2476

Merged
manticore-projects merged 1 commit into
JSQLParser:masterfrom
fudianchn:fix/ternary-named-parameter
Aug 16, 2026
Merged

fix: do not treat a named parameter ':' as the ternary else separator (#2475)#2476
manticore-projects merged 1 commit into
JSQLParser:masterfrom
fudianchn:fix/ternary-named-parameter

Conversation

@fudianchn

Copy link
Copy Markdown
Contributor

What

The PostgreSQL jsonb operator ? keeps working when the same condition also contains JDBC named parameters:

SELECT * FROM t WHERE j ? :key
SELECT * FROM t WHERE j ? 'k' AND x = :p

Why / Root cause

isTernaryAhead() (introduced with the ClickHouse ternary in #2466) committed to the ternary reading as soon as its token scan found ANY standalone : at bracket depth 0 before a clause boundary. A JDBC named parameter starts with the very same : token, so the statements above were misread as a ternary and then failed with Expected ':' closing the ternary conditional operator (both parsed before #2466: regression).

How

The scan now requires the : to be preceded by a token that can legally END an expression (canEndExpression()): identifier, literal, type keyword (a cast target, e.g. b :: int), JDBC parameter, non-reserved keyword, or a balanced closing bracket. The then-branch of a ternary must be a complete expression, so its closing : can never follow an operator directly. A : in operand position (after =, AND, the leading ?, ...) starts a named parameter instead: the scan skips it and keeps looking for the real separator.

Consequences:

  • j ? :key / j ? 'k' AND x = :p parse again as jsonb operator + named parameter (same JsonOperator AST as before fix: support ClickHouse C-style ternary operator (? :) (#2436) #2466).
  • Real ternaries are unaffected: all pre-existing TernaryExpressionTest cases stay green (a ? b : c, a ? ? : c with a positional parameter as then-branch, parentheses, a ? b OR c : d, CASE ... END : e, right-associative nesting).
  • Ternaries whose then-branch ends in a cast keep working: a ? b :: int : c still parses as a TernaryExpression with a CastExpression then-branch (the type keyword ends the expression, so the : after it closes the ternary).
  • Named parameters keep working inside ternary branches: a ? :t : :e now has a defined reading (named parameter then- and else-branches).

Scope

Only the ternary-vs-JSON disambiguation predicate changes; the ternary grammar productions, the TernaryExpression AST, and the JSON operator handling are untouched.

Testing

TernaryExpressionTest.testTernaryAndJsonbWithJdbcNamedParameters (6 round-trip cases) and testJsonbOperatorWithNamedParameter (AST shape) reproduce the issue: 5 of these executions fail on master with JSQLParserException. testTernaryWithCastThenBranch guards the cast-then-branch reading. All 42 tests of TernaryExpressionTest pass with this change.

Performance

gradle jmh, JSQLParserBenchmark.parseSQLStatements on performance.sql, version=latest, 10 forks x 10 iterations (100 samples per run, two interleaved runs per build) on a 32-core host:

build ms/op
master (7b64825) 3.717 ± 0.023 / 3.729 ± 0.022
this PR (4e411b1) 3.726 ± 0.021 / 3.715 ± 0.020

The two builds interleave across the repeated runs (pair 1: +0.24%, pair 2: -0.37%); every delta lies within the 99.9% confidence intervals: no regression.

Verification of the original issue

Both statements from the issue fail on master and parse + round-trip with this change.

Fixes #2475

@manticore-projects
manticore-projects merged commit 5080d19 into JSQLParser:master Aug 16, 2026
7 checks passed
@fudianchn

Copy link
Copy Markdown
Contributor Author

While regression-testing the current snapshot build I found one edge case where this change silently altered behavior.

A ternary whose then-branch ends with a plain positional parameter no longer parses as a TernaryExpression; it silently becomes a JsonOperator (verified by comparing 7b64825 against current master):

statement 7b64825 current master
SELECT a ? ? : c FROM t TernaryExpression JsonOperator(a, JsonExpression(?:c))
SELECT a ? b + ? : c FROM t TernaryExpression JsonOperator
SELECT a ? ? : ? FROM t TernaryExpression JsonOperator
SELECT a ? $1 : c FROM t TernaryExpression TernaryExpression (unaffected)

Cause: canEndExpression() whitelists S_PARAMETER ($N style) but not the bare ?. That token is anonymous (no named token constant, outside the non-reserved range), so the scan skips the : closing the then-branch and the parser commits to the jsonb reading. In the wrong tree the ? ends up inside JsonExpression's idents, so it is no longer a JdbcParameter node either (relevant for parameter extraction and SQL rewriting).

The round-trip case SELECT a ? ? : c FROM t in TernaryExpressionTest still passes, because the wrong AST deparses to a ? ?:c which re-parses to the same wrong AST; only an AST-shape assertion would catch it.

The fix is one line: add || "?".equals(t.image) to the default branch of canEndExpression() (same style as the existing ")" / "]" image checks). Verified locally: the three statements above parse as TernaryExpression again with JdbcParameter as the then-branch (same AST as 7b64825), both #2475 statements still parse as jsonb + named parameter, and all 42 TernaryExpressionTest tests stay green. The : in the #2475 cases follows null or an operator, so the new check is not reached there.

I can open a follow-up PR with the one-liner plus an AST-shape regression test if that works.

@manticore-projects

Copy link
Copy Markdown
Contributor

I can open a follow-up PR with the one-liner plus an AST-shape regression test if that works.

Sure, and lesson learned: we should not chase too many too exotic features. There is a good reason for SQL Standard providing CASE expressions and RDBMS implementing IIF() function instead of that operator.
We have added now lots of complexity for rather little benefit.

@fudianchn

Copy link
Copy Markdown
Contributor Author

Understood. Going forward I will focus on standard SQL alignment.

Follow-up PR: #2477.

manticore-projects pushed a commit that referenced this pull request Aug 16, 2026
…hen-branch (#2477)

A bare '?' (anonymous token, no kind constant) was missing from
canEndExpression(), so a ternary whose then-branch ends with a positional
parameter was silently misread as a jsonb operator: SELECT a ? ? : c FROM t
parsed as JsonOperator(a, JsonExpression(?:c)) instead of
TernaryExpression(a, JdbcParameter, c). Recognize it by image, like the
closing brackets, and pin the AST shape with a regression test.

Follow-up to #2476.

Signed-off-by: 付典 <fudianchn@gmail.com>
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.

[BUG] JSQLParser 5.4-SNAPSHOT : PostgreSQL : jsonb operator "?" fails together with JDBC named parameters (regression via #2466)

2 participants