diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 5e6d9c46e..594baffb1 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -590,17 +590,27 @@ public class CCJSqlParser extends AbstractJSqlParser { return left; } + private boolean isPostgreSqlJsonAccess(Token token) { + return ("->".equals(token.image) || "->>".equals(token.image) + || "#>".equals(token.image) || "#>>".equals(token.image)) + && Dialect.POSTGRESQL.name().equals(getAsString(Feature.dialect)); + } + protected Expression prattArithRest(Expression left, int minPrec) throws ParseException { while (!interrupted) { Token t = getToken(1); int op = t.kind; int prec; + boolean jsonAccess = isPostgreSqlJsonAccess(t); // Named tokens: OP_SLASH(/), OP_CARET(^), K_DIV, OP_CONCAT(||), // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>), // S_HASH_OPERATOR(#) // String-literal tokens: *, +, -, %, & (unnamed in JavaCC grammar) - if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6; + if (jsonAccess || op == OP_JSON_PATH_DELETE || op == OP_JSON_PATH_EXISTS + || (op == OP_CONCAT && Dialect.POSTGRESQL.name() + .equals(getAsString(Feature.dialect)))) prec = 4; + else if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6; else if (op == S_HASH_OPERATOR) prec = 5; else if (op == OP_CONCAT || op == OP_PIPE || op == OP_LSHIFT || op == OP_RSHIFT) prec = 5; @@ -626,7 +636,16 @@ public class CCJSqlParser extends AbstractJSqlParser { jj_consume_token(op, getToken(1).image); Expression right = prattArithRest(PrimaryExpression(), prec + 1); - if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + if (jsonAccess) { + JsonExpression access = left instanceof JsonExpression + ? (JsonExpression) left : new JsonExpression(left); + access.addIdent(right, t.image); + left = access; + } + else if (op == OP_JSON_PATH_DELETE || op == OP_JSON_PATH_EXISTS) { + left = new JsonOperator(t.image).withLeftExpression(left).withRightExpression(right); + } + else if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == S_HASH_OPERATOR) { Intersects r = new Intersects(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == OP_CARET) { net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } else if (op == K_DIV) { IntegerDivision r = new IntegerDivision(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } @@ -2732,6 +2751,8 @@ TOKEN : /* Operators */ | | | >"> +| +| | | "> | @@ -2881,13 +2902,16 @@ TOKEN: truncateIdentifierToken(matchedToken, hashIndex); } } - // PostgreSQL does not allow # in unquoted identifiers. Re-lex it as an - // operator, including #> and #>>, even when it touches the left operand. + // PostgreSQL does not allow # or @ in unquoted identifiers. Re-lex them as + // operators, including #>, #>>, #- and @?, when touching the left operand. if (matchedToken.kind == S_IDENTIFIER && AbstractJSqlParser.Dialect.POSTGRESQL.name().equals(configuration.getValue(Feature.dialect))) { int hashIndex = matchedToken.image.indexOf('#'); - if (hashIndex > 0) { - truncateIdentifierToken(matchedToken, hashIndex); + int atIndex = matchedToken.image.indexOf('@'); + int operatorIndex = hashIndex < 0 ? atIndex + : atIndex < 0 ? hashIndex : Math.min(hashIndex, atIndex); + if (operatorIndex > 0) { + truncateIdentifierToken(matchedToken, operatorIndex); } else if (hashIndex == 0) { truncateIdentifierToken(matchedToken, 1); matchedToken.kind = S_HASH_OPERATOR; @@ -10630,7 +10654,8 @@ Expression SimpleExpression(): { [ LOOKAHEAD( 5, { getToken(1).kind == S_AT_IDENTIFIER } ) user = UserVariable() ( operation = "=" | operation = ":=" ) ] retval=PrimaryExpression() - { retval = prattArithRest(retval, 5); } + // PostgreSQL path operators bind below arithmetic and above comparisons. + { retval = prattArithRest(retval, 4); } { if (user != null) { VariableAssignment assignment = new VariableAssignment(); @@ -10986,8 +11011,9 @@ Expression PrimaryExpression() #PrimaryExpression: // Check for JSON operands [ - LOOKAHEAD(2, { (ternaryThenBranchDepth == 0 && mapKeyDepth == 0) - || !":".equals(getToken(1).image) }) ( + LOOKAHEAD(2, { !isPostgreSqlJsonAccess(getToken(1)) + && ((ternaryThenBranchDepth == 0 && mapKeyDepth == 0) + || !":".equals(getToken(1).image)) }) ( LOOKAHEAD(2) ( token="->" | @@ -11354,8 +11380,9 @@ JsonExpression JsonExpression(Expression expr, List p.withDialect(Dialect.POSTGRESQL)); + } + + @ParameterizedTest + @ValueSource(strings = { + "SELECT '{\"a\":1}'::jsonb #- '{a}'", + "SELECT doc#-'{a}' FROM events", + "SELECT doc/*comment*/#-ARRAY['a', '0'] FROM events", + "SELECT doc@?'$.a[*] ? (@ > 2)' FROM events", + "SELECT NULL::jsonb @? '$.a'", + "SELECT doc #- '{a}' #- '{b}' FROM events" + }) + void buildsJsonOperatorsAndPreservesOutput(String sql) throws Exception { + PlainSelect select = parse(sql); + JsonOperator operator = + assertInstanceOf(JsonOperator.class, select.getSelectItem(0).getExpression()); + assertEquals(sql.contains("@?") ? "@?" : "#-", operator.getStringExpression()); + StringBuilder output = new StringBuilder(); + select.accept(new StatementDeParser(output), null); + assertEquals(select.toString(), output.toString()); + assertEquals(select.toString(), parse(output.toString()).toString()); + assertFalse(output.toString().contains("# -")); + } + + @Test + void preservesAssociativityAndComparisonPrecedence() throws Exception { + PlainSelect select = parse("SELECT doc #- '{a}' #- '{b}' = '{}'::jsonb FROM events"); + EqualsTo comparison = + assertInstanceOf(EqualsTo.class, select.getSelectItem(0).getExpression()); + JsonOperator deletion = + assertInstanceOf(JsonOperator.class, comparison.getLeftExpression()); + assertEquals("#-", deletion.getStringExpression()); + assertInstanceOf(JsonOperator.class, deletion.getLeftExpression()); + IsBooleanExpression predicate = assertInstanceOf(IsBooleanExpression.class, + parse("SELECT doc #- '{a}' @? '$.b' IS TRUE FROM events") + .getSelectItem(0).getExpression()); + JsonOperator exists = assertInstanceOf(JsonOperator.class, predicate.getLeftExpression()); + assertEquals("@?", exists.getStringExpression()); + assertInstanceOf(JsonOperator.class, exists.getLeftExpression()); + } + + @Test + void sharesLeftAssociativityWithPostgresqlConcatenation() throws Exception { + Concat concat = assertInstanceOf(Concat.class, + parse("SELECT doc #- '{a}' || other FROM events").getSelectItem(0).getExpression()); + assertInstanceOf(JsonOperator.class, concat.getLeftExpression()); + JsonOperator deletion = assertInstanceOf(JsonOperator.class, + parse("SELECT doc || other #- '{a}' FROM events").getSelectItem(0).getExpression()); + assertInstanceOf(Concat.class, deletion.getLeftExpression()); + } + + @Test + void composesAccessAndPathOperatorsInBothParserModes() throws Exception { + for (boolean complex : List.of(false, true)) { + for (String operator : List.of("->", "#>")) { + CCJSqlParser parser = CCJSqlParserUtil.newParser( + "SELECT doc " + operator + " '{a}' #- '{b}' FROM events") + .withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex); + JsonOperator deletion = assertInstanceOf(JsonOperator.class, + ((PlainSelect) parser.Statement()).getSelectItem(0).getExpression()); + JsonExpression access = + assertInstanceOf(JsonExpression.class, deletion.getLeftExpression()); + assertInstanceOf(StringValue.class, access.getIdentList().get(0).getKey()); + } + CCJSqlParser parser = CCJSqlParserUtil.newParser( + "SELECT doc #- '{a}' ->> 'b' = 'x' FROM events") + .withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex); + EqualsTo comparison = assertInstanceOf(EqualsTo.class, + ((PlainSelect) parser.Statement()).getSelectItem(0).getExpression()); + JsonExpression access = + assertInstanceOf(JsonExpression.class, comparison.getLeftExpression()); + assertInstanceOf(JsonOperator.class, access.getExpression()); + assertInstanceOf(StringValue.class, access.getIdentList().get(0).getKey()); + } + } + + @Test + void distinguishesPathOperatorFromParametersAndVisitsOperands() throws Exception { + PlainSelect select = parse("SELECT doc @? ? FROM events WHERE doc #- ? = ?::jsonb"); + JsonOperator exists = (JsonOperator) select.getSelectItem(0).getExpression(); + assertEquals(1, ((JdbcParameter) exists.getRightExpression()).getIndex()); + EqualsTo comparison = (EqualsTo) select.getWhere(); + JsonOperator deletion = (JsonOperator) comparison.getLeftExpression(); + assertEquals(2, ((JdbcParameter) deletion.getRightExpression()).getIndex()); + + List visited = new ArrayList<>(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + visited.add(value.getValue()); + return getBuilder().append("'changed'"); + } + }; + StringBuilder output = new StringBuilder(); + parse("SELECT doc #- '{a}' @? '$.b' FROM events") + .accept(new StatementDeParser(expressions, new SelectDeParser(), output), null); + assertEquals(List.of("{a}", "$.b"), visited); + assertEquals("SELECT doc #- 'changed' @? 'changed' FROM events", output.toString()); + } + + @Test + void retainsExistingHashAndJsonOperators() throws Exception { + for (String sql : List.of("SELECT 5 # -1", "SELECT doc #> '{a}' FROM events", + "SELECT doc #>> '{a}' FROM events", "SELECT doc ? 'a' FROM events", + "SELECT doc @@ '$.a > 2' FROM events")) { + assertEquals(parse(sql).toString(), parse(parse(sql).toString()).toString()); + } + Expression expression = parse("SELECT doc #- '{a}' FROM events") + .getSelectItem(0).getExpression(); + ((JsonOperator) expression).setRightExpression(new StringValue("{b}")); + assertEquals("doc #- '{b}'", expression.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT '{}'::jsonb #-", "SELECT '{}'::jsonb @?", + "SELECT '{}'::jsonb @? , 1"}) + void requiresRightOperand(String sql) { + assertThrows(net.sf.jsqlparser.JSQLParserException.class, () -> parse(sql)); + } + + @Test + void retainsAtSignIdentifierBehaviorInOtherDialects() throws Exception { + for (Dialect dialect : List.of(Dialect.SQLSERVER, Dialect.MYSQL)) { + PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT name@host FROM users", + p -> p.withDialect(dialect)); + assertEquals("name@host", select.getSelectItem(0).getExpression().toString()); + } + CCJSqlParser parser = CCJSqlParserUtil.newParser("doc@?'$.a'") + .withDialect(Dialect.POSTGRESQL); + assertEquals("doc", parser.getNextToken().image); + Token operator = parser.getNextToken(); + assertEquals("@?", operator.image); + assertEquals(4, operator.beginColumn); + assertEquals(5, operator.endColumn); + } +}