From 4e411b1f6d167a297a275a5524972f54e9c9adc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Sat, 15 Aug 2026 21:03:26 +0800 Subject: [PATCH] fix: do not treat a named parameter ':' as the ternary else separator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 34 ++++++++++++++- .../expression/TernaryExpressionTest.java | 43 ++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2b71488f1..973004a6e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -227,11 +227,14 @@ public class CCJSqlParser extends AbstractJSqlParser { * {@code cond ? then : else} rather than the PostgreSQL JSON operator: * a standalone ":" closes the then-branch at the same nesting depth before * any expression boundary (",", ";", EOF, an unbalanced closing bracket or a - * clause keyword such as FROM/WHERE). + * clause keyword such as FROM/WHERE). A ":" in operand position (directly + * after an operator) starts a JDBC named parameter instead and cannot close + * the then-branch. */ protected boolean isTernaryAhead() { try { int depth = 0; + Token prev = null; for (int i = 2; ; i++) { Token t = getToken(i); if (t == null || t.kind == EOF) { @@ -246,7 +249,7 @@ public class CCJSqlParser extends AbstractJSqlParser { } depth--; } else if (depth == 0) { - if (":".equals(image)) { + if (":".equals(image) && canEndExpression(prev)) { return true; } if (",".equals(image) || ";".equals(image)) { @@ -262,12 +265,39 @@ public class CCJSqlParser extends AbstractJSqlParser { break; } } + prev = t; } } catch (Exception e) { return false; } } + /** + * True when the token can be the LAST token of an expression: an + * identifier, a literal, a JDBC parameter, or a balanced closing bracket. + * Only such a token may directly precede the ":" closing the ternary + * then-branch, since the then-branch must be a complete expression. A ":" + * in operand position instead (directly after an operator, e.g. `x = :name` + * or directly after the leading "?") starts a JDBC named parameter. + * DATA_TYPE tokens end expressions as well, because a cast's target type + * (`b :: int`) or a bare type keyword closes the then-branch. + */ + private boolean canEndExpression(Token t) { + if (t == null) { + return false; + } + switch (t.kind) { + case S_IDENTIFIER: case S_QUOTED_IDENTIFIER: case S_PARAMETER: + case S_LONG: case S_DOUBLE: case S_HEX: case S_CHAR_LITERAL: + case K_DATE_LITERAL: case K_DATETIMELITERAL: case DATA_TYPE: + case K_NULL: case K_TRUE: case K_FALSE: + return true; + default: + return (t.kind >= MIN_NON_RESERVED_WORD && t.kind <= MAX_NON_RESERVED_WORD) + || ")".equals(t.image) || "]".equals(t.image); + } + } + /** * Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch. diff --git a/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java index 3f3270176..03f535db3 100644 --- a/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/TernaryExpressionTest.java @@ -14,6 +14,7 @@ import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator; +import net.sf.jsqlparser.expression.operators.relational.JsonOperator; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; @@ -67,7 +68,11 @@ void testIssue2436() throws JSQLParserException { "SELECT a ? b : ? FROM t", "SELECT * FROM t WHERE x = ? AND y ? z : w", // case expression inside a branch - "SELECT a ? CASE WHEN b THEN c ELSE d END : e FROM t" + "SELECT a ? CASE WHEN b THEN c ELSE d END : e FROM t", + // cast target type ends the then-branch (a bare ":" after a type + // keyword is the ternary separator, not a named parameter) + "SELECT a ? b :: int : c FROM t", + "SELECT x > 0 ? CAST(b AS int) : c FROM t" }) void testTernaryInVariousContexts(String sqlStr) throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); @@ -129,4 +134,40 @@ void testRightAssociativeNesting() throws JSQLParserException { void testUnrelatedSyntaxUnaffected(String sqlStr) throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sqlStr, true); } + + @Test + void testJsonbOperatorWithNamedParameter() throws JSQLParserException { + // regression guard (ternary support, #2466): a ":" in operand position + // starts a JDBC named parameter and must not close a ternary then-branch + Select select = (Select) CCJSqlParserUtil.parse("SELECT * FROM t WHERE j ? :key"); + Expression where = ((PlainSelect) select).getWhere(); + Assertions.assertTrue(where instanceof JsonOperator); + Assertions.assertTrue( + ((JsonOperator) where).getRightExpression() instanceof JdbcNamedParameter); + } + + @Test + void testTernaryWithCastThenBranch() throws JSQLParserException { + // regression guard: a DATA_TYPE token can end the then-branch, so the + // ":" after the cast target type must close the ternary + Select select = (Select) CCJSqlParserUtil.parse("SELECT a ? b :: int : c FROM t"); + TernaryExpression ternary = (TernaryExpression) ((PlainSelect) select).getSelectItem(0) + .getExpression(); + Assertions.assertTrue(ternary.getThenExpression() instanceof CastExpression); + } + + @ParameterizedTest + @ValueSource(strings = { + // PostgreSQL jsonb operator combined with JDBC named parameters + "SELECT * FROM t WHERE j ? :key", + "SELECT * FROM t WHERE j ? 'k' AND x = :p", + "SELECT * FROM t WHERE j ? 'k' OR x = :p", + "SELECT * FROM t WHERE j ? :key AND x = 1", + // named parameters keep working inside ternary branches, too + "SELECT a ? :t : :e FROM t", + "SELECT * FROM t WHERE c = :c AND a ? :t : :e" + }) + void testTernaryAndJsonbWithJdbcNamedParameters(String sqlStr) throws JSQLParserException { + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } }