From 9ab1ebded5fa493b7b9e17806b878fb86b64cee9 Mon Sep 17 00:00:00 2001 From: Emily Ong Date: Thu, 16 Jan 2025 15:31:41 +0800 Subject: [PATCH] feat: Support unknown keyword (#2141) --- .../expression/ExpressionVisitor.java | 7 +++ .../expression/ExpressionVisitorAdapter.java | 6 ++ .../relational/IsUnknownExpression.java | 60 +++++++++++++++++++ .../parser/ParserKeywordsUtils.java | 1 + .../sf/jsqlparser/util/TablesNamesFinder.java | 7 +++ .../util/deparser/ExpressionDeParser.java | 16 +++++ .../validator/ExpressionValidator.java | 11 ++++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 18 ++++++ src/site/sphinx/keywords.rst | 12 ++-- .../relational/IsUnknownExpressionTest.java | 47 +++++++++++++++ .../builder/ReflectionModelTest.java | 1 + .../statement/select/SelectTest.java | 12 ++++ .../validator/ExpressionValidatorTest.java | 6 ++ 13 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java create mode 100644 src/test/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpressionTest.java diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 19067bf47..660d432bf 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -42,6 +42,7 @@ import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; 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.JsonOperator; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; import net.sf.jsqlparser.expression.operators.relational.Matches; @@ -267,6 +268,12 @@ default void visit(IsBooleanExpression isBooleanExpression) { this.visit(isBooleanExpression, null); } + T visit(IsUnknownExpression isUnknownExpression, S context); + + default void visit(IsUnknownExpression isUnknownExpression) { + this.visit(isUnknownExpression, null); + } + T visit(LikeExpression likeExpression, S context); default void visit(LikeExpression likeExpression) { diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 96f3c5ea2..77205c94b 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -42,6 +42,7 @@ import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; 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.JsonOperator; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; import net.sf.jsqlparser.expression.operators.relational.Matches; @@ -263,6 +264,11 @@ public T visit(IsBooleanExpression isBooleanExpression, S context) { return isBooleanExpression.getLeftExpression().accept(this, context); } + @Override + public T visit(IsUnknownExpression isUnknownExpression, S context) { + return isUnknownExpression.getLeftExpression().accept(this, context); + } + @Override public T visit(LikeExpression likeExpression, S context) { return visitBinaryExpression(likeExpression, context); diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java new file mode 100644 index 000000000..506d6e246 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpression.java @@ -0,0 +1,60 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2025 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression.operators.relational; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +public class IsUnknownExpression extends ASTNodeAccessImpl implements Expression { + + private Expression leftExpression; + private boolean isNot = false; + + public Expression getLeftExpression() { + return leftExpression; + } + + public void setLeftExpression(Expression expression) { + leftExpression = expression; + } + + public boolean isNot() { + return isNot; + } + + public void setNot(boolean isNot) { + this.isNot = isNot; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String toString() { + return leftExpression + " IS" + (isNot ? " NOT" : "") + " UNKNOWN"; + } + + public IsUnknownExpression withLeftExpression(Expression leftExpression) { + this.setLeftExpression(leftExpression); + return this; + } + + public IsUnknownExpression withNot(boolean isNot) { + this.setNot(isNot); + return this; + } + + public E getLeftExpression(Class type) { + return type.cast(getLeftExpression()); + } +} diff --git a/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java b/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java index 9c6dc52d9..d93fa0471 100644 --- a/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java +++ b/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java @@ -133,6 +133,7 @@ public class ParserKeywordsUtils { {"UNBOUNDED", RESTRICTED_JSQLPARSER}, {"UNION", RESTRICTED_SQL2016}, {"UNIQUE", RESTRICTED_SQL2016}, + {"UNKNOWN", RESTRICTED_SQL2016}, {"UNPIVOT", RESTRICTED_JSQLPARSER}, {"USE", RESTRICTED_JSQLPARSER}, {"USING", RESTRICTED_SQL2016}, diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index 3e8bc9e1b..3025aca3f 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -101,6 +101,7 @@ import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; 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.JsonOperator; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; import net.sf.jsqlparser.expression.operators.relational.Matches; @@ -530,6 +531,12 @@ public Void visit(IsBooleanExpression isBooleanExpression, S context) { return null; } + @Override + public Void visit(IsUnknownExpression isUnknownExpression, S context) { + + return null; + } + @Override public Void visit(JdbcParameter jdbcParameter, S context) { diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index d0f175e78..5d834c907 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -101,6 +101,7 @@ import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; 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.JsonOperator; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; import net.sf.jsqlparser.expression.operators.relational.Matches; @@ -428,6 +429,17 @@ public StringBuilder visit(IsBooleanExpression isBooleanExpression, S contex return buffer; } + @Override + public StringBuilder visit(IsUnknownExpression isUnknownExpression, S context) { + isUnknownExpression.getLeftExpression().accept(this, context); + if (isUnknownExpression.isNot()) { + buffer.append(" IS NOT UNKNOWN"); + } else { + buffer.append(" IS UNKNOWN"); + } + return buffer; + } + @Override public StringBuilder visit(JdbcParameter jdbcParameter, S context) { buffer.append(jdbcParameter.getParameterCharacter()); @@ -514,6 +526,10 @@ public void visit(IsBooleanExpression isBooleanExpression) { visit(isBooleanExpression, null); } + public void visit(IsUnknownExpression isUnknownExpression) { + visit(isUnknownExpression, null); + } + public void visit(JdbcParameter jdbcParameter) { visit(jdbcParameter, null); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index c4fc26a94..bea4d7845 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -102,6 +102,7 @@ import net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression; 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.JsonOperator; import net.sf.jsqlparser.expression.operators.relational.LikeExpression; import net.sf.jsqlparser.expression.operators.relational.Matches; @@ -289,6 +290,12 @@ public Void visit(IsBooleanExpression isBooleanExpression, S context) { return null; } + @Override + public Void visit(IsUnknownExpression isUnknownExpression, S context) { + isUnknownExpression.getLeftExpression().accept(this, context); + return null; + } + @Override public Void visit(JdbcParameter jdbcParameter, S context) { validateFeature(Feature.jdbcParameter); @@ -383,6 +390,10 @@ public void visit(IsBooleanExpression isBooleanExpression) { visit(isBooleanExpression, null); // Call the parametrized visit method with null context } + public void visit(IsUnknownExpression isUnknownExpression) { + visit(isUnknownExpression, null); // Call the parametrized visit method with null context + } + public void visit(JdbcParameter jdbcParameter) { visit(jdbcParameter, null); // Call the parametrized visit method with null context } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 65d0d2a7c..f0d1f0782 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -496,6 +496,7 @@ TOKEN: /* SQL Keywords. prefixed with K_ to avoid name clashes */ | | | +| | | | @@ -4153,6 +4154,8 @@ Expression SQLCondition(): | LOOKAHEAD(IsBooleanExpression()) result=IsBooleanExpression(left) | + LOOKAHEAD(IsUnknownExpression()) result=IsUnknownExpression(left) + | LOOKAHEAD(2) result=LikeExpression(left) | LOOKAHEAD(IsDistinctExpression()) result=IsDistinctExpression(left) @@ -4366,6 +4369,21 @@ Expression IsBooleanExpression(Expression leftExpression): } } +Expression IsUnknownExpression(Expression leftExpression): +{ + IsUnknownExpression result = new IsUnknownExpression(); +} +{ + ( + [ { result.setNot(true); } ] + ) + + { + result.setLeftExpression(leftExpression); + return result; + } +} + Expression ExistsExpression(): { ExistsExpression result = new ExistsExpression(); diff --git a/src/site/sphinx/keywords.rst b/src/site/sphinx/keywords.rst index d301f23e6..f6e3926de 100644 --- a/src/site/sphinx/keywords.rst +++ b/src/site/sphinx/keywords.rst @@ -77,7 +77,7 @@ The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and +----------------------+-------------+-----------+ | QUALIFY | Yes | | +----------------------+-------------+-----------+ -| HAVING | Yes | Yes | +| HAVING | Yes | Yes | +----------------------+-------------+-----------+ | IF | Yes | Yes | +----------------------+-------------+-----------+ @@ -97,7 +97,7 @@ The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and +----------------------+-------------+-----------+ | INTERVAL | Yes | Yes | +----------------------+-------------+-----------+ -| INTO | Yes | Yes | +| INTO | Yes | Yes | +----------------------+-------------+-----------+ | IS | Yes | Yes | +----------------------+-------------+-----------+ @@ -109,7 +109,7 @@ The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and +----------------------+-------------+-----------+ | LIKE | Yes | Yes | +----------------------+-------------+-----------+ -| LIMIT | Yes | Yes | +| LIMIT | Yes | Yes | +----------------------+-------------+-----------+ | MINUS | Yes | Yes | +----------------------+-------------+-----------+ @@ -139,7 +139,9 @@ The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and +----------------------+-------------+-----------+ | OPTIMIZE | Yes | Yes | +----------------------+-------------+-----------+ -| PIVOT | Yes | Yes | +| OVERWRITE | Yes | Yes | ++----------------------+-------------+-----------+ +| PIVOT | Yes | Yes | +----------------------+-------------+-----------+ | PREFERRING | Yes | Yes | +----------------------+-------------+-----------+ @@ -181,6 +183,8 @@ The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and +----------------------+-------------+-----------+ | UNIQUE | Yes | Yes | +----------------------+-------------+-----------+ +| UNKNOWN | Yes | Yes | ++----------------------+-------------+-----------+ | UNPIVOT | Yes | Yes | +----------------------+-------------+-----------+ | USE | Yes | Yes | diff --git a/src/test/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpressionTest.java new file mode 100644 index 000000000..322842395 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/operators/relational/IsUnknownExpressionTest.java @@ -0,0 +1,47 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2023 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression.operators.relational; + +import net.sf.jsqlparser.schema.Column; +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.assertDoesNotThrow; + +class IsUnknownExpressionTest { + + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM mytable WHERE 1 IS UNKNOWN", + "SELECT * FROM mytable WHERE 1 IS NOT UNKNOWN", + }) + public void testIsUnknownExpression(String sqlStr) { + assertDoesNotThrow(() -> TestUtils.assertSqlCanBeParsedAndDeparsed(sqlStr)); + } + + @Test + void testStringConstructor() { + Column column = new Column("x"); + + IsUnknownExpression defaultIsUnknownExpression = + new IsUnknownExpression().withLeftExpression(column); + TestUtils.assertExpressionCanBeDeparsedAs(defaultIsUnknownExpression, "x IS UNKNOWN"); + + IsUnknownExpression isUnknownExpression = + new IsUnknownExpression().withLeftExpression(column).withNot(false); + TestUtils.assertExpressionCanBeDeparsedAs(isUnknownExpression, "x IS UNKNOWN"); + + IsUnknownExpression isNotUnknownExpression = + new IsUnknownExpression().withLeftExpression(column).withNot(true); + TestUtils.assertExpressionCanBeDeparsedAs(isNotUnknownExpression, "x IS NOT UNKNOWN"); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java b/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java index 7d4c33714..c458429df 100644 --- a/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/builder/ReflectionModelTest.java @@ -110,6 +110,7 @@ public class ReflectionModelTest { new net.sf.jsqlparser.expression.operators.relational.InExpression(), new net.sf.jsqlparser.expression.operators.relational.IsBooleanExpression(), new net.sf.jsqlparser.expression.operators.relational.IsNullExpression(), + new net.sf.jsqlparser.expression.operators.relational.IsUnknownExpression(), new net.sf.jsqlparser.expression.operators.relational.JsonOperator("@>"), new net.sf.jsqlparser.expression.operators.relational.LikeExpression(), new net.sf.jsqlparser.expression.operators.relational.Matches(), diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index 4164f1cbc..b77d17cd9 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -2271,6 +2271,18 @@ public void testIsNotFalse() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(statement); } + @Test + public void testIsUnknown() throws JSQLParserException { + String statement = "SELECT col FROM tbl WHERE col IS UNKNOWN"; + assertSqlCanBeParsedAndDeparsed(statement); + } + + @Test + public void testIsNotUnknown() throws JSQLParserException { + String statement = "SELECT col FROM tbl WHERE col IS NOT UNKNOWN"; + assertSqlCanBeParsedAndDeparsed(statement); + } + @Test public void testTSQLJoin() throws JSQLParserException { String stmt = "SELECT * FROM tabelle1, tabelle2 WHERE tabelle1.a *= tabelle2.b"; diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java index edba27f66..d83bb2e51 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidatorTest.java @@ -150,6 +150,12 @@ public void testIsNull() { validateNoErrors("SELECT * FROM tab t WHERE t.col IS NOT NULL", 1, EXPRESSIONS); } + @Test + public void testIsUnknown() { + validateNoErrors("SELECT * FROM tab t WHERE t.col IS UNKNOWN", 1, EXPRESSIONS); + validateNoErrors("SELECT * FROM tab t WHERE t.col IS NOT UNKNOWN", 1, EXPRESSIONS); + } + @Test public void testLike() { validateNoErrors("SELECT * FROM tab t WHERE t.col LIKE '%search for%'", 1, EXPRESSIONS);