Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -590,17 +590,27 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
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;
Expand All @@ -626,7 +636,16 @@ public class CCJSqlParser extends AbstractJSqlParser<CCJSqlParser> {
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; }
Expand Down Expand Up @@ -2732,6 +2751,8 @@ TOKEN : /* Operators */
| <OP_SLASH: "/">
| <OP_LSHIFT: "<<">
| <OP_RSHIFT: ">>">
| <OP_JSON_PATH_DELETE: "#-">
| <OP_JSON_PATH_EXISTS: "@?">
| <OP_DOUBLEAND: "&&">
| <OP_CONTAINS: "&>">
| <OP_CONTAINEDBY: "<&">
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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="->"
|
Expand Down Expand Up @@ -11354,8 +11380,9 @@ JsonExpression JsonExpression(Expression expr, List<Map.Entry<Expression, String
}

(
LOOKAHEAD(2, { (ternaryThenBranchDepth == 0 && mapKeyDepth == 0)
|| !":".equals(getToken(1).image) }) (
LOOKAHEAD(2, { !isPostgreSqlJsonAccess(getToken(1))
&& ((ternaryThenBranchDepth == 0 && mapKeyDepth == 0)
|| !":".equals(getToken(1).image)) }) (
token="->"
|
token=":"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.parser;

import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.JdbcParameter;
import net.sf.jsqlparser.expression.JsonExpression;
import net.sf.jsqlparser.expression.StringValue;
import net.sf.jsqlparser.expression.operators.arithmetic.Concat;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression;
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.SelectDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

class PostgreSqlJsonPathOperatorTest {
private PlainSelect parse(String sql) throws Exception {
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> 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<String> visited = new ArrayList<>();
ExpressionDeParser expressions = new ExpressionDeParser() {
@Override
public <S> 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);
}
}
Loading