diff --git a/.github/workflows/maven.yml b/.github/workflows/dependencies_check.yml
similarity index 61%
rename from .github/workflows/maven.yml
rename to .github/workflows/dependencies_check.yml
index 2a4f4d9d..2cb0303c 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/dependencies_check.yml
@@ -1,6 +1,3 @@
-# This workflow will build a Java project with Maven
-# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
-
name: Dependencies Check
on:
@@ -17,5 +14,12 @@ jobs:
uses: actions/setup-java@v1
with:
java-version: 11
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
- name: Checking dependencies for vulnerabilities
run: mvn org.sonatype.ossindex.maven:ossindex-maven-plugin:audit -f pom.xml
\ No newline at end of file
diff --git a/.github/workflows/maven_central_release.yml b/.github/workflows/maven_central_release.yml
index 09757aaf..1146142f 100644
--- a/.github/workflows/maven_central_release.yml
+++ b/.github/workflows/maven_central_release.yml
@@ -18,6 +18,13 @@ jobs:
- name: Import GPG Key
run:
gpg --import --batch <(echo "${{ secrets.OSSRH_GPG_SECRET_KEY }}")
+ - name: Cache local Maven repository
+ uses: actions/cache@v2
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
- name: Publish to Central Repository
env:
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md
index 4b0f9318..30bb32b0 100644
--- a/doc/changes/changelog.md
+++ b/doc/changes/changelog.md
@@ -1,7 +1,7 @@
# Changes
-* [4.3.1](changes_4.3.1.md)
+* [4.4.0](changes_4.4.0.md)
* [4.3.0](changes_4.3.0.md)
* [4.2.0](changes_4.2.0.md)
* [4.1.0](changes_4.1.0.md)
-* [4.0.0](changes_4.0.0.md)
\ No newline at end of file
+* [4.0.0](changes_4.0.0.md)
diff --git a/doc/changes/changes_4.3.1.md b/doc/changes/changes_4.3.1.md
deleted file mode 100644
index b247eec9..00000000
--- a/doc/changes/changes_4.3.1.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# SQL Statement Builder 4.3.1, released 2020-XX-XX
-
-Code Name: Refactoring
-
-## Refactoring
-
-* #98: Refactored comparison and like class structure
- The refactoring changed the internal representation of the `Comparison`.
- The public API from `BooleanTerm` did however not change.
-* #95: Refactored `ValueExpressionVisitor`
diff --git a/doc/changes/changes_4.4.0.md b/doc/changes/changes_4.4.0.md
new file mode 100644
index 00000000..a6b355c6
--- /dev/null
+++ b/doc/changes/changes_4.4.0.md
@@ -0,0 +1,34 @@
+# SQL Statement Builder 4.4.0, released 2021-03-04
+
+Code Name: More Predicates
+
+## Summary
+
+In this release, we have added additional Exasol predicates such as `IS [NOT]
+NULL` or `[NOT] BETWEEN`. We also refactored the comparison and `LIKE`
+operators.
+
+## Features / Improvements
+
+* #104: Added additional predicates
+
+## Refactoring
+
+* #98: Refactored comparison and like class structure
+ The refactoring changed the internal representation of the `Comparison`.
+ The public API from `BooleanTerm` did however not change.
+* #95: Refactored `ValueExpressionVisitor`
+
+## Dependency Updates
+
+* Updated `org.mockito:mockito-core:3.5.13` to `3.8.0`
+* Updated `org.mockito:mockito-junit-jupiter:3.5.13` to `3.8.0`
+* Updated `org.junit.jupiter:junit-jupiter:5.7.0` to `5.7.1`
+* Updated `nl.jqno.equalsverifier:equalsverifier:3.4.3` to `3.5.5`
+
+### Plugin Updates
+
+* Updated `org.codehaus.mojo:versions-maven-plugin:2.7` to `2.8.1`
+* Updated `org.itsallcode:openfasttrace-maven-plugin:0.1.0` to `1.0.0`
+* Updated `org.jacoco:jacoco-maven-plugin:0.8.5` to `0.8.6`
+
diff --git a/doc/design.md b/doc/design.md
index 114d36df..e5359386 100644
--- a/doc/design.md
+++ b/doc/design.md
@@ -95,6 +95,17 @@ Covers:
Needs: impl, utest
+#### Predicate Operators
+`dsn~predicate-operators~1`
+
+Forwarded from requirements.
+
+Covers:
+
+* `req~predicate-operators~1`
+
+Needs: impl, utest
+
#### Boolean Literals
`dsn~boolean-literals~1`
diff --git a/doc/system_requirements.md b/doc/system_requirements.md
index 67a43e0f..1a3f89c1 100644
--- a/doc/system_requirements.md
+++ b/doc/system_requirements.md
@@ -110,7 +110,7 @@ This is necessary since complex statements are usually build as a result of mult
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -125,7 +125,7 @@ If users can't get illegal structures to compile, they don't need to spend time
Covers:
-* [feat~compile-time-error-checking~1](#compile-time-error-checking)
+* [`feat~compile-time-error-checking~1`](#compile-time-error-checking)
Needs: dsn
@@ -138,7 +138,7 @@ ESB supports the following arithmetic operators: `+`, `-`, `*`, `/`.
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -158,7 +158,7 @@ ESB supports the following comparison operations:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -169,18 +169,34 @@ ESB supports the following boolean operators: `AND`, `OR` and `NOT`
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
#### [NOT] LIKE Predicate
`req~like-predicate~1`
-ESB supports the [NOT] LIKE predicate.
+ESB supports the `[NOT] LIKE` predicate.
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
+
+Needs: dsn
+
+#### Predicate Operators
+`req~predicate-operators~1`
+
+ESB supports the following predicate operators:
+
+* `[NOT] BETWEEN`
+* `EXISTS`
+* `[NOT] IN`
+* `IS [NOT] NULL`
+
+Covers:
+
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -194,7 +210,7 @@ ESB can convert the following string literals into boolean values, independently
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -205,7 +221,7 @@ ESB supports the following literal values: `default`, `double`, `float`, `intege
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -222,7 +238,7 @@ ESB supports the following way to construct tables from a value table:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -249,7 +265,7 @@ Create table:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -272,7 +288,7 @@ Drop table:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -292,7 +308,7 @@ ESB supports the following `INSERT` statement:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -306,7 +322,7 @@ ESB supports a list of explicit values as `INSERT` source:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -338,7 +354,7 @@ ESB supports the following `MERGE` statement:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -375,7 +391,7 @@ ESB supports the following `SELECT` statement:
Covers:
-* [feat~statement-definition~1](#statement-definition)
+* [`feat~statement-definition~1`](#statement-definition)
Needs: dsn
@@ -392,7 +408,7 @@ While keyword case is mostly an esthetic point, different users still have diffe
Covers:
-* [feat~sql-string-rendering~1](#sql-string-rendering)
+* [`feat~sql-string-rendering~1`](#sql-string-rendering)
Needs: dsn
@@ -411,7 +427,7 @@ The Exasol database for example requires identifiers to be enclosed in double qu
Covers:
-* [feat~sql-string-rendering~1](#sql-string-rendering)
+* [`feat~sql-string-rendering~1`](#sql-string-rendering)
Needs: dsn
@@ -422,7 +438,7 @@ ESB renders abstract `SELECT` statements into SQL query strings.
Covers:
-* [feat~sql-string-rendering~1](#sql-string-rendering)
+* [`feat~sql-string-rendering~1`](#sql-string-rendering)
Needs: dsn
@@ -433,7 +449,7 @@ ESB renders abstract `CREATE` statements into SQL data definition language strin
Covers:
-* [feat~sql-string-rendering~1](#sql-string-rendering)
+* [`feat~sql-string-rendering~1`](#sql-string-rendering)
Needs: dsn
@@ -444,7 +460,7 @@ ESB renders abstract `DROP` statements into SQL data definition language strings
Covers:
-* [feat~sql-string-rendering~1](#sql-string-rendering)
+* [`feat~sql-string-rendering~1`](#sql-string-rendering)
Needs: dsn
@@ -455,7 +471,7 @@ ESB renders abstract `INSERT` statements into SQL data manipulation language str
Covers:
-* [feat~sql-string-rendering~1](#sql-string-rendering)
+* [`feat~sql-string-rendering~1`](#sql-string-rendering)
Needs: dsn
@@ -466,7 +482,7 @@ ESB renders abstract `MERGE` statements into SQL data manipulation language stri
Covers:
-* [feat~sql-string-rendering~1](#sql-string-rendering)
+* [`feat~sql-string-rendering~1`](#sql-string-rendering)
Needs: dsn
@@ -483,6 +499,6 @@ Neighboring systems of an Exasol database often do not have equivalent data type
Covers:
-* [feat~data-conversion~1](#data-conversion)
+* [`feat~data-conversion~1`](#data-conversion)
-Needs: dsn
\ No newline at end of file
+Needs: dsn
diff --git a/pom.xml b/pom.xml
index 2b25ca25..11291c39 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,3 +1,4 @@
+
@@ -67,7 +68,7 @@
org.junit.jupiter
junit-jupiter
- 5.7.0
+ 5.7.1
test
@@ -79,19 +80,19 @@
org.mockito
mockito-core
- 3.5.13
+ 3.8.0
test
org.mockito
mockito-junit-jupiter
- 3.5.13
+ 3.8.0
test
nl.jqno.equalsverifier
equalsverifier
- 3.4.3
+ 3.5.5
test
@@ -109,7 +110,7 @@
org.jacoco
jacoco-maven-plugin
- 0.8.5
+ 0.8.6
@@ -123,12 +124,39 @@
report
+
+ prepare-agent
+
+ prepare-agent
+
+
+
+ prepare-agent-integration
+
+ prepare-agent-integration
+
+
+
+ report-integration
+ verify
+
+ report-integration
+
+
org.apache.maven.plugins
maven-surefire-plugin
3.0.0-M4
+
+
+ -Djava.util.logging.config.file=src/test/resources/logging.properties ${argLine}
+
+ **IT.java
+
+
org.apache.maven.plugins
@@ -187,7 +215,7 @@
org.itsallcode
openfasttrace-maven-plugin
- 0.1.0
+ 1.0.0
trace-requirements
@@ -213,7 +241,7 @@
org.codehaus.mojo
versions-maven-plugin
- 2.7
+ 2.8.1
package
@@ -223,6 +251,9 @@
+
+ file:///${project.basedir}/versionsMavenPluginRules.xml
+
org.apache.maven.plugins
@@ -263,6 +294,66 @@
+
+ com.exasol
+ project-keeper-maven-plugin
+ 0.5.0
+
+
+
+ verify
+
+
+
+
+
+ maven_central
+ integration_tests
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.0.0-M3
+
+
+ -Djava.util.logging.config.file=src/test/resources/logging.properties ${argLine}
+
+ **IT.java
+
+
+
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+ com.exasol
+ error-code-crawler-maven-plugin
+ 0.1.1
+
+
+
+ verify
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java
index f04889d9..6ce8d960 100644
--- a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java
+++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java
@@ -2,6 +2,7 @@
import com.exasol.sql.expression.comparison.Comparison;
import com.exasol.sql.expression.literal.BooleanLiteral;
+import com.exasol.sql.expression.predicate.Predicate;
/**
* Visitor interface for a {@link BooleanTerm}
@@ -16,4 +17,6 @@ public interface BooleanExpressionVisitor {
public void visit(Or or);
public void visit(Comparison comparison);
-}
\ No newline at end of file
+
+ public void visit(Predicate predicate);
+}
diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java
index 583fcacf..3b7c7dcd 100644
--- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java
+++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java
@@ -1,9 +1,12 @@
package com.exasol.sql.expression;
+import com.exasol.sql.dql.select.Select;
import com.exasol.sql.expression.comparison.LikeComparison;
import com.exasol.sql.expression.comparison.SimpleComparison;
import com.exasol.sql.expression.comparison.SimpleComparisonOperator;
import com.exasol.sql.expression.literal.BooleanLiteral;
+import com.exasol.sql.expression.predicate.*;
+import com.exasol.sql.expression.predicate.IsNullPredicate.IsNullPredicateOperator;
// [impl->dsn~boolean-operators~1]
public abstract class BooleanTerm extends AbstractBooleanExpression {
@@ -110,6 +113,53 @@ public static BooleanExpression ge(final ValueExpression left, final ValueExpres
return new SimpleComparison(SimpleComparisonOperator.GREATER_THAN_OR_EQUAL, left, right);
}
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression isNull(final ValueExpression operand) {
+ return new IsNullPredicate(operand);
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression isNotNull(final ValueExpression operand) {
+ return new IsNullPredicate(IsNullPredicateOperator.IS_NOT_NULL, operand);
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression in(final ValueExpression operand, final ValueExpression... operands) {
+ return InPredicate.builder().expression(operand).operands(operands).build();
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression notIn(final ValueExpression operand, final ValueExpression... operands) {
+ return InPredicate.builder().expression(operand).operands(operands).not().build();
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression in(final ValueExpression operand, final Select select) {
+ return InPredicate.builder().expression(operand).selectQuery(select).build();
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression notIn(final ValueExpression operand, final Select select) {
+ return InPredicate.builder().expression(operand).selectQuery(select).not().build();
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression exists(final Select select) {
+ return new ExistsPredicate(select);
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression between(final ValueExpression expression, final ValueExpression start,
+ final ValueExpression end) {
+ return BetweenPredicate.builder().expression(expression).start(start).end(end).build();
+ }
+
+ // [impl->dsn~predicate-operators~1]
+ public static BooleanExpression notBetween(final ValueExpression expression, final ValueExpression start,
+ final ValueExpression end) {
+ return BetweenPredicate.builder().expression(expression).start(start).end(end).not().build();
+ }
+
/**
* Create a logical operation from an operator name and a list of operands
*
@@ -136,4 +186,4 @@ public static BooleanExpression operation(final String operator, final BooleanEx
"Unknown boolean connector \"" + operator + "\". Must be one of \"and\" or \"or\".");
}
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/AbstractPredicate.java b/src/main/java/com/exasol/sql/expression/predicate/AbstractPredicate.java
new file mode 100644
index 00000000..8eaddce8
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/AbstractPredicate.java
@@ -0,0 +1,36 @@
+package com.exasol.sql.expression.predicate;
+
+import com.exasol.sql.expression.BooleanExpressionVisitor;
+import com.exasol.sql.expression.ValueExpressionVisitor;
+
+/**
+ * An abstract basis for predicate classes.
+ */
+public abstract class AbstractPredicate implements Predicate {
+ private final PredicateOperator operator;
+
+ /**
+ * Creates a new instance of {@link AbstractPredicate}.
+ *
+ * @param operator a predicate operator
+ */
+ protected AbstractPredicate(final PredicateOperator operator) {
+ this.operator = operator;
+ }
+
+ @Override
+ public PredicateOperator getOperator() {
+ return this.operator;
+ }
+
+ @Override
+ public void accept(final BooleanExpressionVisitor visitor) {
+ visitor.visit(this);
+ }
+
+ @Override
+ public void accept(final ValueExpressionVisitor visitor) {
+ visitor.visit(this);
+ }
+
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/BetweenPredicate.java b/src/main/java/com/exasol/sql/expression/predicate/BetweenPredicate.java
new file mode 100644
index 00000000..d2da2124
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/BetweenPredicate.java
@@ -0,0 +1,143 @@
+package com.exasol.sql.expression.predicate;
+
+import com.exasol.sql.expression.ValueExpression;
+
+/**
+ * A class that represents a {@code [NOT] BETWEEN} predicate.
+ */
+// [impl->dsn~predicate-operators~1]
+public class BetweenPredicate extends AbstractPredicate {
+ private final ValueExpression expression;
+ private final ValueExpression start;
+ private final ValueExpression end;
+
+ private BetweenPredicate(final Builder builder) {
+ super(builder.operator);
+ this.expression = builder.expression;
+ this.start = builder.start;
+ this.end = builder.end;
+ }
+
+ /**
+ * Returns the left expression in the {@code [NOT] BETWEEN} predicate.
+ *
+ * @return left expression in the predicate
+ */
+ public ValueExpression getExpression() {
+ return expression;
+ }
+
+ /**
+ * Returns the start expression in the {@code [NOT] BETWEEN} predicate.
+ *
+ * @return start expression in the predicate
+ */
+ public ValueExpression getStartExpression() {
+ return start;
+ }
+
+ /**
+ * Returns the end expression in the {@code [NOT] BETWEEN} predicate.
+ *
+ * @return end expression in the predicate
+ */
+ public ValueExpression getEndExpression() {
+ return end;
+ }
+
+ /**
+ * Creates a new builder for {@link BetweenPredicate}.
+ *
+ * @return new {@link Builder}
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A class that represents {@link BetweenPredicate} operator.
+ */
+ public enum BetweenPredicateOperator implements PredicateOperator {
+ BETWEEN, NOT_BETWEEN;
+
+ @Override
+ public String toString() {
+ return super.toString().replace("_", " ");
+ }
+ }
+
+ /**
+ * A builder for {@link BetweenPredicate}.
+ */
+ public static class Builder {
+ private ValueExpression expression;
+ private ValueExpression start;
+ private ValueExpression end;
+ private BetweenPredicateOperator operator = BetweenPredicateOperator.BETWEEN;
+
+ /**
+ * A private constructor to hide the public default.
+ */
+ private Builder() {
+ // intentionally empty
+ }
+
+ /**
+ * Adds the left expression of predicate.
+ *
+ * @param expression in predicate expression
+ * @return this
for fluent programming
+ */
+ public Builder expression(final ValueExpression expression) {
+ this.expression = expression;
+ return this;
+ }
+
+ /**
+ * Adds the start expression of predicate.
+ *
+ * @param start start expression in predicate
+ * @return this
for fluent programming
+ */
+ public Builder start(final ValueExpression start) {
+ this.start = start;
+ return this;
+ }
+
+ /**
+ * Adds the end expression of predicate.
+ *
+ * @param end end expression in predicate
+ * @return this
for fluent programming
+ */
+ public Builder end(final ValueExpression end) {
+ this.end = end;
+ return this;
+ }
+
+ /**
+ * Sets {@code NOT BETWEEN} predicate.
+ *
+ * @return this
for fluent programming
+ */
+ public Builder not() {
+ this.operator = BetweenPredicateOperator.NOT_BETWEEN;
+ return this;
+ }
+
+ /**
+ * Creates a new instance of {@code [NOT] BETWEEN} predicate class.
+ *
+ * @return new instance of {@link BetweenPredicate}
+ */
+ public BetweenPredicate build() {
+ return new BetweenPredicate(this);
+ }
+ }
+
+ @Override
+ public void accept(final PredicateVisitor visitor) {
+ visitor.visit(this);
+ }
+
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/ExistsPredicate.java b/src/main/java/com/exasol/sql/expression/predicate/ExistsPredicate.java
new file mode 100644
index 00000000..fe764088
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/ExistsPredicate.java
@@ -0,0 +1,48 @@
+package com.exasol.sql.expression.predicate;
+
+import com.exasol.sql.dql.select.Select;
+
+/**
+ * A class that represents a {@code EXISTS} predicate.
+ */
+// [impl->dsn~predicate-operators~1]
+public class ExistsPredicate extends AbstractPredicate {
+ private final Select selectQuery;
+
+ /**
+ * Creates a new instance of {@link ExistsPredicate}.
+ *
+ * @param selectQuery sub select query
+ */
+ public ExistsPredicate(final Select selectQuery) {
+ super(ExistsPredicateOperator.EXISTS);
+ this.selectQuery = selectQuery;
+ }
+
+ /**
+ * Returns the sub select query in the {@code EXISTS} predicate.
+ *
+ * @return sub select query
+ */
+ public Select getSelectQuery() {
+ return selectQuery;
+ }
+
+ /**
+ * An operator for {@link ExistsPredicate} class.
+ */
+ public enum ExistsPredicateOperator implements PredicateOperator {
+ EXISTS;
+
+ @Override
+ public String toString() {
+ return super.toString().replace("_", " ");
+ }
+ }
+
+ @Override
+ public void accept(final PredicateVisitor visitor) {
+ visitor.visit(this);
+ }
+
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/InPredicate.java b/src/main/java/com/exasol/sql/expression/predicate/InPredicate.java
new file mode 100644
index 00000000..c6e7122b
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/InPredicate.java
@@ -0,0 +1,167 @@
+package com.exasol.sql.expression.predicate;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.exasol.sql.dql.select.Select;
+import com.exasol.sql.expression.ValueExpression;
+
+/**
+ * A class that represents a {@code [NOT] IN} predicate.
+ */
+// [impl->dsn~predicate-operators~1]
+public class InPredicate extends AbstractPredicate {
+ private final ValueExpression expression;
+ private final List operands;
+ private final Select selectQuery;
+
+ private InPredicate(final Builder builder) {
+ super(builder.operator);
+ this.expression = builder.expression;
+ this.operands = builder.operands;
+ this.selectQuery = builder.selectQuery;
+ }
+
+ /**
+ * Checks if {@link InPredicate} has a sub query.
+ *
+ * @return {@code true} if predicate has a sub query, otherwise return {@code false}
+ */
+ public boolean hasSelectQuery() {
+ return selectQuery != null;
+ }
+
+ /**
+ * Returns the left expression in the {@code [NOT] IN} predicate.
+ *
+ * @return expression in predicate
+ */
+ public ValueExpression getExpression() {
+ return expression;
+ }
+
+ /**
+ * Returns the value expressions in the {@code [NOT] IN} predicate.
+ *
+ * @return value expression operands
+ */
+ public List getOperands() {
+ return operands;
+ }
+
+ /**
+ * Returns the sub select query in the {@code [NOT] IN} predicate.
+ *
+ * @return sub select query
+ */
+ public Select getSelectQuery() {
+ return selectQuery;
+ }
+
+ /**
+ * Creates a new builder for {@link InPredicate}.
+ *
+ * @return new {@link Builder}
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A class that represents {@link InPredicate} operator.
+ */
+ public enum InPredicateOperator implements PredicateOperator {
+ IN, NOT_IN;
+
+ @Override
+ public String toString() {
+ return super.toString().replace("_", " ");
+ }
+ }
+
+ /**
+ * A builder for {@link InPredicate}.
+ */
+ public static class Builder {
+ private ValueExpression expression;
+ private List operands = null;
+ private Select selectQuery = null;
+ private InPredicateOperator operator = InPredicateOperator.IN;
+
+ /**
+ * A private constructor to hide the public default.
+ */
+ private Builder() {
+ // intentionally empty
+ }
+
+ /**
+ * Adds the predicate expression.
+ *
+ * @param expression in predicate expression
+ * @return this
for fluent programming
+ */
+ public Builder expression(final ValueExpression expression) {
+ this.expression = expression;
+ return this;
+ }
+
+ /**
+ * Adds the operands.
+ *
+ * @param operands operands for {@code [NOT] IN} predicate
+ * @return this
for fluent programming
+ */
+ public Builder operands(final ValueExpression... operands) {
+ if (this.selectQuery != null) {
+ throw new IllegalArgumentException(getExceptionMessage());
+ }
+ this.operands = Arrays.asList(operands);
+ return this;
+ }
+
+ /**
+ * Adds the sub select query.
+ *
+ * @param select sub select for {@code [NOT] IN} predicate
+ * @return this
for fluent programming
+ */
+ public Builder selectQuery(final Select select) {
+ if (this.operands != null) {
+ throw new IllegalArgumentException(getExceptionMessage());
+ }
+ this.selectQuery = select;
+ return this;
+ }
+
+ private String getExceptionMessage() {
+ return "The '[NOT] IN' predicate cannot have both select query and expressions. "
+ + "Please use only either expressions or sub select query.";
+ }
+
+ /**
+ * Sets {@code NOT IN} predicate.
+ *
+ * @return this
for fluent programming
+ */
+ public Builder not() {
+ this.operator = InPredicateOperator.NOT_IN;
+ return this;
+ }
+
+ /**
+ * Creates a new instance of {@code [NOT] IN} predicate class.
+ *
+ * @return new instance of {@link InPredicate}
+ */
+ public InPredicate build() {
+ return new InPredicate(this);
+ }
+ }
+
+ @Override
+ public void accept(final PredicateVisitor visitor) {
+ visitor.visit(this);
+ }
+
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/IsNullPredicate.java b/src/main/java/com/exasol/sql/expression/predicate/IsNullPredicate.java
new file mode 100644
index 00000000..949bda58
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/IsNullPredicate.java
@@ -0,0 +1,59 @@
+package com.exasol.sql.expression.predicate;
+
+import com.exasol.sql.expression.ValueExpression;
+
+/**
+ * A class that represents a {@code IS [NOT] NULL} predicate.
+ */
+// [impl->dsn~predicate-operators~1]
+public class IsNullPredicate extends AbstractPredicate {
+ private final ValueExpression operand;
+
+ /**
+ * Creates a new instance of {@link IsNullPredicate} for {@code IS NULL} predicate.
+ *
+ * @param operand value expression to check for null
+ */
+ public IsNullPredicate(final ValueExpression operand) {
+ super(IsNullPredicateOperator.IS_NULL);
+ this.operand = operand;
+ }
+
+ /**
+ * Creates a new instance of {@link IsNullPredicate} for {@code IS [NOT] NULL} predicate.
+ *
+ * @param operator predicate operator
+ * @param operand value expression to check for null
+ */
+ public IsNullPredicate(final IsNullPredicateOperator operator, final ValueExpression operand) {
+ super(operator);
+ this.operand = operand;
+ }
+
+ /**
+ * Returns the value expression to be checked for {@code null}.
+ *
+ * @return value expression operand
+ */
+ public ValueExpression getOperand() {
+ return this.operand;
+ }
+
+ /**
+ * An operator for {@link IsNullPredicate} class.
+ */
+ public enum IsNullPredicateOperator implements PredicateOperator {
+ IS_NULL, IS_NOT_NULL;
+
+ @Override
+ public String toString() {
+ return super.toString().replace("_", " ");
+ }
+ }
+
+ @Override
+ public void accept(final PredicateVisitor visitor) {
+ visitor.visit(this);
+ }
+
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/Predicate.java b/src/main/java/com/exasol/sql/expression/predicate/Predicate.java
new file mode 100644
index 00000000..0abbe25c
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/Predicate.java
@@ -0,0 +1,23 @@
+package com.exasol.sql.expression.predicate;
+
+import com.exasol.sql.expression.BooleanExpression;
+
+/**
+ * Interface for classes that implement predicate expressions.
+ */
+public interface Predicate extends BooleanExpression {
+
+ /**
+ * Returns the predicate operator.
+ *
+ * @return predicate operator
+ */
+ public PredicateOperator getOperator();
+
+ /**
+ * Accepts {@link PredicateVisitor}.
+ *
+ * @param visitor predicate visitor to accept
+ */
+ public void accept(PredicateVisitor visitor);
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/PredicateOperator.java b/src/main/java/com/exasol/sql/expression/predicate/PredicateOperator.java
new file mode 100644
index 00000000..3718cb48
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/PredicateOperator.java
@@ -0,0 +1,16 @@
+package com.exasol.sql.expression.predicate;
+
+/**
+ * An interface for the predicate operators.
+ */
+public interface PredicateOperator {
+
+ /**
+ * Returns the predicate operator symbol.
+ *
+ * @return predicate operator symbol
+ */
+ @Override
+ public String toString();
+
+}
diff --git a/src/main/java/com/exasol/sql/expression/predicate/PredicateVisitor.java b/src/main/java/com/exasol/sql/expression/predicate/PredicateVisitor.java
new file mode 100644
index 00000000..eb92ab1e
--- /dev/null
+++ b/src/main/java/com/exasol/sql/expression/predicate/PredicateVisitor.java
@@ -0,0 +1,16 @@
+package com.exasol.sql.expression.predicate;
+
+/**
+ * An interface for {@link Predicate} visitor.
+ */
+public interface PredicateVisitor {
+
+ public void visit(IsNullPredicate isNullPredicate);
+
+ public void visit(InPredicate inPredicate);
+
+ public void visit(ExistsPredicate existsPredicate);
+
+ public void visit(BetweenPredicate betweenPredicate);
+
+}
diff --git a/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java
index 4610001b..815ddcea 100644
--- a/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java
+++ b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java
@@ -1,10 +1,13 @@
package com.exasol.sql.expression.rendering;
+import java.util.Arrays;
import java.util.List;
import com.exasol.datatype.type.DataType;
import com.exasol.sql.ColumnsDefinition;
import com.exasol.sql.UnnamedPlaceholder;
+import com.exasol.sql.dql.select.Select;
+import com.exasol.sql.dql.select.rendering.SelectRenderer;
import com.exasol.sql.expression.*;
import com.exasol.sql.expression.comparison.Comparison;
import com.exasol.sql.expression.comparison.ComparisonVisitor;
@@ -17,6 +20,7 @@
import com.exasol.sql.expression.function.exasol.ExasolFunction;
import com.exasol.sql.expression.function.exasol.ExasolUdf;
import com.exasol.sql.expression.literal.*;
+import com.exasol.sql.expression.predicate.*;
import com.exasol.sql.rendering.ColumnsDefinitionRenderer;
import com.exasol.sql.rendering.StringRendererConfig;
@@ -24,12 +28,12 @@
* Renderer for common value expressions.
*/
public class ValueExpressionRenderer extends AbstractExpressionRenderer implements BooleanExpressionVisitor,
- ComparisonVisitor, ValueExpressionVisitor, LiteralVisitor, FunctionVisitor {
+ ComparisonVisitor, FunctionVisitor, LiteralVisitor, PredicateVisitor, ValueExpressionVisitor {
int nestedLevel = 0;
/**
* Create a new instance of {@link ValueExpressionRenderer}.
- *
+ *
* @param config render configuration
*/
public ValueExpressionRenderer(final StringRendererConfig config) {
@@ -106,7 +110,6 @@ private void openComparison(final Comparison comparison) {
this.builder.append(" ");
this.builder.append(comparison.getOperator().toString());
this.builder.append(" ");
-
appendOperand(comparison.getRightOperand());
}
@@ -114,11 +117,72 @@ private void closeComparison() {
endParenthesisIfNested();
}
+ /* Predicate visitor */
+
+ @Override
+ public void visit(final Predicate predicate) {
+ predicate.accept((PredicateVisitor) this);
+ }
+
+ @Override
+ public void visit(final IsNullPredicate isNullPredicate) {
+ startParenthesisIfNested();
+ appendOperand(isNullPredicate.getOperand());
+ append(" ");
+ append(isNullPredicate.getOperator().toString());
+ endParenthesisIfNested();
+ }
+
+ @Override
+ public void visit(final InPredicate inPredicate) {
+ startParenthesisIfNested();
+ appendOperand(inPredicate.getExpression());
+ append(" ");
+ append(inPredicate.getOperator().toString());
+ append(" (");
+ if (inPredicate.hasSelectQuery()) {
+ appendSelect(inPredicate.getSelectQuery());
+ } else {
+ visit(inPredicate.getOperands());
+ }
+ append(")");
+ endParenthesisIfNested();
+ }
+
+ @Override
+ public void visit(final ExistsPredicate existsPredicate) {
+ startParenthesisIfNested();
+ append(existsPredicate.getOperator().toString());
+ append(" (");
+ appendSelect(existsPredicate.getSelectQuery());
+ append(")");
+ endParenthesisIfNested();
+ }
+
+ @Override
+ public void visit(final BetweenPredicate betweenPredicate) {
+ startParenthesisIfNested();
+ appendOperand(betweenPredicate.getExpression());
+ append(" ");
+ append(betweenPredicate.getOperator().toString());
+ append(" ");
+ appendOperand(betweenPredicate.getStartExpression());
+ appendKeyword(" AND ");
+ appendOperand(betweenPredicate.getEndExpression());
+ endParenthesisIfNested();
+ }
+
+ private void appendSelect(final Select select) {
+ final SelectRenderer selectRenderer = SelectRenderer.create(config);
+ select.accept(selectRenderer);
+ append(selectRenderer.render());
+ }
+
/* Value expression visitor */
- public void visit(final ValueExpression... valueExpression) {
+ public void visit(final List valueExpressions) {
boolean isFirst = true;
- for (final ValueExpression parameter : valueExpression) {
+ for (final ValueExpression parameter : valueExpressions) {
if (!isFirst) {
append(", ");
}
@@ -127,6 +191,10 @@ public void visit(final ValueExpression... valueExpression) {
}
}
+ public void visit(final ValueExpression... valueExpressions) {
+ visit(Arrays.asList(valueExpressions));
+ }
+
@Override
public void visit(final ColumnReference columnReference) {
appendAutoQuoted(columnReference.toString());
diff --git a/src/test/java/com/exasol/sql/expression/rendering/PredicateExpressionRendererTest.java b/src/test/java/com/exasol/sql/expression/rendering/PredicateExpressionRendererTest.java
new file mode 100644
index 00000000..579eeccb
--- /dev/null
+++ b/src/test/java/com/exasol/sql/expression/rendering/PredicateExpressionRendererTest.java
@@ -0,0 +1,146 @@
+package com.exasol.sql.expression.rendering;
+
+import static com.exasol.hamcrest.ValueExpressionRenderResultMatcher.rendersTo;
+import static com.exasol.hamcrest.ValueExpressionRenderResultMatcher.rendersWithConfigTo;
+import static com.exasol.sql.expression.BooleanTerm.*;
+import static com.exasol.sql.expression.ExpressionTerm.*;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dql.select.Select;
+import com.exasol.sql.expression.BooleanExpression;
+import com.exasol.sql.expression.ValueExpression;
+import com.exasol.sql.expression.literal.Literal;
+import com.exasol.sql.expression.predicate.InPredicate;
+import com.exasol.sql.rendering.StringRendererConfig;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+// [utest->dsn~predicate-operators~1]
+class PredicateExpressionRendererTest {
+ private Select select;
+
+ @BeforeEach
+ void beforeEach() {
+ select = StatementFactory.getInstance().select();
+ select.from().table("test");
+ }
+
+ @Test
+ void testIsNullPredicate() {
+ assertThat(isNull(stringLiteral("e")), rendersTo("'e' IS NULL"));
+ }
+
+ @Test
+ void testIsNotNullPredicate() {
+ assertThat(isNotNull(stringLiteral("e")), rendersTo("'e' IS NOT NULL"));
+ }
+
+ @Test
+ void testColumnIsNullPredicate() {
+ assertThat(isNull(column("c")), rendersTo("c IS NULL"));
+ }
+
+ @Test
+ void testExpressionIsNullPredicate() {
+ final ValueExpression expr = plus(integerLiteral(1), integerLiteral(1));
+ assertThat(isNull(expr), rendersTo("(1+1) IS NULL"));
+ }
+
+ @Test
+ void testNestedIsNullPredicate() {
+ final BooleanExpression expr = and(isNull(stringLiteral("a")), isNotNull(stringLiteral("b")));
+ assertThat(expr, rendersTo("('a' IS NULL) AND ('b' IS NOT NULL)"));
+ }
+
+ @Test
+ void testIsNullPredicateWithConfig() {
+ final StringRendererConfig config = StringRendererConfig.builder().lowerCase(true).build();
+ assertThat(isNotNull(not(true)), rendersWithConfigTo(config, "not(true) IS NOT NULL"));
+ }
+
+ @Test
+ void testInPredicate() {
+ final BooleanExpression inPredicate = in(stringLiteral("e"), integerLiteral(1), integerLiteral(2));
+ assertThat(inPredicate, rendersTo("'e' IN (1, 2)"));
+ }
+
+ @Test
+ void testNotInPredicate() {
+ final BooleanExpression inPredicate = notIn(stringLiteral("e"), integerLiteral(3));
+ assertThat(inPredicate, rendersTo("'e' NOT IN (3)"));
+ }
+
+ @Test
+ void testNestedInPredicate() {
+ final BooleanExpression expr = or(in(stringLiteral("a"), booleanLiteral(true), booleanLiteral(false)),
+ notIn(stringLiteral("b"), integerLiteral(13)));
+ assertThat(expr, rendersTo("('a' IN (TRUE, FALSE)) OR ('b' NOT IN (13))"));
+ }
+
+ @Test
+ void testInPredicateWithSelect() {
+ final BooleanExpression inPredicate = in(stringLiteral("e"), select.all().limit(2));
+ assertThat(inPredicate, rendersTo("'e' IN (SELECT * FROM test LIMIT 2)"));
+ }
+
+ @Test
+ void testNotInPredicateWithSelect() {
+ final BooleanExpression inPredicate = notIn(integerLiteral(5), select.field("id"));
+ assertThat(inPredicate, rendersTo("5 NOT IN (SELECT id FROM test)"));
+ }
+
+ @Test
+ void testInPredicateBothExpressionAndSelectException() {
+ final InPredicate.Builder builder = InPredicate.builder().expression(integerLiteral(1))
+ .operands(integerLiteral(2));
+ final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> builder.selectQuery(select));
+ assertThat(exception.getMessage(),
+ containsString("The '[NOT] IN' predicate cannot have both select query and expressions"));
+ }
+
+ @Test
+ void testInPredicateBothSelectAndExpressionException() {
+ final InPredicate.Builder builder = InPredicate.builder().expression(integerLiteral(1)).selectQuery(select);
+ final Literal operand = stringLiteral("a");
+ final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> builder.operands(operand));
+ assertThat(exception.getMessage(),
+ containsString("The '[NOT] IN' predicate cannot have both select query and expressions"));
+ }
+
+ @Test
+ void testExistsPredicate() {
+ assertThat(exists(select.all()), rendersTo("EXISTS (SELECT * FROM test)"));
+ }
+
+ @Test
+ void testNestedExistsPredicate() {
+ final BooleanExpression expr = or(not(true), exists(select.field("id")));
+ assertThat(expr, rendersTo("NOT(TRUE) OR (EXISTS (SELECT id FROM test))"));
+ }
+
+ @Test
+ void testBetweenPredicate() {
+ final BooleanExpression expr = between(integerLiteral(2), integerLiteral(1), integerLiteral(3));
+ assertThat(expr, rendersTo("2 BETWEEN 1 AND 3"));
+ }
+
+ @Test
+ void testNotBetweenPredicate() {
+ final BooleanExpression expr = notBetween(stringLiteral("c"), stringLiteral("a"), stringLiteral("b"));
+ assertThat(expr, rendersTo("'c' NOT BETWEEN 'a' AND 'b'"));
+ }
+
+ @Test
+ void testNestedBetweenPredicate() {
+ final BooleanExpression expr = and(between(integerLiteral(2), integerLiteral(1), integerLiteral(3)),
+ notBetween(stringLiteral("c"), stringLiteral("a"), stringLiteral("b")));
+ assertThat(expr, rendersTo("(2 BETWEEN 1 AND 3) AND ('c' NOT BETWEEN 'a' AND 'b')"));
+ }
+
+}
diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties
new file mode 100644
index 00000000..8c97abe9
--- /dev/null
+++ b/src/test/resources/logging.properties
@@ -0,0 +1,6 @@
+handlers=java.util.logging.ConsoleHandler
+.level=INFO
+java.util.logging.ConsoleHandler.level=ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+java.util.logging.SimpleFormatter.format=%1$tF %1$tT.%1$tL [%4$-7s] %5$s %n
+com.exasol.level=ALL
diff --git a/versionsMavenPluginRules.xml b/versionsMavenPluginRules.xml
index c566b427..35bd03d2 100644
--- a/versionsMavenPluginRules.xml
+++ b/versionsMavenPluginRules.xml
@@ -4,13 +4,14 @@
xsi:schemaLocation="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0 http://mojo.codehaus.org/versions-maven-plugin/xsd/rule-2.0.0.xsd">
- (?i).*Alpha(?:-?\d+)?
- (?i).*a(?:-?\d+)?
- (?i).*Beta(?:-?\d+)?
- (?i).*-B(?:-?\d+)?
- (?i).*RC(?:-?\d+)?
- (?i).*CR(?:-?\d+)?
- (?i).*M(?:-?\d+)?
+ (?i).*Alpha(?:-?[\d.]+)?
+ (?i).*a(?:-?[\d.]+)?
+ (?i).*Beta(?:-?[\d.]+)?
+ (?i).*-B(?:-?[\d.]+)?
+ (?i).*-b(?:-?[\d.]+)?
+ (?i).*RC(?:-?[\d.]+)?
+ (?i).*CR(?:-?[\d.]+)?
+ (?i).*M(?:-?[\d.]+)?