Skip to content

fix: parse CASE with a signed switch value - #2869

Open
ybai08 wants to merge 1 commit into
JSQLParser:masterfrom
ybai08:fix/case-signed-switch
Open

ybai08 wants to merge 1 commit into
JSQLParser:masterfrom
ybai08:fix/case-signed-switch

Conversation

@ybai08

@ybai08 ybai08 commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

A simple CASE whose switch value starts with - or + no longer parses:

Input master
SELECT CASE -1 WHEN -1 THEN 'x' END FROM t ParseException
SELECT CASE -a WHEN 1 THEN 2 END FROM t ParseException
CCJSqlParserUtil.parseExpression("CASE -a WHEN 1 THEN 2 END") returns case - a (a Subtraction on a column named case); the rest is dropped

CASE ~1 WHEN …, CASE (-1) WHEN … and CASE 0 - 1 WHEN … parse. SQL allows any value expression as the CASE operand, and SQLite evaluates these statements.

The cause is isCaseExpressionAhead, added in the Pratt rewrite (365078d) in place of LOOKAHEAD(3) on CaseWhenExpression(). It treats CASE followed by + or - as a column named case, so that SELECT case - 1 FROM t parses.

With this change, when + or - follows CASE, a new helper isWhenAheadOfCaseOperand walks the token chain (nextWithItemToken, like the other lookahead helpers). It answers "CASE expression" if a WHEN comes before the expression ends, skipping brackets and nested CASE … END. It answers "column" at ), END, THEN, ELSE, FROM, WHERE, ,, ; or EOF at the same level. So case - 1, case + 1, case - CASE WHEN a THEN 1 END and case - (CASE a WHEN 1 THEN 2 END) still parse as a binary expression on a column named case. Only CASE followed by a sign goes through the walk, and it stops at the first token that ends the expression.

Tests (CaseExpressionTest):

  • testSignedSwitchExpression: 7 expressions (-1, -a, +a, -a + b, -t.a, -abs(a), -(a) as the switch value). Each must round-trip and parse to a CaseExpression with that switch expression. All 7 fail on master c84ad0eb (expected: <case-1 when-1 then 'negative' else 'other' end> but was: <case-1>, etc.).
  • testCaseColumnFollowedBySign: the 4 column forms above, which must still give a BinaryExpression. They pass on master and with the change.

What was run (Gradle with a JDK 17 toolchain):

  • ./gradlew test --tests CaseExpressionTest: 28 tests, 7 failed without the grammar change, 0 failed with it.
  • ./gradlew check: BUILD SUCCESSFUL. 12,273 tests, 0 failures, 25 skipped. Spotless, Checkstyle, PMD, SpotBugs and JaCoCo also pass.
  • A differential check against SQLite (Python sqlite3). 20,000 random SELECT <expr> FROM t statements using arithmetic, bitwise, comparison, LIKE/GLOB, BETWEEN, IN, IS [NOT] NULL, CASE, CAST and a few functions were parsed and deparsed with JSqlParser. SQLite evaluated both the original and the deparsed SQL. On master, 0 results differ, and 43 statements that SQLite accepts fail to parse because of a signed CASE switch value. With this change, those 43 parse and their deparsed SQL gives the same result in SQLite. No statement that parsed on master fails now, and 0 results differ.

Not run: gradle jmh.

isCaseExpressionAhead reads CASE followed by + or - as a column named
case, so CASE -1 WHEN -1 THEN 'x' END failed to parse in a statement,
and parseExpression returned only the subtraction case - 1 and dropped
the rest. The LOOKAHEAD(3) it replaced in the Pratt rewrite took these
as CASE expressions.

After CASE + or CASE -, look for a WHEN of this CASE, outside of
brackets and nested CASE expressions, before anything that ends the
expression. A column named case followed by a sign, as in case - 1 or
case - CASE WHEN a THEN 1 END, still parses as before.

This branch has not been deployed

No deployments
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.

1 participant