From e9499ae8713dcba05e53117098c3e237341095b9 Mon Sep 17 00:00:00 2001 From: Muhammet Orazov Date: Thu, 4 Mar 2021 10:15:40 +0100 Subject: [PATCH] #104: Added additional predicates (#105) Added more Exasol predicates: - `IS [NOT] NULL` - `[NOT] IN` - `EXISTS` - `[NOT] BETWEEN` Fixes #104. --- .../{maven.yml => dependencies_check.yml} | 10 +- .github/workflows/maven_central_release.yml | 7 + doc/changes/changelog.md | 4 +- doc/changes/changes_4.3.1.md | 10 -- doc/changes/changes_4.4.0.md | 34 ++++ doc/design.md | 11 ++ doc/system_requirements.md | 66 ++++--- pom.xml | 107 ++++++++++- .../expression/BooleanExpressionVisitor.java | 5 +- .../exasol/sql/expression/BooleanTerm.java | 52 +++++- .../predicate/AbstractPredicate.java | 36 ++++ .../predicate/BetweenPredicate.java | 143 +++++++++++++++ .../expression/predicate/ExistsPredicate.java | 48 +++++ .../sql/expression/predicate/InPredicate.java | 167 ++++++++++++++++++ .../expression/predicate/IsNullPredicate.java | 59 +++++++ .../sql/expression/predicate/Predicate.java | 23 +++ .../predicate/PredicateOperator.java | 16 ++ .../predicate/PredicateVisitor.java | 16 ++ .../rendering/ValueExpressionRenderer.java | 78 +++++++- .../PredicateExpressionRendererTest.java | 146 +++++++++++++++ src/test/resources/logging.properties | 6 + versionsMavenPluginRules.xml | 15 +- 22 files changed, 997 insertions(+), 62 deletions(-) rename .github/workflows/{maven.yml => dependencies_check.yml} (61%) delete mode 100644 doc/changes/changes_4.3.1.md create mode 100644 doc/changes/changes_4.4.0.md create mode 100644 src/main/java/com/exasol/sql/expression/predicate/AbstractPredicate.java create mode 100644 src/main/java/com/exasol/sql/expression/predicate/BetweenPredicate.java create mode 100644 src/main/java/com/exasol/sql/expression/predicate/ExistsPredicate.java create mode 100644 src/main/java/com/exasol/sql/expression/predicate/InPredicate.java create mode 100644 src/main/java/com/exasol/sql/expression/predicate/IsNullPredicate.java create mode 100644 src/main/java/com/exasol/sql/expression/predicate/Predicate.java create mode 100644 src/main/java/com/exasol/sql/expression/predicate/PredicateOperator.java create mode 100644 src/main/java/com/exasol/sql/expression/predicate/PredicateVisitor.java create mode 100644 src/test/java/com/exasol/sql/expression/rendering/PredicateExpressionRendererTest.java create mode 100644 src/test/resources/logging.properties 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.]+)?