From 9fe66c0098c08b1cbf5a7f59323666e0ec28aee5 Mon Sep 17 00:00:00 2001 From: ybai08 <271969568+ybai08@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:41:13 -0500 Subject: [PATCH] fix: parse CASE with a signed switch value 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. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 28 ++++++++++++++- .../expression/CaseExpressionTest.java | 34 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f2bf3b59cb..55c1041f26 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1315,7 +1315,9 @@ public class CCJSqlParser extends AbstractJSqlParser { String img = t2.image; if (img.length() == 1) { char c = img.charAt(0); - if (c == '=' || c == '>' || c == '<' || c == '+' || c == '-' || + // CASE -x WHEN ... has a signed switch value, CASE - x subtracts from a column + if (c == '+' || c == '-') return isWhenAheadOfCaseOperand(t2); + if (c == '=' || c == '>' || c == '<' || c == '*' || c == ',' || c == '.' || c == ';') return false; } } @@ -1326,6 +1328,30 @@ public class CCJSqlParser extends AbstractJSqlParser { } } + /** + * Whether a WHEN of this CASE follows the given token, outside of brackets and nested CASE + * expressions, before anything that ends the expression. + */ + private boolean isWhenAheadOfCaseOperand(Token t) { + int depth = 0; + for (t = nextWithItemToken(t); !interrupted && t.kind != EOF; t = nextWithItemToken(t)) { + if (t.kind == OPENING_BRACKET || t.kind == K_CASE) { + depth++; + } else if (depth > 0) { + if (t.kind == CLOSING_BRACKET || t.kind == K_END) { + depth--; + } + } else if (t.kind == K_WHEN) { + return true; + } else if (t.kind == CLOSING_BRACKET || t.kind == K_END || t.kind == K_THEN + || t.kind == K_ELSE || t.kind == K_FROM || t.kind == K_WHERE + || ",".equals(t.image) || ";".equals(t.image)) { + return false; + } + } + return false; + } + /** * XML constructor functions take a bare leading keyword before their first argument, * e.g. XMLPARSE(CONTENT expr), XMLPARSE(DOCUMENT expr), XMLELEMENT(NAME ident, ...). diff --git a/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java index 6c0cbb50c9..5194dbe660 100644 --- a/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/CaseExpressionTest.java @@ -10,8 +10,14 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; /** * @author Mathieu Goeminne @@ -166,4 +172,32 @@ void testFormatClause() throws JSQLParserException { String sqlStr = "SELECT CAST('18-12-03' AS DATE FORMAT 'YY-MM-DD') AS string_to_date"; TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @ParameterizedTest + @ValueSource(strings = { + "CASE -1 WHEN -1 THEN 'negative' ELSE 'other' END", + "CASE -a WHEN 1 THEN 2 END", + "CASE +a WHEN 1 THEN 2 END", + "CASE -a + b WHEN 1 THEN 2 END", + "CASE -t.a WHEN 1 THEN 2 END", + "CASE -abs(a) WHEN 1 THEN 2 END", + "CASE -(a) WHEN 1 THEN 2 END"}) + void testSignedSwitchExpression(String expressionStr) throws JSQLParserException { + TestUtils.assertExpressionCanBeParsedAndDeparsed(expressionStr, true); + CaseExpression caseExpression = assertInstanceOf(CaseExpression.class, + CCJSqlParserUtil.parseExpression(expressionStr)); + assertEquals(expressionStr.substring(5, expressionStr.indexOf(" WHEN")), + caseExpression.getSwitchExpression().toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "case - 1", + "case + 1", + "case - CASE WHEN a THEN 1 END", + "case - (CASE a WHEN 1 THEN 2 END)"}) + void testCaseColumnFollowedBySign(String expressionStr) throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT " + expressionStr + " FROM t", true); + assertInstanceOf(BinaryExpression.class, CCJSqlParserUtil.parseExpression(expressionStr)); + } }