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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression;
import net.sf.jsqlparser.expression.operators.relational.IsJsonExpression;
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.expression.operators.relational.Matches;
Expand Down Expand Up @@ -110,7 +111,7 @@
}

default <S> T visitLimit(Limit limit, S context) {
if (limit != null && !limit.isLimitNull() && !limit.isLimitAll()) {

Check warning on line 114 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 114 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (ubuntu-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 114 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 114 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (macos-latest)

[deprecation] isLimitNull() in Limit has been deprecated

Check warning on line 114 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitAll() in Limit has been deprecated

Check warning on line 114 in src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java

View workflow job for this annotation

GitHub Actions / Maven Verify (windows-latest)

[deprecation] isLimitNull() in Limit has been deprecated
if (limit.getOffset() != null) {
limit.getOffset().accept(this, context);
}
Expand Down Expand Up @@ -370,6 +371,14 @@
this.visit(isBooleanExpression, null);
}

default <S> T visit(IsJsonExpression expression, S context) {
return expression.getLeftExpression().accept(this, context);
}

default void visit(IsJsonExpression expression) {
visit(expression, null);
}

<S> T visit(IsUnknownExpression isUnknownExpression, S context);

default void visit(IsUnknownExpression isUnknownExpression) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*-
* #%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.expression.operators.relational;

import java.util.function.Consumer;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.ExpressionVisitor;
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;

/** SQL/JSON type and key uniqueness predicate; the JSON value is not evaluated by the parser. */
public class IsJsonExpression extends ASTNodeAccessImpl implements Expression {
public enum Type {
VALUE, SCALAR, ARRAY, OBJECT
}

public enum UniqueKeys {
WITH, WITHOUT
}

private Expression leftExpression;
private boolean not;
private Type type;
private UniqueKeys uniqueKeys;
private boolean useKeysKeyword = true;

public Expression getLeftExpression() {
return leftExpression;
}

public void setLeftExpression(Expression leftExpression) {
this.leftExpression = leftExpression;
}

public boolean isNot() {
return not;
}

public void setNot(boolean not) {
this.not = not;
}

public Type getType() {
return type;
}

public void setType(Type type) {
this.type = type;
}

public UniqueKeys getUniqueKeys() {
return uniqueKeys;
}

public void setUniqueKeys(UniqueKeys uniqueKeys) {
this.uniqueKeys = uniqueKeys;
}

public boolean isUseKeysKeyword() {
return useKeysKeyword;
}

public void setUseKeysKeyword(boolean useKeysKeyword) {
this.useKeysKeyword = useKeysKeyword;
}

public StringBuilder appendTo(StringBuilder builder, Consumer<Expression> expressionRenderer) {
expressionRenderer.accept(leftExpression);
builder.append(not ? " IS NOT JSON" : " IS JSON");
if (type != null) {
builder.append(' ').append(type);
}
if (uniqueKeys != null) {
builder.append(' ').append(uniqueKeys).append(" UNIQUE");
if (useKeysKeyword) {
builder.append(" KEYS");
}
}
return builder;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
return appendTo(builder, builder::append).toString();
}

@Override
public <T, S> T accept(ExpressionVisitor<T> visitor, S context) {
return visitor.visit(this, context);
}
}
7 changes: 7 additions & 0 deletions src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression;
import net.sf.jsqlparser.expression.operators.relational.IsJsonExpression;
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.expression.operators.relational.Matches;
Expand Down Expand Up @@ -721,6 +722,12 @@ public <S> Void visit(IsBooleanExpression isBooleanExpression, S context) {
return null;
}

@Override
public <S> Void visit(IsJsonExpression expression, S context) {
expression.getLeftExpression().accept(this, context);
return null;
}

@Override
public <S> Void visit(IsUnknownExpression isUnknownExpression, S context) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
import net.sf.jsqlparser.expression.operators.relational.IsDistinctExpression;
import net.sf.jsqlparser.expression.operators.relational.IsNullExpression;
import net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression;
import net.sf.jsqlparser.expression.operators.relational.IsJsonExpression;
import net.sf.jsqlparser.expression.operators.relational.JsonOperator;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.expression.operators.relational.Matches;
Expand Down Expand Up @@ -477,6 +478,11 @@ public <S> StringBuilder visit(IsBooleanExpression isBooleanExpression, S contex
return builder;
}

@Override
public <S> StringBuilder visit(IsJsonExpression expression, S context) {
return expression.appendTo(builder, value -> value.accept(this, context));
}

@Override
public <S> StringBuilder visit(IsUnknownExpression isUnknownExpression, S context) {
if (isUnknownExpression
Expand Down
22 changes: 21 additions & 1 deletion src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
Original file line number Diff line number Diff line change
Expand Up @@ -10009,6 +10009,7 @@ Expression Condition():
| LOOKAHEAD(2) result = MemberOfExpression(left)
| LOOKAHEAD(3) result=IsNullExpression(left)
| LOOKAHEAD(3) result=IsBooleanExpression(left)
| LOOKAHEAD(3) result=IsJsonExpression(left)
| LOOKAHEAD(3) result=IsUnknownExpression(left)
| LOOKAHEAD(2) result=LikeExpression(left)
| LOOKAHEAD(3) result=IsDistinctExpression(left)
Expand Down Expand Up @@ -10141,7 +10142,8 @@ Expression SQLCondition():
|
LOOKAHEAD(3) result=IsBooleanExpression(left)
|
LOOKAHEAD(3) result=IsUnknownExpression(left)
LOOKAHEAD(3) result=IsJsonExpression(left)
| LOOKAHEAD(3) result=IsUnknownExpression(left)
|
LOOKAHEAD(2) result=LikeExpression(left)
|
Expand Down Expand Up @@ -10401,6 +10403,24 @@ Expression IsBooleanExpression(Expression leftExpression):
}
}

Expression IsJsonExpression(Expression leftExpression):
{
IsJsonExpression result = new IsJsonExpression();
Token type;
}
{
<K_IS> [ <K_NOT> { result.setNot(true); } ] <K_JSON>
[ LOOKAHEAD({ isKeywordAhead("VALUE") || isKeywordAhead("SCALAR")
|| isKeywordAhead("ARRAY") || isKeywordAhead("OBJECT") })
( type=<K_VALUE> | type=<K_ARRAY_LITERAL> | type=<S_IDENTIFIER> | type=<DATA_TYPE> )
{ result.setType(IsJsonExpression.Type.valueOf(type.image.toUpperCase(Locale.ROOT))); } ]
[ LOOKAHEAD(2) ( <K_WITH> { result.setUniqueKeys(IsJsonExpression.UniqueKeys.WITH); }
| <K_WITHOUT> { result.setUniqueKeys(IsJsonExpression.UniqueKeys.WITHOUT); } )
<K_UNIQUE> { result.setUseKeysKeyword(false); }
[ LOOKAHEAD(<K_KEYS>) <K_KEYS> { result.setUseKeysKeyword(true); } ] ]
{ result.setLeftExpression(leftExpression); return result; }
}

Expression IsUnknownExpression(Expression leftExpression):
{
IsUnknownExpression result = new IsUnknownExpression();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*-
* #%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.expression.operators.relational;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
import java.util.ArrayList;
import java.util.List;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.parser.feature.Feature;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.util.TablesNamesFinder;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import net.sf.jsqlparser.util.deparser.StatementDeParser;
import net.sf.jsqlparser.util.validation.Validation;
import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class IsJsonExpressionTest {
@ParameterizedTest
@ValueSource(strings = {"", " VALUE", " SCALAR", " ARRAY", " OBJECT",
" WITH UNIQUE KEYS", " WITHOUT UNIQUE", " OBJECT WITH UNIQUE",
" ARRAY WITHOUT UNIQUE KEYS"})
void modelsOptionalTypeAndUniquenessInBothParserModes(String options) throws Exception {
for (boolean complex : List.of(false, true)) {
for (String not : List.of("", " NOT")) {
String sql = "SELECT payload IS" + not + " JSON" + options + " FROM events";
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sql,
p -> p.withDialect(Dialect.POSTGRESQL).withAllowComplexParsing(complex));
IsJsonExpression predicate = assertInstanceOf(IsJsonExpression.class,
select.getSelectItem(0).getExpression());
assertEquals(!not.isEmpty(), predicate.isNot());
assertEquals(sql, select.toString());
roundTrip(select);
}
}
}

@Test
void exposesMutableOptionsAndVisitsTheOperandWithContext() throws Exception {
PlainSelect select = parse("SELECT '{\"a\":1}' IS JSON");
IsJsonExpression predicate = (IsJsonExpression) select.getSelectItem(0).getExpression();
assertNull(predicate.getType());
assertNull(predicate.getUniqueKeys());
predicate.setType(IsJsonExpression.Type.OBJECT);
predicate.setUniqueKeys(IsJsonExpression.UniqueKeys.WITH);
predicate.setNot(true);
predicate.setLeftExpression(new StringValue("{}"));
assertEquals("SELECT '{}' IS NOT JSON OBJECT WITH UNIQUE KEYS", select.toString());
roundTrip(select);
Object expected = new Object();
List<String> seen = new ArrayList<>();
predicate.accept(new ExpressionVisitorAdapter<Void>() {
@Override
public <S> Void visit(StringValue value, S context) {
assertSame(expected, context);
seen.add(value.getValue());
return null;
}
}, expected);
assertEquals(List.of("{}"), seen);
StringBuilder output = new StringBuilder();
ExpressionDeParser visitor = new ExpressionDeParser() {
@Override
public <S> StringBuilder visit(StringValue value, S context) {
assertSame(expected, context);
return getBuilder().append("'[]'");
}
};
visitor.setBuilder(output);
predicate.accept(visitor, expected);
assertEquals("'[]' IS NOT JSON OBJECT WITH UNIQUE KEYS", output.toString());
}

@Test
void traversesSubqueriesAndValidatesOperandFeatures() throws Exception {
PlainSelect select = parse("SELECT 1 FROM t WHERE (SELECT payload FROM hidden) IS JSON");
assertThat(new TablesNamesFinder().getTables((Statement) select)).containsExactlyInAnyOrder(
"t",
"hidden");
FeaturesAllowed allowed =
new FeaturesAllowed().add(FeaturesAllowed.DML).remove(Feature.jdbcParameter);
assertFalse(
new Validation(List.of(allowed), "SELECT 1 WHERE ? IS JSON").validate().isEmpty());
roundTrip(parse("SELECT NULL IS JSON OR '{}' IS NOT JSON AND '1' IS JSON SCALAR"));
}

@ParameterizedTest
@ValueSource(strings = {"SELECT x IS JSON WITH", "SELECT x IS JSON WITH KEYS",
"SELECT x IS JSON WITH UNIQUE KEYS WITHOUT UNIQUE",
"SELECT x IS NOT JSON WITH UNIQUE,"})
void rejectsIncompleteOptions(String sql) {
assertThrows(JSQLParserException.class, () -> parse(sql));
}

private static PlainSelect parse(String sql) throws JSQLParserException {
return (PlainSelect) CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL));
}

private static void roundTrip(PlainSelect select) throws Exception {
StringBuilder out = new StringBuilder();
select.accept(new StatementDeParser(out), null);
assertEquals(select.toString(), out.toString());
assertEquals(select.toString(), parse(out.toString()).toString());
}
}
Loading