From 55f77548be7347e82bd96828cd0c691c1f9bc1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 18 Sep 2018 17:30:29 +0200 Subject: [PATCH 01/43] PMI-66: Initial statement builder. --- .classpath | 27 ++++++++ .gitignore | 7 +++ .project | 18 ++++++ .settings/org.eclipse.core.resources.prefs | 4 ++ .settings/org.eclipse.jdt.core.prefs | 6 ++ README.md | 33 +++++++++- doc/system_requirements.md | 2 + launch/sql-statment-builder all tests.launch | 26 ++++++++ model/diagrams/cl_fragments.plantuml | 17 +++++ pom.xml | 53 ++++++++++++++++ .../com/exasol/sql/AbstractFragement.java | 63 +++++++++++++++++++ src/main/java/com/exasol/sql/Fragment.java | 24 +++++++ .../java/com/exasol/sql/FragmentVisitor.java | 14 +++++ .../java/com/exasol/sql/SqlStatement.java | 8 +++ src/main/java/com/exasol/sql/dql/Field.java | 25 ++++++++ .../com/exasol/sql/dql/FieldDefinition.java | 7 +++ src/main/java/com/exasol/sql/dql/Select.java | 39 ++++++++++++ .../com/exasol/sql/dql/StatementFactory.java | 33 ++++++++++ .../exasol/sql/dql/StringRendererConfig.java | 48 ++++++++++++++ .../com/exasol/sql/dql/TableExpression.java | 15 +++++ .../exasol/sql/rendering/StringRenderer.java | 56 +++++++++++++++++ src/test/java/com/exasol/dql/TestSelect.java | 59 +++++++++++++++++ 22 files changed, 583 insertions(+), 1 deletion(-) create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/org.eclipse.core.resources.prefs create mode 100644 .settings/org.eclipse.jdt.core.prefs create mode 100644 doc/system_requirements.md create mode 100644 launch/sql-statment-builder all tests.launch create mode 100644 model/diagrams/cl_fragments.plantuml create mode 100644 pom.xml create mode 100644 src/main/java/com/exasol/sql/AbstractFragement.java create mode 100644 src/main/java/com/exasol/sql/Fragment.java create mode 100644 src/main/java/com/exasol/sql/FragmentVisitor.java create mode 100644 src/main/java/com/exasol/sql/SqlStatement.java create mode 100644 src/main/java/com/exasol/sql/dql/Field.java create mode 100644 src/main/java/com/exasol/sql/dql/FieldDefinition.java create mode 100644 src/main/java/com/exasol/sql/dql/Select.java create mode 100644 src/main/java/com/exasol/sql/dql/StatementFactory.java create mode 100644 src/main/java/com/exasol/sql/dql/StringRendererConfig.java create mode 100644 src/main/java/com/exasol/sql/dql/TableExpression.java create mode 100644 src/main/java/com/exasol/sql/rendering/StringRenderer.java create mode 100644 src/test/java/com/exasol/dql/TestSelect.java diff --git a/.classpath b/.classpath new file mode 100644 index 00000000..962da8d7 --- /dev/null +++ b/.classpath @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..357db2cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/bin/ +/target/ +**/*.md.html +**/*.bak +**/*.swp +**/*.log +**/*.out \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 00000000..547c8446 --- /dev/null +++ b/.project @@ -0,0 +1,18 @@ + + + sql-statement-builder + This module provides a Builder for SQL statements that helps creating the correct structure and validates variable parts of the statements. NO_M2ECLIPSE_SUPPORT: Project files created with the maven-eclipse-plugin are not supported in M2Eclipse. + + + + org.eclipse.jdt.core.javabuilder + + + org.eclipse.m2e.core.maven2Builder + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + \ No newline at end of file diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..f9fe3459 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..b8947ec6 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/README.md b/README.md index 3ae35b3a..aa1b2e0c 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ -# sql-statement-builder \ No newline at end of file +# sql-statement-builder + +## Usage + +```java +SqlStatement statement = StatementFactory.getInstance().select().all().from("foo.bar"); + +SqlStatement statement = StatementFactory.getInstance() + .select() + .field("name") + .from("bar") + .join("zoo").on("zoo.bar_id").eq("bar.id") + + + +``` + +## Development + +The following sub-sections provide information about building and extending the project. + +### Build Time Dependencies + +The list below show all build time dependencies in alphabetical order. Note that except the Maven build tool all required modules are downloaded automatically by Maven. + +| Dependency | Purpose | License | +------------------------------------------------------------|--------------------------------------------------------|-------------------------------- +| [Apache Maven](https://maven.apache.org/) | Build tool | Apache License 2.0 | +| [Equals Verifier](https://github.com/jqno/equalsverifier) | Automatic contract checker for `equals()` and `hash()` | Apache License 2.0 | +| [Hamcrest](http://hamcrest.org/) | Advanced matchers for JUnit | GNU BSD-3-Clause | +| [JUnit 5](https://junit.org/junit5/) | Unit testing framework | Eclipse Public License 1.0 | +| [Mockito](http://site.mockito.org/) | Mocking framework | MIT License | \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md new file mode 100644 index 00000000..ae4b6d15 --- /dev/null +++ b/doc/system_requirements.md @@ -0,0 +1,2 @@ +* Upper case / lower case +* One line / pretty \ No newline at end of file diff --git a/launch/sql-statment-builder all tests.launch b/launch/sql-statment-builder all tests.launch new file mode 100644 index 00000000..c0bc8ef8 --- /dev/null +++ b/launch/sql-statment-builder all tests.launch @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/diagrams/cl_fragments.plantuml b/model/diagrams/cl_fragments.plantuml new file mode 100644 index 00000000..40121cca --- /dev/null +++ b/model/diagrams/cl_fragments.plantuml @@ -0,0 +1,17 @@ +@startuml +hide empty methods +hide empty attributes +skinparam style strictuml +!pragma horizontalLineBetweenDifferentPackageAllowed + +interface Fragment <> +interface FieldDefinition <> +class Select +class Field + +FieldDefinition -u-> Fragment +Field "1..*" -l-> Select : parent + +Field -u-> FieldDefinition +Select -u-> Fragment +@enduml \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..ae43a0e6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + sql-statement-builder + sql-statement-builder + 0.1.0 + Exasol SQL Statement Builder + This module provides a Builder for SQL statements that helps creating the correct structure and validates variable parts of the statements. + + UTF-8 + 1.8 + 5.3.1 + 1.3.1 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.hamcrest + hamcrest-core + 1.3 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + + \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/AbstractFragement.java b/src/main/java/com/exasol/sql/AbstractFragement.java new file mode 100644 index 00000000..d6b368ae --- /dev/null +++ b/src/main/java/com/exasol/sql/AbstractFragement.java @@ -0,0 +1,63 @@ +package com.exasol.sql; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides an abstract base for SQL statement fragments. It also + * keeps track of the relationships to other fragments. + * + * @param the type of the concrete class implementing the missing parts. + */ +public abstract class AbstractFragement implements Fragment { + private final Fragment root; + protected final Fragment parent; + protected final List children = new ArrayList<>(); + + protected AbstractFragement(final Fragment parent) { + if (parent == null) { + this.root = this; + } else { + this.root = parent.getRoot(); + } + this.parent = parent; + } + + @Override + public Fragment getRoot() { + return this.root; + } + + @Override + public Fragment getParent() { + return this.parent; + } + + protected void addChild(final Fragment child) { + this.children.add(child); + } + + protected List getChildren() { + return this.children; + } + + @Override + public Fragment getChild(final int index) { + return this.children.get(index); + } + + @Override + public boolean isFirstSibling() { + return (this.parent != null) && (this.getParent().getChild(0) == this); + } + + @Override + public void accept(final FragmentVisitor visitor) { + acceptConcrete(visitor); + for (final Fragment child : getChildren()) { + child.accept(visitor); + } + } + + protected abstract void acceptConcrete(final FragmentVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/Fragment.java b/src/main/java/com/exasol/sql/Fragment.java new file mode 100644 index 00000000..77544c93 --- /dev/null +++ b/src/main/java/com/exasol/sql/Fragment.java @@ -0,0 +1,24 @@ +package com.exasol.sql; + +public interface Fragment { + @Override + public String toString(); + + public Fragment getParent(); + + public void accept(FragmentVisitor visitor); + + public Fragment getRoot(); + + public boolean isFirstSibling(); + + /** + * Get child at index position + * + * @param index position of the child + * @return child at index + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || + * index >= size()) + */ + public Fragment getChild(int index) throws IndexOutOfBoundsException; +} diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java new file mode 100644 index 00000000..f73b140e --- /dev/null +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -0,0 +1,14 @@ +package com.exasol.sql; + +import com.exasol.sql.dql.*; + +/** + * This interface represents a visitor for SQL statement fragments. + */ +public interface FragmentVisitor { + public void visit(final Select select); + + public void visit(final Field field); + + public void visit(final TableExpression tableExpression); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/SqlStatement.java b/src/main/java/com/exasol/sql/SqlStatement.java new file mode 100644 index 00000000..bdbceaf6 --- /dev/null +++ b/src/main/java/com/exasol/sql/SqlStatement.java @@ -0,0 +1,8 @@ +package com.exasol.sql; + +/** + * This interface represents an SQL statement. + */ +public interface SqlStatement extends Fragment { + +} diff --git a/src/main/java/com/exasol/sql/dql/Field.java b/src/main/java/com/exasol/sql/dql/Field.java new file mode 100644 index 00000000..b10ac338 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Field.java @@ -0,0 +1,25 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +public class Field extends AbstractFragement implements FieldDefinition { + private final String name; + + protected Field(final Fragment parent, final String name) { + super(parent); + this.name = name; + } + + public String getName() { + return this.name; + } + + public static Field all(final Fragment parent) { + return new Field(parent, "*"); + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } +} diff --git a/src/main/java/com/exasol/sql/dql/FieldDefinition.java b/src/main/java/com/exasol/sql/dql/FieldDefinition.java new file mode 100644 index 00000000..be560c0a --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/FieldDefinition.java @@ -0,0 +1,7 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.Fragment; + +public interface FieldDefinition extends Fragment { + +} diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java new file mode 100644 index 00000000..f901ceee --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -0,0 +1,39 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +/** + * This class implements an SQL {@link Select} statement + */ +public class Select extends AbstractFragement implements SqlStatement { + public Select(final Fragment parent) { + super(parent); + } + + @Override + public String toString() { + return "SELECT"; + } + + /** + * Create a wildcard field for all involved fields. + * + * @return this instance for fluent programming + */ + public Select all() { + addChild(Field.all(this)); + return this; + } + + public Select field(final String... names) { + for (final String name : names) { + addChild(new Field(this, name)); + } + return this; + } + + @Override + public void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } +} diff --git a/src/main/java/com/exasol/sql/dql/StatementFactory.java b/src/main/java/com/exasol/sql/dql/StatementFactory.java new file mode 100644 index 00000000..23491300 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/StatementFactory.java @@ -0,0 +1,33 @@ +package com.exasol.sql.dql; + +/** + * The {@link StatementFactory} implements an factory for SQL statements. + */ +public final class StatementFactory { + private static StatementFactory instance; + + /** + * Get an instance of a {@link StatementFactory} + * + * @return the existing instance otherwise creates one. + */ + public static synchronized StatementFactory getInstance() { + if (instance == null) { + instance = new StatementFactory(); + } + return instance; + } + + private StatementFactory() { + // prevent instantiation outside singleton + } + + /** + * Create a {@link Select} statement + * + * @return a new instance of a {@link Select} statement + */ + public Select select() { + return new Select(null); + } +} diff --git a/src/main/java/com/exasol/sql/dql/StringRendererConfig.java b/src/main/java/com/exasol/sql/dql/StringRendererConfig.java new file mode 100644 index 00000000..b3591c79 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/StringRendererConfig.java @@ -0,0 +1,48 @@ +package com.exasol.sql.dql; + +/** + * This class implements a parameter object containing the configuration options + * for the {@link StatementFactory}. + */ +public class StringRendererConfig { + private final boolean lowerCase; + + private StringRendererConfig(final boolean lowerCase) { + this.lowerCase = lowerCase; + } + + /** + * Get whether the statements should be produced in lower case. + * + * @return true if statements are produced in lower case + */ + public boolean produceLowerCase() { + return this.lowerCase; + } + + /** + * Builder for {@link StringRendererConfig} + */ + public static class Builder { + private boolean lowerCase = false; + + /** + * Create a new instance of a {@link StringRendererConfig} + * + * @return new instance + */ + public StringRendererConfig build() { + return new StringRendererConfig(this.lowerCase); + } + + /** + * Define whether the statement should be produced in lower case + * + * @param lowerCase set to true if the statement should be produced + * in lower case + */ + public void lowerCase(final boolean lowerCase) { + this.lowerCase = lowerCase; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/TableExpression.java b/src/main/java/com/exasol/sql/dql/TableExpression.java new file mode 100644 index 00000000..f54b43a5 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/TableExpression.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +public class TableExpression extends AbstractFragement { + + public TableExpression(final Fragment parent) { + super(parent); + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } +} diff --git a/src/main/java/com/exasol/sql/rendering/StringRenderer.java b/src/main/java/com/exasol/sql/rendering/StringRenderer.java new file mode 100644 index 00000000..42d8fff3 --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/StringRenderer.java @@ -0,0 +1,56 @@ +package com.exasol.sql.rendering; + +import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.dql.*; + +/** + * The {@link StringRenderer} turns SQL statement structures in to SQL strings. + */ +public class StringRenderer implements FragmentVisitor { + private final StringBuilder builder = new StringBuilder(); + private final StringRendererConfig config; + + /** + * Create a new {@link StringRenderer} using the default + * {@link StringRendererConfig}. + */ + public StringRenderer() { + this.config = new StringRendererConfig.Builder().build(); + } + + /** + * Create a new {@link StringRenderer} with custom render settings. + * + * @param config render configuration settings + */ + public StringRenderer(final StringRendererConfig config) { + this.config = config; + } + + @Override + public void visit(final Select select) { + this.builder.append(this.config.produceLowerCase() ? "select" : "SELECT"); + } + + @Override + public void visit(final Field field) { + if (!field.isFirstSibling()) { + this.builder.append(","); + } + this.builder.append(" "); + this.builder.append(field.getName()); + } + + @Override + public void visit(final TableExpression tableExpression) { + } + + /** + * Render an SQL statement to a string. + * + * @return rendered string + */ + public String render() { + return this.builder.toString(); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/TestSelect.java new file mode 100644 index 00000000..669b86df --- /dev/null +++ b/src/test/java/com/exasol/dql/TestSelect.java @@ -0,0 +1,59 @@ +package com.exasol.dql; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.exasol.sql.Fragment; +import com.exasol.sql.dql.StatementFactory; +import com.exasol.sql.dql.StringRendererConfig; +import com.exasol.sql.rendering.StringRenderer; + +class TestSelect { + private StringRenderer renderer; + + @BeforeEach + void beforeEach() { + this.renderer = new StringRenderer(); + } + + @Test + void testGetParentReturnsNull() { + assertThat(StatementFactory.getInstance().select().getParent(), nullValue()); + } + + @Test + void testEmptySelect() { + final Fragment fragment = StatementFactory.getInstance().select(); + assertFragmentRenderedTo(fragment, "SELECT"); + } + + private void assertFragmentRenderedTo(final Fragment fragment, final String expected) { + fragment.getRoot().accept(this.renderer); + assertThat(this.renderer.render(), equalTo(expected)); + } + + @Test + void testEmptySelectLowerCase() { + final StringRendererConfig.Builder builder = new StringRendererConfig.Builder(); + builder.lowerCase(true); + this.renderer = new StringRenderer(builder.build()); + final Fragment fragment = StatementFactory.getInstance().select(); + assertFragmentRenderedTo(fragment, "select"); + } + + @Test + void testSelectAll() { + final Fragment fragment = StatementFactory.getInstance().select().all(); + assertFragmentRenderedTo(fragment, "SELECT *"); + } + + @Test + void testSelectFieldNames() { + final Fragment fragment = StatementFactory.getInstance().select().field("a", "b"); + assertFragmentRenderedTo(fragment, "SELECT a, b"); + } +} \ No newline at end of file From 3a5ecbf0fc83f1babdd830478f6918b359ad4cbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 19 Sep 2018 08:57:19 +0200 Subject: [PATCH 02/43] PMI-66: Added basic FROM clause. --- model/diagrams/cl_fragments.plantuml | 21 ++++++---- .../java/com/exasol/sql/FragmentVisitor.java | 6 ++- .../java/com/exasol/sql/dql/FromClause.java | 17 ++++++++ src/main/java/com/exasol/sql/dql/Select.java | 14 ++++++- src/main/java/com/exasol/sql/dql/Table.java | 35 +++++++++++++++++ .../com/exasol/sql/dql/TableExpression.java | 15 ------- .../com/exasol/sql/dql/TableReference.java | 6 +++ .../exasol/sql/rendering/StringRenderer.java | 39 +++++++++++++------ src/test/java/com/exasol/dql/TestSelect.java | 20 ++++++---- 9 files changed, 129 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/exasol/sql/dql/FromClause.java create mode 100644 src/main/java/com/exasol/sql/dql/Table.java delete mode 100644 src/main/java/com/exasol/sql/dql/TableExpression.java create mode 100644 src/main/java/com/exasol/sql/dql/TableReference.java diff --git a/model/diagrams/cl_fragments.plantuml b/model/diagrams/cl_fragments.plantuml index 40121cca..065e34dc 100644 --- a/model/diagrams/cl_fragments.plantuml +++ b/model/diagrams/cl_fragments.plantuml @@ -4,14 +4,19 @@ hide empty attributes skinparam style strictuml !pragma horizontalLineBetweenDifferentPackageAllowed -interface Fragment <> -interface FieldDefinition <> -class Select -class Field +together { + interface Fragment <> + interface FieldDefinition <> + interface TableReference <> +} -FieldDefinition -u-> Fragment -Field "1..*" -l-> Select : parent +FieldDefinition -u-|> Fragment +Field .u.|> FieldDefinition +Select .u.|> Fragment +TableReference -u-|> Fragment -Field -u-> FieldDefinition -Select -u-> Fragment +Select *-d- "1..*" Field +Select *-d- FromClause +FromClause *-d- "1..*" TableReference +Table .u.|> TableReference @enduml \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index f73b140e..bc97767e 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -10,5 +10,9 @@ public interface FragmentVisitor { public void visit(final Field field); - public void visit(final TableExpression tableExpression); + public void visit(FromClause fromClause); + + public void visit(TableReference tableReference); + + public void visit(Table table); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java new file mode 100644 index 00000000..048b6eae --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -0,0 +1,17 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +public class FromClause extends AbstractFragement { + public FromClause(final Fragment parent, final String... names) { + super(parent); + for (final String name : names) { + addChild(new Table(this, name)); + } + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } +} diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index f901ceee..62226dc3 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -36,4 +36,16 @@ public Select field(final String... names) { public void acceptConcrete(final FragmentVisitor visitor) { visitor.visit(this); } -} + + /** + * Add a {@link FromClause} to the statement with table names + * + * @param names table reference names + * @return the FROM clause + */ + public FromClause from(final String... names) { + final FromClause from = new FromClause(this, names); + addChild(from); + return from; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Table.java b/src/main/java/com/exasol/sql/dql/Table.java new file mode 100644 index 00000000..bd77522f --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Table.java @@ -0,0 +1,35 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +/** + * This class represents a {@link Table} in an SQL Statement + */ +public class Table extends AbstractFragement implements TableReference { + private final String name; + + /** + * Create a new {@link Table} + * + * @param parent parent SQL fragment + * @param name table name + */ + public Table(final Fragment parent, final String name) { + super(parent); + this.name = name; + } + + /** + * Get the name of the table + * + * @return table name + */ + public String getName() { + return this.name; + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/TableExpression.java b/src/main/java/com/exasol/sql/dql/TableExpression.java deleted file mode 100644 index f54b43a5..00000000 --- a/src/main/java/com/exasol/sql/dql/TableExpression.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.exasol.sql.dql; - -import com.exasol.sql.*; - -public class TableExpression extends AbstractFragement { - - public TableExpression(final Fragment parent) { - super(parent); - } - - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } -} diff --git a/src/main/java/com/exasol/sql/dql/TableReference.java b/src/main/java/com/exasol/sql/dql/TableReference.java new file mode 100644 index 00000000..c9cd0f4e --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/TableReference.java @@ -0,0 +1,6 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.Fragment; + +public interface TableReference extends Fragment { +} diff --git a/src/main/java/com/exasol/sql/rendering/StringRenderer.java b/src/main/java/com/exasol/sql/rendering/StringRenderer.java index 42d8fff3..2dd970b5 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/StringRenderer.java @@ -1,5 +1,6 @@ package com.exasol.sql.rendering; +import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.dql.*; @@ -27,6 +28,15 @@ public StringRenderer(final StringRendererConfig config) { this.config = config; } + /** + * Render an SQL statement to a string. + * + * @return rendered string + */ + public String render() { + return this.builder.toString(); + } + @Override public void visit(final Select select) { this.builder.append(this.config.produceLowerCase() ? "select" : "SELECT"); @@ -34,23 +44,30 @@ public void visit(final Select select) { @Override public void visit(final Field field) { - if (!field.isFirstSibling()) { - this.builder.append(","); - } + appendCommaWhenNeeded(field); this.builder.append(" "); this.builder.append(field.getName()); } + private void appendCommaWhenNeeded(final Fragment fragment) { + if (!fragment.isFirstSibling()) { + this.builder.append(","); + } + } + @Override - public void visit(final TableExpression tableExpression) { + public void visit(final FromClause fromClause) { + this.builder.append(this.config.produceLowerCase() ? " from" : " FROM"); } - /** - * Render an SQL statement to a string. - * - * @return rendered string - */ - public String render() { - return this.builder.toString(); + @Override + public void visit(final TableReference tableReference) { + } + + @Override + public void visit(final Table table) { + appendCommaWhenNeeded(table); + this.builder.append(" "); + this.builder.append(table.getName()); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/TestSelect.java index 669b86df..31709c44 100644 --- a/src/test/java/com/exasol/dql/TestSelect.java +++ b/src/test/java/com/exasol/dql/TestSelect.java @@ -27,8 +27,7 @@ void testGetParentReturnsNull() { @Test void testEmptySelect() { - final Fragment fragment = StatementFactory.getInstance().select(); - assertFragmentRenderedTo(fragment, "SELECT"); + assertFragmentRenderedTo(StatementFactory.getInstance().select(), "SELECT"); } private void assertFragmentRenderedTo(final Fragment fragment, final String expected) { @@ -41,19 +40,24 @@ void testEmptySelectLowerCase() { final StringRendererConfig.Builder builder = new StringRendererConfig.Builder(); builder.lowerCase(true); this.renderer = new StringRenderer(builder.build()); - final Fragment fragment = StatementFactory.getInstance().select(); - assertFragmentRenderedTo(fragment, "select"); + assertFragmentRenderedTo(StatementFactory.getInstance().select(), "select"); } @Test void testSelectAll() { - final Fragment fragment = StatementFactory.getInstance().select().all(); - assertFragmentRenderedTo(fragment, "SELECT *"); + assertFragmentRenderedTo(StatementFactory.getInstance().select().all(), // + "SELECT *"); } @Test void testSelectFieldNames() { - final Fragment fragment = StatementFactory.getInstance().select().field("a", "b"); - assertFragmentRenderedTo(fragment, "SELECT a, b"); + assertFragmentRenderedTo(StatementFactory.getInstance().select().field("a", "b"), // + "SELECT a, b"); + } + + @Test + void testSelectFromTable() { + assertFragmentRenderedTo(StatementFactory.getInstance().select().all().from("table"), // + "SELECT * FROM table"); } } \ No newline at end of file From e6fee55b97bfa7ab085c5069a68f505005c75a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 19 Sep 2018 12:58:12 +0200 Subject: [PATCH 03/43] PMI-66: Support basic JOIN --- .../{ => class}/cl_fragments.plantuml | 5 +- model/diagrams/exasol.skin | 21 ++++++ .../java/com/exasol/sql/FragmentVisitor.java | 2 + .../java/com/exasol/sql/dql/FromClause.java | 34 +++++++-- src/main/java/com/exasol/sql/dql/Join.java | 48 +++++++++++++ src/main/java/com/exasol/sql/dql/Select.java | 22 ++++-- .../exasol/sql/dql/StringRendererConfig.java | 4 +- src/main/java/com/exasol/sql/dql/Table.java | 19 +++++ .../exasol/sql/rendering/StringRenderer.java | 25 ++++++- src/test/java/com/exasol/dql/TestJoin.java | 18 +++++ src/test/java/com/exasol/dql/TestSelect.java | 59 ++++++++------- .../exasol/hamcrest/RenderResultMatcher.java | 72 +++++++++++++++++++ 12 files changed, 285 insertions(+), 44 deletions(-) rename model/diagrams/{ => class}/cl_fragments.plantuml (75%) create mode 100644 model/diagrams/exasol.skin create mode 100644 src/main/java/com/exasol/sql/dql/Join.java create mode 100644 src/test/java/com/exasol/dql/TestJoin.java create mode 100644 src/test/java/com/exasol/hamcrest/RenderResultMatcher.java diff --git a/model/diagrams/cl_fragments.plantuml b/model/diagrams/class/cl_fragments.plantuml similarity index 75% rename from model/diagrams/cl_fragments.plantuml rename to model/diagrams/class/cl_fragments.plantuml index 065e34dc..0dbc4c44 100644 --- a/model/diagrams/cl_fragments.plantuml +++ b/model/diagrams/class/cl_fragments.plantuml @@ -1,8 +1,5 @@ @startuml -hide empty methods -hide empty attributes -skinparam style strictuml -!pragma horizontalLineBetweenDifferentPackageAllowed +!include ../exasol.skin together { interface Fragment <> diff --git a/model/diagrams/exasol.skin b/model/diagrams/exasol.skin new file mode 100644 index 00000000..3085cae0 --- /dev/null +++ b/model/diagrams/exasol.skin @@ -0,0 +1,21 @@ +hide empty methods +hide empty attributes +skinparam style strictuml +!pragma horizontalLineBetweenDifferentPackageAllowed + +skinparam Arrow { + Color 093e52 + FontColor 093e52 +} + +skinparam Class { + BackgroundColor fffff + FontColor 093e52 + FontStyle bold + BorderColor 093e52 + BackgroundColor<> 00b09b + FontColor<> ffffff + StereotypeFontColor<> ffffff +} + +skinparam padding 5 \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index bc97767e..b05c4a38 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -15,4 +15,6 @@ public interface FragmentVisitor { public void visit(TableReference tableReference); public void visit(Table table); + + public void visit(Join join); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index 048b6eae..41b3bd8c 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -3,15 +3,39 @@ import com.exasol.sql.*; public class FromClause extends AbstractFragement { - public FromClause(final Fragment parent, final String... names) { + public FromClause(final Fragment parent) { super(parent); - for (final String name : names) { - addChild(new Table(this, name)); - } + } + + public static FromClause table(final Fragment parent, final String name) { + final FromClause fromClause = new FromClause(parent); + fromClause.addChild(new Table(fromClause, name)); + return fromClause; + } + + public static FromClause tableAs(final Fragment parent, final String name, final String as) { + final FromClause fromClause = new FromClause(parent); + fromClause.addChild(new Table(fromClause, name, as)); + return fromClause; + } + + public FromClause from(final String name) { + addChild(new Table(this, name)); + return this; + } + + public Fragment fromTableAs(final String name, final String as) { + addChild(new Table(this, name, as)); + return this; } @Override protected void acceptConcrete(final FragmentVisitor visitor) { visitor.visit(this); } -} + + public FromClause join(final String name, final String specification) { + addChild(new Join(this, name, specification)); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Join.java b/src/main/java/com/exasol/sql/dql/Join.java new file mode 100644 index 00000000..14a5075f --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -0,0 +1,48 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +/** + * This class implements the {@link Join} part of a WHERE clause. + */ +public class Join extends AbstractFragement implements Fragment { + private final String name; + private final String specification; + + /** + * Create a new {@link Join} instance + * + * @param parent parent {@link Fragment} + * @param name name of the table to be joined + * @param specification join specification (e.g. ON or USING) + */ + public Join(final Fragment parent, final String name, final String specification) { + super(parent); + this.name = name; + this.specification = specification; + + } + + /** + * Get the name of the joined table + * + * @return name of the joined table + */ + public String getName() { + return this.name; + } + + /** + * Get the join specification + * + * @return join specification + */ + public String getSpecification() { + return this.specification; + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } +} diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 62226dc3..1c37b297 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -38,13 +38,27 @@ public void acceptConcrete(final FragmentVisitor visitor) { } /** - * Add a {@link FromClause} to the statement with table names + * Add a {@link FromClause} to the statement with a table identified by its name * - * @param names table reference names + * @param name table reference name * @return the FROM clause */ - public FromClause from(final String... names) { - final FromClause from = new FromClause(this, names); + public FromClause from(final String name) { + final FromClause from = FromClause.table(this, name); + addChild(from); + return from; + } + + /** + * Add a {@link FromClause} to the statement with an aliased table identified by + * its name + * + * @param name table reference name + * @param as table correlation name + * @return the FROM clause + */ + public FromClause fromTableAs(final String name, final String as) { + final FromClause from = FromClause.tableAs(this, name, as); addChild(from); return from; } diff --git a/src/main/java/com/exasol/sql/dql/StringRendererConfig.java b/src/main/java/com/exasol/sql/dql/StringRendererConfig.java index b3591c79..64abbf58 100644 --- a/src/main/java/com/exasol/sql/dql/StringRendererConfig.java +++ b/src/main/java/com/exasol/sql/dql/StringRendererConfig.java @@ -40,9 +40,11 @@ public StringRendererConfig build() { * * @param lowerCase set to true if the statement should be produced * in lower case + * @return this instance for fluent programming */ - public void lowerCase(final boolean lowerCase) { + public Builder lowerCase(final boolean lowerCase) { this.lowerCase = lowerCase; + return this; } } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Table.java b/src/main/java/com/exasol/sql/dql/Table.java index bd77522f..14f82726 100644 --- a/src/main/java/com/exasol/sql/dql/Table.java +++ b/src/main/java/com/exasol/sql/dql/Table.java @@ -1,5 +1,7 @@ package com.exasol.sql.dql; +import java.util.Optional; + import com.exasol.sql.*; /** @@ -7,6 +9,7 @@ */ public class Table extends AbstractFragement implements TableReference { private final String name; + private final Optional as; /** * Create a new {@link Table} @@ -17,6 +20,13 @@ public class Table extends AbstractFragement implements TableReference { public Table(final Fragment parent, final String name) { super(parent); this.name = name; + this.as = Optional.empty(); + } + + public Table(final Fragment parent, final String name, final String as) { + super(parent); + this.name = name; + this.as = Optional.of(as); } /** @@ -28,6 +38,15 @@ public String getName() { return this.name; } + /** + * Get the correlation name (i.e. an alias) of the table. + * + * @return correlation name + */ + public Optional getAs() { + return this.as; + } + @Override protected void acceptConcrete(final FragmentVisitor visitor) { visitor.visit(this); diff --git a/src/main/java/com/exasol/sql/rendering/StringRenderer.java b/src/main/java/com/exasol/sql/rendering/StringRenderer.java index 2dd970b5..6af91326 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/StringRenderer.java @@ -1,5 +1,7 @@ package com.exasol.sql.rendering; +import java.util.Optional; + import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.dql.*; @@ -16,7 +18,7 @@ public class StringRenderer implements FragmentVisitor { * {@link StringRendererConfig}. */ public StringRenderer() { - this.config = new StringRendererConfig.Builder().build(); + this(new StringRendererConfig.Builder().build()); } /** @@ -39,7 +41,11 @@ public String render() { @Override public void visit(final Select select) { - this.builder.append(this.config.produceLowerCase() ? "select" : "SELECT"); + this.builder.append(produceLowerCase() ? "select" : "SELECT"); + } + + private boolean produceLowerCase() { + return this.config.produceLowerCase(); } @Override @@ -57,7 +63,7 @@ private void appendCommaWhenNeeded(final Fragment fragment) { @Override public void visit(final FromClause fromClause) { - this.builder.append(this.config.produceLowerCase() ? " from" : " FROM"); + this.builder.append(produceLowerCase() ? " from" : " FROM"); } @Override @@ -69,5 +75,18 @@ public void visit(final Table table) { appendCommaWhenNeeded(table); this.builder.append(" "); this.builder.append(table.getName()); + final Optional as = table.getAs(); + if (as.isPresent()) { + this.builder.append(produceLowerCase() ? " as " : " AS "); + this.builder.append(as.get()); + } + } + + @Override + public void visit(final Join join) { + this.builder.append(produceLowerCase() ? " join " : " JOIN "); + this.builder.append(join.getName()); + this.builder.append(produceLowerCase() ? " on " : " ON "); + this.builder.append(join.getSpecification()); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestJoin.java b/src/test/java/com/exasol/dql/TestJoin.java new file mode 100644 index 00000000..b8e1a81d --- /dev/null +++ b/src/test/java/com/exasol/dql/TestJoin.java @@ -0,0 +1,18 @@ +package com.exasol.dql; + +import static com.exasol.hamcrest.RenderResultMatcher.rendersTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.dql.StatementFactory; + +class TestJoin { + @Test + public void testJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").join("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/TestSelect.java index 31709c44..1007e33f 100644 --- a/src/test/java/com/exasol/dql/TestSelect.java +++ b/src/test/java/com/exasol/dql/TestSelect.java @@ -1,25 +1,16 @@ package com.exasol.dql; -import static org.hamcrest.CoreMatchers.equalTo; +import static com.exasol.hamcrest.RenderResultMatcher.rendersTo; +import static com.exasol.hamcrest.RenderResultMatcher.rendersWithConfigTo; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import com.exasol.sql.Fragment; import com.exasol.sql.dql.StatementFactory; import com.exasol.sql.dql.StringRendererConfig; -import com.exasol.sql.rendering.StringRenderer; class TestSelect { - private StringRenderer renderer; - - @BeforeEach - void beforeEach() { - this.renderer = new StringRenderer(); - } - @Test void testGetParentReturnsNull() { assertThat(StatementFactory.getInstance().select().getParent(), nullValue()); @@ -27,37 +18,51 @@ void testGetParentReturnsNull() { @Test void testEmptySelect() { - assertFragmentRenderedTo(StatementFactory.getInstance().select(), "SELECT"); - } - - private void assertFragmentRenderedTo(final Fragment fragment, final String expected) { - fragment.getRoot().accept(this.renderer); - assertThat(this.renderer.render(), equalTo(expected)); + assertThat(StatementFactory.getInstance().select(), rendersTo("SELECT")); } @Test void testEmptySelectLowerCase() { - final StringRendererConfig.Builder builder = new StringRendererConfig.Builder(); - builder.lowerCase(true); - this.renderer = new StringRenderer(builder.build()); - assertFragmentRenderedTo(StatementFactory.getInstance().select(), "select"); + final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + assertThat(StatementFactory.getInstance().select(), rendersWithConfigTo(config, "select")); } @Test void testSelectAll() { - assertFragmentRenderedTo(StatementFactory.getInstance().select().all(), // - "SELECT *"); + assertThat(StatementFactory.getInstance().select().all(), rendersTo("SELECT *")); } @Test void testSelectFieldNames() { - assertFragmentRenderedTo(StatementFactory.getInstance().select().field("a", "b"), // - "SELECT a, b"); + assertThat(StatementFactory.getInstance().select().field("a", "b"), rendersTo("SELECT a, b")); + } + + @Test + void testSelectChainOfFieldNames() { + assertThat(StatementFactory.getInstance().select().field("a", "b").field("c"), rendersTo("SELECT a, b, c")); } @Test void testSelectFromTable() { - assertFragmentRenderedTo(StatementFactory.getInstance().select().all().from("table"), // - "SELECT * FROM table"); + assertThat(StatementFactory.getInstance().select().all().from("table"), rendersTo("SELECT * FROM table")); + } + + @Test + void testSelectFromMultipleTable() { + assertThat(StatementFactory.getInstance().select().all().from("table1").from("table2"), + rendersTo("SELECT * FROM table1, table2")); + } + + @Test + void testSelectFromTableAs() { + assertThat(StatementFactory.getInstance().select().all().fromTableAs("table", "t"), + rendersTo("SELECT * FROM table AS t")); + } + + @Test + void testSelectFromMultipleTableAs() { + assertThat( + StatementFactory.getInstance().select().all().fromTableAs("table1", "t1").fromTableAs("table2", "t2"), + rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java new file mode 100644 index 00000000..2bab1f1f --- /dev/null +++ b/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java @@ -0,0 +1,72 @@ +package com.exasol.hamcrest; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import com.exasol.sql.Fragment; +import com.exasol.sql.dql.StringRendererConfig; +import com.exasol.sql.rendering.StringRenderer; + +/** + * This class implements a matcher for multi-line text that helps finding the + * differences more quickly. + */ +public class RenderResultMatcher extends TypeSafeMatcher { + private final String expectedText; + private final StringRenderer renderer; + private String renderedText = null; + + private RenderResultMatcher(final String expectedText) { + this.expectedText = expectedText; + this.renderer = new StringRenderer(); + } + + private RenderResultMatcher(final StringRendererConfig config, final String expectedText) { + this.expectedText = expectedText; + this.renderer = new StringRenderer(config); + } + + /** + * Match the rendered result against original text. + * + * @param text the text to be matched against the original text. + */ + @Override + public boolean matchesSafely(final Fragment fragment) { + fragment.getRoot().accept(this.renderer); + this.renderedText = this.renderer.render(); + return this.renderedText.equals(this.expectedText); + } + + @Override + protected void describeMismatchSafely(final Fragment fragment, final Description mismatchDescription) { + mismatchDescription.appendText(this.renderedText); + } + + @Override + public void describeTo(final Description description) { + description.appendText(this.expectedText); + } + + /** + * Factory method for {@link RenderResultMatcher} + * + * @param expectedText text that represents the expected rendering result + * @return the matcher + */ + public static RenderResultMatcher rendersTo(final String expectedText) { + return new RenderResultMatcher(expectedText); + } + + /** + * Factory method for {@link RenderResultMatcher} + * + * @param config configuration settings for the {@link StringRenderer} + * @param expectedText text that represents the expected rendering result + * @return the matcher + */ + public static RenderResultMatcher rendersWithConfigTo(final StringRendererConfig config, + final String expectedText) { + return new RenderResultMatcher(config, expectedText); + } +} \ No newline at end of file From 64191cbd95d09d7e7ec8d411a1fb44a323d5cdbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Thu, 20 Sep 2018 09:33:24 +0200 Subject: [PATCH 04/43] PMI-66: Experiment with sub-visitors --- .../class/cl_hierarchical_visitor.plantuml | 37 ++++++++++++ pom.xml | 2 +- ...ctFragement.java => AbstractFragment.java} | 4 +- src/main/java/com/exasol/sql/dql/Field.java | 2 +- .../java/com/exasol/sql/dql/FromClause.java | 39 +++++++++++- src/main/java/com/exasol/sql/dql/Join.java | 15 ++++- .../java/com/exasol/sql/dql/JoinType.java | 27 +++++++++ src/main/java/com/exasol/sql/dql/Select.java | 2 +- src/main/java/com/exasol/sql/dql/Table.java | 2 +- .../sql/expression/BooleanExpression.java | 18 ++++++ .../BooleanExpressionFragmentVisitor.java | 9 +++ .../exasol/sql/rendering/StringRenderer.java | 8 ++- .../StringRendererConfig.java | 4 +- .../visitor/AbstractHierarchicalVisitor.java | 25 ++++++++ .../util/visitor/HierarchicalVisitor.java | 31 ++++++++++ .../com/exasol/util/visitor/Visitable.java | 5 ++ src/test/java/com/exasol/dql/TestJoin.java | 60 +++++++++++++++++++ src/test/java/com/exasol/dql/TestSelect.java | 2 +- .../exasol/hamcrest/RenderResultMatcher.java | 2 +- .../sql/expression/TestBooleanExpression.java | 14 +++++ .../util/visitor/DummyChildVisitable.java | 13 ++++ .../util/visitor/DummyChildVisitor.java | 18 ++++++ .../util/visitor/DummyParentVisitable.java | 5 ++ .../util/visitor/DummyParentVisitor.java | 17 ++++++ .../exasol/util/visitor/DummyVisitable.java | 5 ++ .../util/visitor/TestHierarchicalVisitor.java | 40 +++++++++++++ 26 files changed, 392 insertions(+), 14 deletions(-) create mode 100644 model/diagrams/class/cl_hierarchical_visitor.plantuml rename src/main/java/com/exasol/sql/{AbstractFragement.java => AbstractFragment.java} (92%) create mode 100644 src/main/java/com/exasol/sql/dql/JoinType.java create mode 100644 src/main/java/com/exasol/sql/expression/BooleanExpression.java create mode 100644 src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java rename src/main/java/com/exasol/sql/{dql => rendering}/StringRendererConfig.java (94%) create mode 100644 src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java create mode 100644 src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java create mode 100644 src/main/java/com/exasol/util/visitor/Visitable.java create mode 100644 src/test/java/com/exasol/sql/expression/TestBooleanExpression.java create mode 100644 src/test/java/com/exasol/util/visitor/DummyChildVisitable.java create mode 100644 src/test/java/com/exasol/util/visitor/DummyChildVisitor.java create mode 100644 src/test/java/com/exasol/util/visitor/DummyParentVisitable.java create mode 100644 src/test/java/com/exasol/util/visitor/DummyParentVisitor.java create mode 100644 src/test/java/com/exasol/util/visitor/DummyVisitable.java create mode 100644 src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java diff --git a/model/diagrams/class/cl_hierarchical_visitor.plantuml b/model/diagrams/class/cl_hierarchical_visitor.plantuml new file mode 100644 index 00000000..cc706f29 --- /dev/null +++ b/model/diagrams/class/cl_hierarchical_visitor.plantuml @@ -0,0 +1,37 @@ +@startuml +!include ../exasol.skin +package com.exasol.util { + interface HierarchicalVisitor <> + abstract class AbstractHierarchicalVisitor <> { + + register(child : HiearchicalVisitor) : void + } +} +package com.exasol.sql { + interface Fragment <> + abstract class AbstractFragment <> { + + accept(visitor : FragmentVisitor) : void + } + interface FragmentVisitor + +} + +package com.exasol.sql.dql { + class SqlStatement + class Field +} + +package com.exasol.sql.rendering { + class BooleanExpressionRenderingVisitor +} + +AbstractHierarchicalVisitor .u.|> HierarchicalVisitor +FragmentVisitor -u-|> AbstractHierarchicalVisitor +BooleanExpressionRenderingVisitor -u-|> AbstractHierarchicalVisitor +BooleanExpressionRenderingVisitor -l-> FragmentVisitor : registered sub-visitor + +AbstractFragment .u.|> Fragment +SqlStatement -d-|> Fragment +Field -d-|> Fragment +AbstractFragment -r-> FragmentVisitor : accepts + +@enduml \ No newline at end of file diff --git a/pom.xml b/pom.xml index ae43a0e6..60e682fc 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.hamcrest - hamcrest-core + hamcrest-all 1.3 test diff --git a/src/main/java/com/exasol/sql/AbstractFragement.java b/src/main/java/com/exasol/sql/AbstractFragment.java similarity index 92% rename from src/main/java/com/exasol/sql/AbstractFragement.java rename to src/main/java/com/exasol/sql/AbstractFragment.java index d6b368ae..fba72384 100644 --- a/src/main/java/com/exasol/sql/AbstractFragement.java +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -9,12 +9,12 @@ * * @param the type of the concrete class implementing the missing parts. */ -public abstract class AbstractFragement implements Fragment { +public abstract class AbstractFragment implements Fragment { private final Fragment root; protected final Fragment parent; protected final List children = new ArrayList<>(); - protected AbstractFragement(final Fragment parent) { + protected AbstractFragment(final Fragment parent) { if (parent == null) { this.root = this; } else { diff --git a/src/main/java/com/exasol/sql/dql/Field.java b/src/main/java/com/exasol/sql/dql/Field.java index b10ac338..98a38961 100644 --- a/src/main/java/com/exasol/sql/dql/Field.java +++ b/src/main/java/com/exasol/sql/dql/Field.java @@ -2,7 +2,7 @@ import com.exasol.sql.*; -public class Field extends AbstractFragement implements FieldDefinition { +public class Field extends AbstractFragment implements FieldDefinition { private final String name; protected Field(final Fragment parent, final String name) { diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index 41b3bd8c..f5d44d69 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -2,7 +2,7 @@ import com.exasol.sql.*; -public class FromClause extends AbstractFragement { +public class FromClause extends AbstractFragment { public FromClause(final Fragment parent) { super(parent); } @@ -35,7 +35,42 @@ protected void acceptConcrete(final FragmentVisitor visitor) { } public FromClause join(final String name, final String specification) { - addChild(new Join(this, name, specification)); + addChild(new Join(this, JoinType.DEFAULT, name, specification)); + return this; + } + + public FromClause innerJoin(final String name, final String specification) { + addChild(new Join(this, JoinType.INNER, name, specification)); + return this; + } + + public FromClause leftJoin(final String name, final String specification) { + addChild(new Join(this, JoinType.LEFT, name, specification)); + return this; + } + + public FromClause rightJoin(final String name, final String specification) { + addChild(new Join(this, JoinType.RIGHT, name, specification)); + return this; + } + + public FromClause fullJoin(final String name, final String specification) { + addChild(new Join(this, JoinType.FULL, name, specification)); + return this; + } + + public FromClause leftOuterJoin(final String name, final String specification) { + addChild(new Join(this, JoinType.LEFT_OUTER, name, specification)); + return this; + } + + public FromClause rightOuterJoin(final String name, final String specification) { + addChild(new Join(this, JoinType.RIGHT_OUTER, name, specification)); + return this; + } + + public FromClause fullOuterJoin(final String name, final String specification) { + addChild(new Join(this, JoinType.FULL_OUTER, name, specification)); return this; } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Join.java b/src/main/java/com/exasol/sql/dql/Join.java index 14a5075f..855224f0 100644 --- a/src/main/java/com/exasol/sql/dql/Join.java +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -5,7 +5,8 @@ /** * This class implements the {@link Join} part of a WHERE clause. */ -public class Join extends AbstractFragement implements Fragment { +public class Join extends AbstractFragment implements Fragment { + private final JoinType type; private final String name; private final String specification; @@ -13,14 +14,24 @@ public class Join extends AbstractFragement implements Fragment { * Create a new {@link Join} instance * * @param parent parent {@link Fragment} + * @param type type of join (e.g. INNER, LEFT or RIGHT OUTER) * @param name name of the table to be joined * @param specification join specification (e.g. ON or USING) */ - public Join(final Fragment parent, final String name, final String specification) { + public Join(final Fragment parent, final JoinType type, final String name, final String specification) { super(parent); + this.type = type; this.name = name; this.specification = specification; + } + /** + * Get the type of the join + * + * @return join type (e.g. INNER or LEFT) + */ + public JoinType getType() { + return this.type; } /** diff --git a/src/main/java/com/exasol/sql/dql/JoinType.java b/src/main/java/com/exasol/sql/dql/JoinType.java new file mode 100644 index 00000000..050849fc --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/JoinType.java @@ -0,0 +1,27 @@ +package com.exasol.sql.dql; + +/** + * This class represents the {@link Join} types supported by SQL. + * + *
+ * DEFAULT = INNER     : ( (*) )
+ * LEFT = LEFT_OUTER   : (*(*) )
+ * RIGHT = RIGHT_OUTER : ( (*)*)
+ * FULL = FULL_OUTER   : (*(*)*)
+ * 
+ */ +public enum JoinType { + DEFAULT(""), INNER("INNER"), LEFT("LEFT"), RIGHT("RIGHT"), FULL("FULL"), LEFT_OUTER("LEFT OUTER"), + RIGHT_OUTER("RIGHT OUTER"), FULL_OUTER("FULL OUTER"); + + private final String text; + + private JoinType(final String text) { + this.text = text; + } + + @Override + public String toString() { + return this.text; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 1c37b297..134bab3e 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -5,7 +5,7 @@ /** * This class implements an SQL {@link Select} statement */ -public class Select extends AbstractFragement implements SqlStatement { +public class Select extends AbstractFragment implements SqlStatement { public Select(final Fragment parent) { super(parent); } diff --git a/src/main/java/com/exasol/sql/dql/Table.java b/src/main/java/com/exasol/sql/dql/Table.java index 14f82726..6b8223a3 100644 --- a/src/main/java/com/exasol/sql/dql/Table.java +++ b/src/main/java/com/exasol/sql/dql/Table.java @@ -7,7 +7,7 @@ /** * This class represents a {@link Table} in an SQL Statement */ -public class Table extends AbstractFragement implements TableReference { +public class Table extends AbstractFragment implements TableReference { private final String name; private final Optional as; diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpression.java b/src/main/java/com/exasol/sql/expression/BooleanExpression.java new file mode 100644 index 00000000..df7c1a65 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanExpression.java @@ -0,0 +1,18 @@ +package com.exasol.sql.expression; + +import com.exasol.sql.*; + +public class BooleanExpression extends AbstractFragment { + protected BooleanExpression(final Fragment parent) { + super(parent); + } + + public static BooleanExpression not(final String string) { + return new BooleanExpression(null); + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java b/src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java new file mode 100644 index 00000000..7f202a6a --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java @@ -0,0 +1,9 @@ +package com.exasol.sql.expression; + +import com.exasol.sql.FragmentVisitor; + +public interface BooleanExpressionFragmentVisitor extends FragmentVisitor { + + public void visit(BooleanExpression booleanExpression); + +} diff --git a/src/main/java/com/exasol/sql/rendering/StringRenderer.java b/src/main/java/com/exasol/sql/rendering/StringRenderer.java index 6af91326..0a3ce086 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/StringRenderer.java @@ -5,11 +5,12 @@ import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.dql.*; +import com.exasol.util.visitor.AbstractHierarchicalVisitor; /** * The {@link StringRenderer} turns SQL statement structures in to SQL strings. */ -public class StringRenderer implements FragmentVisitor { +public class StringRenderer extends AbstractHierarchicalVisitor implements FragmentVisitor { private final StringBuilder builder = new StringBuilder(); private final StringRendererConfig config; @@ -84,6 +85,11 @@ public void visit(final Table table) { @Override public void visit(final Join join) { + final JoinType type = join.getType(); + if (type != JoinType.DEFAULT) { + this.builder.append(" "); + this.builder.append(produceLowerCase() ? type.toString().toLowerCase() : type.toString()); + } this.builder.append(produceLowerCase() ? " join " : " JOIN "); this.builder.append(join.getName()); this.builder.append(produceLowerCase() ? " on " : " ON "); diff --git a/src/main/java/com/exasol/sql/dql/StringRendererConfig.java b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java similarity index 94% rename from src/main/java/com/exasol/sql/dql/StringRendererConfig.java rename to src/main/java/com/exasol/sql/rendering/StringRendererConfig.java index 64abbf58..591ed3f3 100644 --- a/src/main/java/com/exasol/sql/dql/StringRendererConfig.java +++ b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java @@ -1,4 +1,6 @@ -package com.exasol.sql.dql; +package com.exasol.sql.rendering; + +import com.exasol.sql.dql.StatementFactory; /** * This class implements a parameter object containing the configuration options diff --git a/src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java b/src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java new file mode 100644 index 00000000..bdd4a5b8 --- /dev/null +++ b/src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java @@ -0,0 +1,25 @@ +package com.exasol.util.visitor; + +import java.util.*; + +/** + * Abstract base class for building visitors that are organized in a hierarchy. + */ +public abstract class AbstractHierarchicalVisitor implements HierarchicalVisitor { + private final Map, HierarchicalVisitor> registeredVisitors = new HashMap<>(); + + @Override + public void register(final HierarchicalVisitor visitor, final Class responsibleFor) { + this.registeredVisitors.put(responsibleFor, visitor); + } + + @Override + public Collection getRegisted() { + return this.registeredVisitors.values(); + } + + @Override + public HierarchicalVisitor getRegisteredVisitorForType(final Visitable host) { + return this.registeredVisitors.get(host.getClass()); + } +} diff --git a/src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java b/src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java new file mode 100644 index 00000000..1860edd2 --- /dev/null +++ b/src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java @@ -0,0 +1,31 @@ +package com.exasol.util.visitor; + +import java.util.Collection; + +/** + * This in the interface for visitors that allow plugging in sub-visitors. + */ +public interface HierarchicalVisitor { + /** + * Register a sub-visitor + * + * @param child sub-visitor to be registered + */ + public void register(HierarchicalVisitor child, Class responsibleFor); + + /** + * Get a list of registered sub-visitors + * + * @return list of registered sub-visitors + */ + public Collection getRegisted(); + + HierarchicalVisitor getRegisteredVisitorForType(final Visitable host); + + /** + * Fallback methods that allows delegation of visiting to registered visitors. + * + * @param host object requesting the visit + */ + public void visit(Visitable host); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/util/visitor/Visitable.java b/src/main/java/com/exasol/util/visitor/Visitable.java new file mode 100644 index 00000000..a22cae17 --- /dev/null +++ b/src/main/java/com/exasol/util/visitor/Visitable.java @@ -0,0 +1,5 @@ +package com.exasol.util.visitor; + +public interface Visitable { + +} diff --git a/src/test/java/com/exasol/dql/TestJoin.java b/src/test/java/com/exasol/dql/TestJoin.java index b8e1a81d..1e68d4e1 100644 --- a/src/test/java/com/exasol/dql/TestJoin.java +++ b/src/test/java/com/exasol/dql/TestJoin.java @@ -15,4 +15,64 @@ public void testJoin() { "left_table.foo_id = right_table.foo_id"), rendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id")); } + + @Test + public void testInnerJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").innerJoin("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table INNER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } + + @Test + public void testLeftJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").leftJoin("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table LEFT JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } + + @Test + public void testRigthJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").rightJoin("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table RIGHT JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } + + @Test + public void testFullJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").fullOuterJoin("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo( + "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } + + @Test + public void testLeftOuterJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").leftOuterJoin("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo( + "SELECT * FROM left_table LEFT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } + + @Test + public void testRigthOuterJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").rightOuterJoin("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo( + "SELECT * FROM left_table RIGHT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } + + @Test + public void testFullOuterJoin() { + assertThat( + StatementFactory.getInstance().select().all().from("left_table").fullOuterJoin("right_table", + "left_table.foo_id = right_table.foo_id"), + rendersTo( + "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/TestSelect.java index 1007e33f..c437bd41 100644 --- a/src/test/java/com/exasol/dql/TestSelect.java +++ b/src/test/java/com/exasol/dql/TestSelect.java @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Test; import com.exasol.sql.dql.StatementFactory; -import com.exasol.sql.dql.StringRendererConfig; +import com.exasol.sql.rendering.StringRendererConfig; class TestSelect { @Test diff --git a/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java index 2bab1f1f..354399e1 100644 --- a/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java +++ b/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java @@ -4,8 +4,8 @@ import org.hamcrest.TypeSafeMatcher; import com.exasol.sql.Fragment; -import com.exasol.sql.dql.StringRendererConfig; import com.exasol.sql.rendering.StringRenderer; +import com.exasol.sql.rendering.StringRendererConfig; /** * This class implements a matcher for multi-line text that helps finding the diff --git a/src/test/java/com/exasol/sql/expression/TestBooleanExpression.java b/src/test/java/com/exasol/sql/expression/TestBooleanExpression.java new file mode 100644 index 00000000..2b711143 --- /dev/null +++ b/src/test/java/com/exasol/sql/expression/TestBooleanExpression.java @@ -0,0 +1,14 @@ +package com.exasol.sql.expression; + +import static com.exasol.hamcrest.RenderResultMatcher.rendersTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +class TestBooleanExpression { + @Test + void testUnaryNot() { + final BooleanExpression expression = BooleanExpression.not("foo"); + assertThat(expression, rendersTo("NOT foo")); + } +} diff --git a/src/test/java/com/exasol/util/visitor/DummyChildVisitable.java b/src/test/java/com/exasol/util/visitor/DummyChildVisitable.java new file mode 100644 index 00000000..1083aa80 --- /dev/null +++ b/src/test/java/com/exasol/util/visitor/DummyChildVisitable.java @@ -0,0 +1,13 @@ +package com.exasol.util.visitor; + +public class DummyChildVisitable implements Visitable { + private boolean visitedHost; + + public void visit(final DummyChildVisitable host) { + this.visitedHost = true; + } + + public boolean visitedHost() { + return this.visitedHost; + } +} diff --git a/src/test/java/com/exasol/util/visitor/DummyChildVisitor.java b/src/test/java/com/exasol/util/visitor/DummyChildVisitor.java new file mode 100644 index 00000000..6d388f3c --- /dev/null +++ b/src/test/java/com/exasol/util/visitor/DummyChildVisitor.java @@ -0,0 +1,18 @@ +package com.exasol.util.visitor; + +public class DummyChildVisitor extends AbstractHierarchicalVisitor { + private boolean visitedHost; + + public void visit(final DummyChildVisitable host) { + this.visitedHost = true; + } + + public boolean visitedHost() { + return this.visitedHost; + } + + @Override + public void visit(final Visitable host) { + throw new UnsupportedOperationException(); + } +} diff --git a/src/test/java/com/exasol/util/visitor/DummyParentVisitable.java b/src/test/java/com/exasol/util/visitor/DummyParentVisitable.java new file mode 100644 index 00000000..10cdfce4 --- /dev/null +++ b/src/test/java/com/exasol/util/visitor/DummyParentVisitable.java @@ -0,0 +1,5 @@ +package com.exasol.util.visitor; + +public class DummyParentVisitable implements Visitable { + +} diff --git a/src/test/java/com/exasol/util/visitor/DummyParentVisitor.java b/src/test/java/com/exasol/util/visitor/DummyParentVisitor.java new file mode 100644 index 00000000..f1952083 --- /dev/null +++ b/src/test/java/com/exasol/util/visitor/DummyParentVisitor.java @@ -0,0 +1,17 @@ +package com.exasol.util.visitor; + +public class DummyParentVisitor extends AbstractHierarchicalVisitor { + private boolean visitedHost; + + public void visit(final DummyParentVisitable host) { + this.visitedHost = true; + } + + public void visit(final Visitable host) { + getRegisteredVisitorForType(host).visit(host); + } + + public boolean visitedHost() { + return this.visitedHost; + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/util/visitor/DummyVisitable.java b/src/test/java/com/exasol/util/visitor/DummyVisitable.java new file mode 100644 index 00000000..9fee50d2 --- /dev/null +++ b/src/test/java/com/exasol/util/visitor/DummyVisitable.java @@ -0,0 +1,5 @@ +package com.exasol.util.visitor; + +public class DummyVisitable implements Visitable { + +} diff --git a/src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java b/src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java new file mode 100644 index 00000000..44f2a27c --- /dev/null +++ b/src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java @@ -0,0 +1,40 @@ +package com.exasol.util.visitor; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestHierarchicalVisitor { + private DummyParentVisitor parent; + private DummyChildVisitor child; + + @BeforeEach + void beforeEach() { + this.parent = new DummyParentVisitor(); + this.child = new DummyChildVisitor(); + this.parent.register(this.child, DummyChildVisitable.class); + } + + @Test + void testRegisterSubVisitor() { + assertThat(this.parent.getRegisted(), containsInAnyOrder(this.child)); + } + + @Test + void testVisitInParent() { + final DummyParentVisitable host = new DummyParentVisitable(); + this.parent.visit(host); + assertThat(this.parent.visitedHost(), equalTo(true)); + } + + @Test + void testVisitInParentThenDelegate() { + final DummyChildVisitable host = new DummyChildVisitable(); + this.parent.visit(host); + assertThat(this.parent.visitedHost(), equalTo(false)); + assertThat(this.child.visitedHost(), equalTo(true)); + } +} \ No newline at end of file From f02cf6373a39c31b0b915d347a316174b084447e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Fri, 21 Sep 2018 12:03:30 +0200 Subject: [PATCH 05/43] PMI-66: Removed duplication in tree handling. Improved renderer for boolean expressions. --- .../class/cl_hierarchical_visitor.plantuml | 37 ------ model/diagrams/class/cl_visitor.plantuml | 31 +++++ model/diagrams/exasol.skin | 17 +++ .../java/com/exasol/sql/AbstractFragment.java | 51 ++------ src/main/java/com/exasol/sql/Fragment.java | 21 +--- .../java/com/exasol/sql/FragmentVisitor.java | 7 ++ src/main/java/com/exasol/sql/dql/Join.java | 2 +- src/main/java/com/exasol/sql/dql/Select.java | 4 + .../com/exasol/sql/dql/StatementFactory.java | 2 +- .../expression/AbstractBooleanExpression.java | 37 ++++++ .../java/com/exasol/sql/expression/And.java | 28 +++++ .../sql/expression/BooleanExpression.java | 27 ++-- .../BooleanExpressionFragmentVisitor.java | 9 -- .../expression/BooleanExpressionVisitor.java | 14 +++ .../exasol/sql/expression/BooleanTerm.java | 41 +++++++ .../com/exasol/sql/expression/Literal.java | 29 +++++ .../java/com/exasol/sql/expression/Not.java | 29 +++++ .../rendering/BooleanExpressionRenderer.java | 72 +++++++++++ ...enderer.java => SqlStatementRenderer.java} | 68 ++++++---- .../exasol/util/AbstractBottomUpTreeNode.java | 116 ++++++++++++++++++ .../com/exasol/util/AbstractTreeNode.java | 71 +++++++++++ src/main/java/com/exasol/util/TreeNode.java | 62 ++++++++++ .../visitor/AbstractHierarchicalVisitor.java | 25 ---- .../util/visitor/HierarchicalVisitor.java | 31 ----- .../com/exasol/util/visitor/Visitable.java | 5 - .../exasol/dql/{ => rendering}/TestJoin.java | 4 +- .../dql/{ => rendering}/TestSelect.java | 6 +- .../hamcrest/AbstractRenderResultMatcher.java | 19 +++ .../BooleanExpressionRenderResultMatcher.java | 66 ++++++++++ .../exasol/hamcrest/RenderResultMatcher.java | 72 ----------- .../SqlFragmentRenderResultMatcher.java | 65 ++++++++++ .../sql/expression/TestBooleanExpression.java | 14 --- .../TestBooleanExpressionRenderer.java | 30 +++++ .../util/visitor/DummyChildVisitable.java | 13 -- .../util/visitor/DummyChildVisitor.java | 18 --- .../util/visitor/DummyParentVisitable.java | 5 - .../util/visitor/DummyParentVisitor.java | 17 --- .../exasol/util/visitor/DummyVisitable.java | 5 - .../util/visitor/TestHierarchicalVisitor.java | 40 ------ 39 files changed, 812 insertions(+), 398 deletions(-) delete mode 100644 model/diagrams/class/cl_hierarchical_visitor.plantuml create mode 100644 model/diagrams/class/cl_visitor.plantuml create mode 100644 src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java create mode 100644 src/main/java/com/exasol/sql/expression/And.java delete mode 100644 src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java create mode 100644 src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java create mode 100644 src/main/java/com/exasol/sql/expression/BooleanTerm.java create mode 100644 src/main/java/com/exasol/sql/expression/Literal.java create mode 100644 src/main/java/com/exasol/sql/expression/Not.java create mode 100644 src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java rename src/main/java/com/exasol/sql/rendering/{StringRenderer.java => SqlStatementRenderer.java} (50%) create mode 100644 src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java create mode 100644 src/main/java/com/exasol/util/AbstractTreeNode.java create mode 100644 src/main/java/com/exasol/util/TreeNode.java delete mode 100644 src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java delete mode 100644 src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java delete mode 100644 src/main/java/com/exasol/util/visitor/Visitable.java rename src/test/java/com/exasol/dql/{ => rendering}/TestJoin.java (96%) rename src/test/java/com/exasol/dql/{ => rendering}/TestSelect.java (91%) create mode 100644 src/test/java/com/exasol/hamcrest/AbstractRenderResultMatcher.java create mode 100644 src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java delete mode 100644 src/test/java/com/exasol/hamcrest/RenderResultMatcher.java create mode 100644 src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java delete mode 100644 src/test/java/com/exasol/sql/expression/TestBooleanExpression.java create mode 100644 src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java delete mode 100644 src/test/java/com/exasol/util/visitor/DummyChildVisitable.java delete mode 100644 src/test/java/com/exasol/util/visitor/DummyChildVisitor.java delete mode 100644 src/test/java/com/exasol/util/visitor/DummyParentVisitable.java delete mode 100644 src/test/java/com/exasol/util/visitor/DummyParentVisitor.java delete mode 100644 src/test/java/com/exasol/util/visitor/DummyVisitable.java delete mode 100644 src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java diff --git a/model/diagrams/class/cl_hierarchical_visitor.plantuml b/model/diagrams/class/cl_hierarchical_visitor.plantuml deleted file mode 100644 index cc706f29..00000000 --- a/model/diagrams/class/cl_hierarchical_visitor.plantuml +++ /dev/null @@ -1,37 +0,0 @@ -@startuml -!include ../exasol.skin -package com.exasol.util { - interface HierarchicalVisitor <> - abstract class AbstractHierarchicalVisitor <> { - + register(child : HiearchicalVisitor) : void - } -} -package com.exasol.sql { - interface Fragment <> - abstract class AbstractFragment <> { - + accept(visitor : FragmentVisitor) : void - } - interface FragmentVisitor - -} - -package com.exasol.sql.dql { - class SqlStatement - class Field -} - -package com.exasol.sql.rendering { - class BooleanExpressionRenderingVisitor -} - -AbstractHierarchicalVisitor .u.|> HierarchicalVisitor -FragmentVisitor -u-|> AbstractHierarchicalVisitor -BooleanExpressionRenderingVisitor -u-|> AbstractHierarchicalVisitor -BooleanExpressionRenderingVisitor -l-> FragmentVisitor : registered sub-visitor - -AbstractFragment .u.|> Fragment -SqlStatement -d-|> Fragment -Field -d-|> Fragment -AbstractFragment -r-> FragmentVisitor : accepts - -@enduml \ No newline at end of file diff --git a/model/diagrams/class/cl_visitor.plantuml b/model/diagrams/class/cl_visitor.plantuml new file mode 100644 index 00000000..6850a479 --- /dev/null +++ b/model/diagrams/class/cl_visitor.plantuml @@ -0,0 +1,31 @@ +@startuml +!include ../exasol.skin + +package com.exasol.sql { + interface Fragment <> + + abstract class AbstractFragment <> { + + accept(visitor : FragmentVisitor) : void + } + + interface FragmentVisitor <> { + + visit(statement : SqlStatement) : void + + visit(field : Field) : void + } + + package dql { + class SqlStatement + class Field + } + + package rendering { + class StringRenderer + } + + AbstractFragment .u.|> Fragment + AbstractFragment -r-> FragmentVisitor : accepts + SqlStatement -u-|> AbstractFragment + Field -u-|> AbstractFragment + StringRenderer .u.|> FragmentVisitor +} +@enduml \ No newline at end of file diff --git a/model/diagrams/exasol.skin b/model/diagrams/exasol.skin index 3085cae0..aaf51dc4 100644 --- a/model/diagrams/exasol.skin +++ b/model/diagrams/exasol.skin @@ -1,6 +1,7 @@ hide empty methods hide empty attributes skinparam style strictuml +skinparam classAttributeIconSize 0 !pragma horizontalLineBetweenDifferentPackageAllowed skinparam Arrow { @@ -18,4 +19,20 @@ skinparam Class { StereotypeFontColor<> ffffff } +skinparam ClassAttribute { + BackgroundColor fffff + FontColor 093e52 + BorderColor 093e52 + BackgroundColor<> 00b09b + FontColor<> ffffff + StereotypeFontColor<> ffffff +} + +skinparam Package { + BackgroundColor fffff + FontColor 093e52 + FontStyle bold + BorderColor 093e52 +} + skinparam padding 5 \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java index fba72384..2d9d55a4 100644 --- a/src/main/java/com/exasol/sql/AbstractFragment.java +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -1,7 +1,7 @@ package com.exasol.sql; -import java.util.ArrayList; -import java.util.List; +import com.exasol.util.AbstractTreeNode; +import com.exasol.util.TreeNode; /** * This class provides an abstract base for SQL statement fragments. It also @@ -9,53 +9,20 @@ * * @param the type of the concrete class implementing the missing parts. */ -public abstract class AbstractFragment implements Fragment { - private final Fragment root; - protected final Fragment parent; - protected final List children = new ArrayList<>(); - - protected AbstractFragment(final Fragment parent) { - if (parent == null) { - this.root = this; - } else { - this.root = parent.getRoot(); - } - this.parent = parent; - } - - @Override - public Fragment getRoot() { - return this.root; - } - - @Override - public Fragment getParent() { - return this.parent; +public abstract class AbstractFragment extends AbstractTreeNode implements Fragment { + protected AbstractFragment() { + super(); } - protected void addChild(final Fragment child) { - this.children.add(child); - } - - protected List getChildren() { - return this.children; - } - - @Override - public Fragment getChild(final int index) { - return this.children.get(index); - } - - @Override - public boolean isFirstSibling() { - return (this.parent != null) && (this.getParent().getChild(0) == this); + protected AbstractFragment(final Fragment parent) { + super(parent); } @Override public void accept(final FragmentVisitor visitor) { acceptConcrete(visitor); - for (final Fragment child : getChildren()) { - child.accept(visitor); + for (final TreeNode child : this.getChildren()) { + ((Fragment) child).accept(visitor); } } diff --git a/src/main/java/com/exasol/sql/Fragment.java b/src/main/java/com/exasol/sql/Fragment.java index 77544c93..2269bf73 100644 --- a/src/main/java/com/exasol/sql/Fragment.java +++ b/src/main/java/com/exasol/sql/Fragment.java @@ -1,24 +1,7 @@ package com.exasol.sql; -public interface Fragment { - @Override - public String toString(); - - public Fragment getParent(); +import com.exasol.util.TreeNode; +public interface Fragment extends TreeNode { public void accept(FragmentVisitor visitor); - - public Fragment getRoot(); - - public boolean isFirstSibling(); - - /** - * Get child at index position - * - * @param index position of the child - * @return child at index - * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || - * index >= size()) - */ - public Fragment getChild(int index) throws IndexOutOfBoundsException; } diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index b05c4a38..eae9b03c 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -1,6 +1,7 @@ package com.exasol.sql; import com.exasol.sql.dql.*; +import com.exasol.sql.expression.*; /** * This interface represents a visitor for SQL statement fragments. @@ -17,4 +18,10 @@ public interface FragmentVisitor { public void visit(Table table); public void visit(Join join); + + public void visit(AbstractBooleanExpression booleanExpression); + + public void visit(Not not); + + public void visit(Literal literal); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Join.java b/src/main/java/com/exasol/sql/dql/Join.java index 855224f0..7573974e 100644 --- a/src/main/java/com/exasol/sql/dql/Join.java +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -27,7 +27,7 @@ public Join(final Fragment parent, final JoinType type, final String name, final /** * Get the type of the join - * + * * @return join type (e.g. INNER or LEFT) */ public JoinType getType() { diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 134bab3e..7dbbc770 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -10,6 +10,10 @@ public Select(final Fragment parent) { super(parent); } + public Select() { + super(); + } + @Override public String toString() { return "SELECT"; diff --git a/src/main/java/com/exasol/sql/dql/StatementFactory.java b/src/main/java/com/exasol/sql/dql/StatementFactory.java index 23491300..3751a088 100644 --- a/src/main/java/com/exasol/sql/dql/StatementFactory.java +++ b/src/main/java/com/exasol/sql/dql/StatementFactory.java @@ -28,6 +28,6 @@ private StatementFactory() { * @return a new instance of a {@link Select} statement */ public Select select() { - return new Select(null); + return new Select(); } } diff --git a/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java b/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java new file mode 100644 index 00000000..e2a3f896 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java @@ -0,0 +1,37 @@ +package com.exasol.sql.expression; + +import com.exasol.util.AbstractBottomUpTreeNode; +import com.exasol.util.TreeNode; + +/** + * Abstract base class for all types of BooleanExpressions + */ +public abstract class AbstractBooleanExpression extends AbstractBottomUpTreeNode implements BooleanExpression { + protected AbstractBooleanExpression() { + super(); + } + + protected AbstractBooleanExpression(final BooleanExpression expression) { + super(expression); + } + + protected AbstractBooleanExpression(final BooleanExpression... expressions) { + super(expressions); + } + + @Override + public void accept(final BooleanExpressionVisitor visitor) { + acceptConcrete(visitor); + for (final TreeNode child : this.getChildren()) { + ((BooleanExpression) child).accept(visitor); + } + } + + /** + * Sub-classes must override this method so that the visitor knows the type of + * the visited class at compile time. + * + * @param visitor visitor to accept + */ + public abstract void acceptConcrete(final BooleanExpressionVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/And.java b/src/main/java/com/exasol/sql/expression/And.java new file mode 100644 index 00000000..31160801 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/And.java @@ -0,0 +1,28 @@ +package com.exasol.sql.expression; + +/** + * This class represents + */ +public class And extends AbstractBooleanExpression { + + public And(final BooleanExpression... expressions) { + super(expressions); + } + + public And(final String... strings) { + this(mapLiterals(strings)); + } + + private static BooleanExpression[] mapLiterals(final String[] strings) { + final BooleanExpression[] literals = new BooleanExpression[strings.length]; + for (int i = 0; i < strings.length; ++i) { + literals[i] = Literal.of(strings[i]); + } + return literals; + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } +} diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpression.java b/src/main/java/com/exasol/sql/expression/BooleanExpression.java index df7c1a65..2bf25f9b 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanExpression.java +++ b/src/main/java/com/exasol/sql/expression/BooleanExpression.java @@ -1,18 +1,15 @@ package com.exasol.sql.expression; -import com.exasol.sql.*; - -public class BooleanExpression extends AbstractFragment { - protected BooleanExpression(final Fragment parent) { - super(parent); - } - - public static BooleanExpression not(final String string) { - return new BooleanExpression(null); - } - - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } +import com.exasol.util.TreeNode; + +/** + * Common interface for all types of boolean expressions + */ +public interface BooleanExpression extends TreeNode { + /** + * Accept a visitor + * + * @param visitor visitor to accept + */ + public void accept(final BooleanExpressionVisitor visitor); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java b/src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java deleted file mode 100644 index 7f202a6a..00000000 --- a/src/main/java/com/exasol/sql/expression/BooleanExpressionFragmentVisitor.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.exasol.sql.expression; - -import com.exasol.sql.FragmentVisitor; - -public interface BooleanExpressionFragmentVisitor extends FragmentVisitor { - - public void visit(BooleanExpression booleanExpression); - -} diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java new file mode 100644 index 00000000..03a08964 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java @@ -0,0 +1,14 @@ +package com.exasol.sql.expression; + +/** + * Visitor interface for a {@link BooleanTerm} + */ +public interface BooleanExpressionVisitor { + void visit(Not not); + + void visit(Literal literal); + + void visit(BooleanTerm booleanTerm); + + void visit(And and); +} diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java new file mode 100644 index 00000000..8b2be732 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -0,0 +1,41 @@ +package com.exasol.sql.expression; + +public class BooleanTerm extends AbstractBooleanExpression { + public BooleanTerm(final BooleanExpression nestedExpression) { + super(nestedExpression); + } + + public static BooleanExpression not(final String string) { + return create(new Not(string)); + } + + private static BooleanExpression create(final BooleanExpression nestedExpression) { + final BooleanExpression expression = new BooleanTerm(nestedExpression); + return expression; + } + + public static BooleanExpression not(final BooleanExpression expression) { + return create(new Not(expression)); + } + + public static BooleanExpression and(final String... strings) { + return create(new And(strings)); + } + + public static BooleanExpression and(final BooleanExpression expression, final String string) { + return create(new And(expression, Literal.of(string))); + } + + public static BooleanExpression and(final String literal, final BooleanExpression expression) { + return create(new And(Literal.of(literal), expression)); + } + + public static BooleanExpression and(final BooleanExpression... expressions) { + return create(new And(expressions)); + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Literal.java b/src/main/java/com/exasol/sql/expression/Literal.java new file mode 100644 index 00000000..747d909f --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Literal.java @@ -0,0 +1,29 @@ +package com.exasol.sql.expression; + +public class Literal extends AbstractBooleanExpression { + private final String literal; + + private Literal(final String literal) { + this.literal = literal; + } + + /** + * Create a new {@link Literal} instance from a String + * + * @param string the string to be turned into a literal + * @return new Literal instance + */ + public static Literal of(final String string) { + return new Literal(string); + } + + @Override + public String toString() { + return this.literal; + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Not.java b/src/main/java/com/exasol/sql/expression/Not.java new file mode 100644 index 00000000..37f1b195 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Not.java @@ -0,0 +1,29 @@ +package com.exasol.sql.expression; + +/** + * This class implements the logical unary NOT + */ +public class Not extends AbstractBooleanExpression { + /** + * Create a new instance of a unary {@link Not} from a string literal + * + * @param string string literal to be negated + */ + protected Not(final String string) { + super(Literal.of(string)); + } + + /** + * Create a new instance of a unary {@link Not} from a boolean expression + * + * @param expression boolean expression literal to be negated + */ + public Not(final BooleanExpression expression) { + super(expression); + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java new file mode 100644 index 00000000..882f9c7e --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -0,0 +1,72 @@ +package com.exasol.sql.expression.rendering; + +import com.exasol.sql.expression.*; +import com.exasol.sql.rendering.StringRendererConfig; +import com.exasol.util.TreeNode; + +public class BooleanExpressionRenderer implements BooleanExpressionVisitor { + private final StringRendererConfig config; + private final StringBuilder front = new StringBuilder(); + private final StringBuilder back = new StringBuilder(); + private boolean needSeparator; + + public BooleanExpressionRenderer(final StringRendererConfig config) { + this.config = config; + } + + public BooleanExpressionRenderer() { + this.config = new StringRendererConfig.Builder().build(); + } + + private void appendKeyWord(final String keyword) { + appendSeparatorIfNecessary(); + this.front.append(this.config.produceLowerCase() ? keyword : keyword.toUpperCase()); + this.needSeparator = true; + } + + @Override + public void visit(final Not not) { + appendKeyWord("NOT"); + } + + @Override + public void visit(final And and) { + + } + + @Override + public void visit(final Literal literal) { + if (literal.isChild() && !literal.isFirstSibling()) { + final TreeNode parent = literal.getParent(); + if (parent instanceof And) { + appendKeyWord("AND"); + } + } + appendLiteral(literal.toString()); + } + + private void appendLiteral(final String string) { + appendSeparatorIfNecessary(); + this.front.append(string); + this.needSeparator = true; + } + + private void appendSeparatorIfNecessary() { + if (this.needSeparator) { + this.front.append(" "); + } + } + + @Override + public void visit(final BooleanTerm booleanTerm) { + if (booleanTerm.isChild()) { + this.front.append("("); + this.back.append(")"); + this.needSeparator = false; + } + } + + public String render() { + return this.front.append(this.back).toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/StringRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java similarity index 50% rename from src/main/java/com/exasol/sql/rendering/StringRenderer.java rename to src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index 0a3ce086..b188f79b 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -5,29 +5,29 @@ import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.dql.*; -import com.exasol.util.visitor.AbstractHierarchicalVisitor; +import com.exasol.sql.expression.*; /** - * The {@link StringRenderer} turns SQL statement structures in to SQL strings. + * The {@link SqlStatementRenderer} turns SQL statement structures in to SQL strings. */ -public class StringRenderer extends AbstractHierarchicalVisitor implements FragmentVisitor { +public class SqlStatementRenderer implements FragmentVisitor { private final StringBuilder builder = new StringBuilder(); private final StringRendererConfig config; /** - * Create a new {@link StringRenderer} using the default + * Create a new {@link SqlStatementRenderer} using the default * {@link StringRendererConfig}. */ - public StringRenderer() { + public SqlStatementRenderer() { this(new StringRendererConfig.Builder().build()); } /** - * Create a new {@link StringRenderer} with custom render settings. + * Create a new {@link SqlStatementRenderer} with custom render settings. * * @param config render configuration settings */ - public StringRenderer(final StringRendererConfig config) { + public SqlStatementRenderer(final StringRendererConfig config) { this.config = config; } @@ -42,29 +42,33 @@ public String render() { @Override public void visit(final Select select) { - this.builder.append(produceLowerCase() ? "select" : "SELECT"); + appendKeyWord("select"); } - private boolean produceLowerCase() { - return this.config.produceLowerCase(); + private void appendKeyWord(final String keyWord) { + append(this.config.produceLowerCase() ? keyWord : keyWord.toUpperCase()); + } + + private StringBuilder append(final String string) { + return this.builder.append(string); } @Override public void visit(final Field field) { appendCommaWhenNeeded(field); - this.builder.append(" "); - this.builder.append(field.getName()); + append(" "); + append(field.getName()); } private void appendCommaWhenNeeded(final Fragment fragment) { if (!fragment.isFirstSibling()) { - this.builder.append(","); + append(","); } } @Override public void visit(final FromClause fromClause) { - this.builder.append(produceLowerCase() ? " from" : " FROM"); + appendKeyWord(" from"); } @Override @@ -74,12 +78,12 @@ public void visit(final TableReference tableReference) { @Override public void visit(final Table table) { appendCommaWhenNeeded(table); - this.builder.append(" "); - this.builder.append(table.getName()); + append(" "); + append(table.getName()); final Optional as = table.getAs(); if (as.isPresent()) { - this.builder.append(produceLowerCase() ? " as " : " AS "); - this.builder.append(as.get()); + appendKeyWord(" as "); + append(as.get()); } } @@ -87,12 +91,28 @@ public void visit(final Table table) { public void visit(final Join join) { final JoinType type = join.getType(); if (type != JoinType.DEFAULT) { - this.builder.append(" "); - this.builder.append(produceLowerCase() ? type.toString().toLowerCase() : type.toString()); + append(" "); + appendKeyWord(type.toString()); } - this.builder.append(produceLowerCase() ? " join " : " JOIN "); - this.builder.append(join.getName()); - this.builder.append(produceLowerCase() ? " on " : " ON "); - this.builder.append(join.getSpecification()); + appendKeyWord(" join "); + append(join.getName()); + appendKeyWord(" on "); + append(join.getSpecification()); + } + + @Override + public void visit(final AbstractBooleanExpression expression) { + + } + + @Override + public void visit(final Not not) { + appendKeyWord(" not"); + } + + @Override + public void visit(final Literal literal) { + append(" "); + append(literal.toString()); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java new file mode 100644 index 00000000..469ab0f3 --- /dev/null +++ b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java @@ -0,0 +1,116 @@ +package com.exasol.util; + +import java.util.*; + +/** + * This is an abstract base class for nodes in a tree structure. + */ +public abstract class AbstractBottomUpTreeNode implements TreeNode { + private TreeNode parent = null; + private final List children; + + /** + * Create a new instance of a {@link AbstractBottomUpTreeNode} that serves as + * leaf node for a tree. + */ + public AbstractBottomUpTreeNode() { + this.children = Collections.emptyList(); + } + + /** + * Create a new instance of a {@link AbstractBottomUpTreeNode} that has one + * child. + */ + public AbstractBottomUpTreeNode(final TreeNode child) { + this.children = new ArrayList(); + this.children.add(child); + assignThisAsParentTo(child); + } + + /** + * Create a new instance of a {@link AbstractBottomUpTreeNode}. + * + * @param children child nodes to be linked to this node. + */ + public AbstractBottomUpTreeNode(final List children) { + this.children = children; + for (final TreeNode child : children) { + assignThisAsParentTo(child); + } + } + + /** + * Create a new instance of a {@link AbstractBottomUpTreeNode}. + * + * @param children child nodes to be linked to this node. + */ + public AbstractBottomUpTreeNode(final TreeNode... children) { + this(Arrays.asList(children)); + } + + private void assignThisAsParentTo(final TreeNode child) { + assertChildType(child); + final TreeNode existingParent = child.getParent(); + if (existingParent == null) { + ((AbstractBottomUpTreeNode) child).parent = this; + } else { + assertChildCanAcceptThisAsParent(child, existingParent); + } + } + + private void assertChildType(final TreeNode child) { + if (!(child instanceof AbstractBottomUpTreeNode)) { + throw new IllegalArgumentException("A bottom up tree can only be constructed from nodes of type \"" + + AbstractBottomUpTreeNode.class.getName() + "\" but got an object of type \"" + + child.getClass().getName() + "\""); + } + } + + private void assertChildCanAcceptThisAsParent(final TreeNode child, final TreeNode existingParent) { + if (existingParent != this) { + throw new IllegalStateException( + "Tried to link node \"" + child.toString() + "\" in bottom-up tree to parent \"" + this.toString() + + "\" which already has a parent \"" + existingParent + "\""); + } + } + + @Override + public TreeNode getRoot() { + if (getParent() == null) { + return this; + } else { + return getParent().getRoot(); + } + } + + @Override + public TreeNode getParent() { + return this.parent; + } + + @Override + public void addChild(final TreeNode child) { + throw new UnsupportedOperationException("Node \"" + child.toString() + + "\" can only be added as child node in parent constructor in a bottom-up tree."); + } + + @Override + public List getChildren() { + return this.children; + } + + @Override + public TreeNode getChild(final int index) { + return this.children.get(index); + } + + @Override + public boolean isChild() { + return (this.parent != null); + } + + @Override + public boolean isFirstSibling() { + return (this.parent != null) && (this.getParent().getChild(0) == this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/util/AbstractTreeNode.java b/src/main/java/com/exasol/util/AbstractTreeNode.java new file mode 100644 index 00000000..7f091fbd --- /dev/null +++ b/src/main/java/com/exasol/util/AbstractTreeNode.java @@ -0,0 +1,71 @@ +package com.exasol.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is an abstract base class for nodes in a tree structure. + */ +public abstract class AbstractTreeNode implements TreeNode { + private final TreeNode root; + private final TreeNode parent; + private final List children = new ArrayList<>(); + + /** + * Create a new instance of a {@link AbstractTreeNode} that serves as root for a + * tree. + */ + public AbstractTreeNode() { + this(null); + } + + /** + * Create a new instance of a {@link AbstractTreeNode}. + * + * @param parent the parent to which this node will be linked as a child or + * null if the current node is the root of the tree. + */ + public AbstractTreeNode(final TreeNode parent) { + this.parent = parent; + if (this.parent == null) { + this.root = this; + } else { + this.root = this.parent.getRoot(); + } + } + + @Override + public TreeNode getRoot() { + return this.root; + } + + @Override + public TreeNode getParent() { + return this.parent; + } + + @Override + public void addChild(final TreeNode child) { + this.children.add(child); + } + + @Override + public List getChildren() { + return this.children; + } + + @Override + public TreeNode getChild(final int index) { + return this.children.get(index); + } + + @Override + public boolean isChild() { + return (this.parent != null); + } + + @Override + public boolean isFirstSibling() { + return (this.parent != null) && (this.getParent().getChild(0) == this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/util/TreeNode.java b/src/main/java/com/exasol/util/TreeNode.java new file mode 100644 index 00000000..9894ac08 --- /dev/null +++ b/src/main/java/com/exasol/util/TreeNode.java @@ -0,0 +1,62 @@ +package com.exasol.util; + +import java.util.List; + +/** + * This class represents a node in a tree structure. + */ +public interface TreeNode { + /** + * Get the root of the tree + * + * @return root node + */ + public TreeNode getRoot(); + + /** + * Get the parent of this node + * + * @return parent node + */ + public TreeNode getParent(); + + /** + * Add a child node below this node. Children are registered in the order in + * which they are added. + * + * @param child child node + */ + public void addChild(TreeNode child); + + /** + * Get all child nodes of this node + * + * @param child child nodes + */ + public List getChildren(); + + /** + * Get child node by position in the list of siblings. The position depends on + * the order in which the children were added. + * + * @param index position in the list of siblings + * @return child node at position + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || + * index >= size()) + */ + public TreeNode getChild(int index) throws IndexOutOfBoundsException; + + /** + * Check whether this node is a child node + * + * @return true if the node is a child of another node + */ + public boolean isChild(); + + /** + * Check whether a child is the first in the list of siblings + * + * @return true if the child is the first in the list of siblings + */ + public boolean isFirstSibling(); +} diff --git a/src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java b/src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java deleted file mode 100644 index bdd4a5b8..00000000 --- a/src/main/java/com/exasol/util/visitor/AbstractHierarchicalVisitor.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.exasol.util.visitor; - -import java.util.*; - -/** - * Abstract base class for building visitors that are organized in a hierarchy. - */ -public abstract class AbstractHierarchicalVisitor implements HierarchicalVisitor { - private final Map, HierarchicalVisitor> registeredVisitors = new HashMap<>(); - - @Override - public void register(final HierarchicalVisitor visitor, final Class responsibleFor) { - this.registeredVisitors.put(responsibleFor, visitor); - } - - @Override - public Collection getRegisted() { - return this.registeredVisitors.values(); - } - - @Override - public HierarchicalVisitor getRegisteredVisitorForType(final Visitable host) { - return this.registeredVisitors.get(host.getClass()); - } -} diff --git a/src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java b/src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java deleted file mode 100644 index 1860edd2..00000000 --- a/src/main/java/com/exasol/util/visitor/HierarchicalVisitor.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.exasol.util.visitor; - -import java.util.Collection; - -/** - * This in the interface for visitors that allow plugging in sub-visitors. - */ -public interface HierarchicalVisitor { - /** - * Register a sub-visitor - * - * @param child sub-visitor to be registered - */ - public void register(HierarchicalVisitor child, Class responsibleFor); - - /** - * Get a list of registered sub-visitors - * - * @return list of registered sub-visitors - */ - public Collection getRegisted(); - - HierarchicalVisitor getRegisteredVisitorForType(final Visitable host); - - /** - * Fallback methods that allows delegation of visiting to registered visitors. - * - * @param host object requesting the visit - */ - public void visit(Visitable host); -} \ No newline at end of file diff --git a/src/main/java/com/exasol/util/visitor/Visitable.java b/src/main/java/com/exasol/util/visitor/Visitable.java deleted file mode 100644 index a22cae17..00000000 --- a/src/main/java/com/exasol/util/visitor/Visitable.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.exasol.util.visitor; - -public interface Visitable { - -} diff --git a/src/test/java/com/exasol/dql/TestJoin.java b/src/test/java/com/exasol/dql/rendering/TestJoin.java similarity index 96% rename from src/test/java/com/exasol/dql/TestJoin.java rename to src/test/java/com/exasol/dql/rendering/TestJoin.java index 1e68d4e1..89d4f153 100644 --- a/src/test/java/com/exasol/dql/TestJoin.java +++ b/src/test/java/com/exasol/dql/rendering/TestJoin.java @@ -1,6 +1,6 @@ -package com.exasol.dql; +package com.exasol.dql.rendering; -import static com.exasol.hamcrest.RenderResultMatcher.rendersTo; +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static org.hamcrest.MatcherAssert.assertThat; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/rendering/TestSelect.java similarity index 91% rename from src/test/java/com/exasol/dql/TestSelect.java rename to src/test/java/com/exasol/dql/rendering/TestSelect.java index c437bd41..2efecec4 100644 --- a/src/test/java/com/exasol/dql/TestSelect.java +++ b/src/test/java/com/exasol/dql/rendering/TestSelect.java @@ -1,7 +1,7 @@ -package com.exasol.dql; +package com.exasol.dql.rendering; -import static com.exasol.hamcrest.RenderResultMatcher.rendersTo; -import static com.exasol.hamcrest.RenderResultMatcher.rendersWithConfigTo; +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersWithConfigTo; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/src/test/java/com/exasol/hamcrest/AbstractRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/AbstractRenderResultMatcher.java new file mode 100644 index 00000000..e0c76963 --- /dev/null +++ b/src/test/java/com/exasol/hamcrest/AbstractRenderResultMatcher.java @@ -0,0 +1,19 @@ +package com.exasol.hamcrest; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +public abstract class AbstractRenderResultMatcher extends TypeSafeMatcher { + protected final String expectedText; + protected String renderedText = null; + + public AbstractRenderResultMatcher(final String expectedText) { + this.expectedText = expectedText; + } + + @Override + public void describeTo(final Description description) { + description.appendText(this.expectedText); + } + +} \ No newline at end of file diff --git a/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java new file mode 100644 index 00000000..22549ff6 --- /dev/null +++ b/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java @@ -0,0 +1,66 @@ +package com.exasol.hamcrest; + +import org.hamcrest.Description; + +import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; +import com.exasol.sql.rendering.SqlStatementRenderer; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * This class implements a matcher for the results of rendering boolean + * expressions to text. + */ +public class BooleanExpressionRenderResultMatcher extends AbstractRenderResultMatcher { + private final BooleanExpressionRenderer renderer; + + private BooleanExpressionRenderResultMatcher(final String expectedText) { + super(expectedText); + this.renderer = new BooleanExpressionRenderer(); + } + + private BooleanExpressionRenderResultMatcher(final StringRendererConfig config, final String expectedText) { + super(expectedText); + this.renderer = new BooleanExpressionRenderer(config); + } + + /** + * Match the rendered result against original text. + * + * @param text the text to be matched against the original text. + */ + @Override + public boolean matchesSafely(final BooleanExpression expression) { + expression.accept(this.renderer); + this.renderedText = this.renderer.render(); + return this.renderedText.equals(this.expectedText); + } + + @Override + protected void describeMismatchSafely(final BooleanExpression expression, final Description mismatchDescription) { + mismatchDescription.appendText(this.renderedText); + } + + /** + * Factory method for {@link BooleanExpressionRenderResultMatcher} + * + * @param expectedText text that represents the expected rendering result + * @return the matcher + */ + public static BooleanExpressionRenderResultMatcher rendersTo(final String expectedText) { + return new BooleanExpressionRenderResultMatcher(expectedText); + } + + /** + * Factory method for {@link BooleanExpressionRenderResultMatcher} + * + * @param config configuration settings for the + * {@link SqlStatementRenderer} + * @param expectedText text that represents the expected rendering result + * @return the matcher + */ + public static BooleanExpressionRenderResultMatcher rendersWithConfigTo(final StringRendererConfig config, + final String expectedText) { + return new BooleanExpressionRenderResultMatcher(config, expectedText); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java deleted file mode 100644 index 354399e1..00000000 --- a/src/test/java/com/exasol/hamcrest/RenderResultMatcher.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.exasol.hamcrest; - -import org.hamcrest.Description; -import org.hamcrest.TypeSafeMatcher; - -import com.exasol.sql.Fragment; -import com.exasol.sql.rendering.StringRenderer; -import com.exasol.sql.rendering.StringRendererConfig; - -/** - * This class implements a matcher for multi-line text that helps finding the - * differences more quickly. - */ -public class RenderResultMatcher extends TypeSafeMatcher { - private final String expectedText; - private final StringRenderer renderer; - private String renderedText = null; - - private RenderResultMatcher(final String expectedText) { - this.expectedText = expectedText; - this.renderer = new StringRenderer(); - } - - private RenderResultMatcher(final StringRendererConfig config, final String expectedText) { - this.expectedText = expectedText; - this.renderer = new StringRenderer(config); - } - - /** - * Match the rendered result against original text. - * - * @param text the text to be matched against the original text. - */ - @Override - public boolean matchesSafely(final Fragment fragment) { - fragment.getRoot().accept(this.renderer); - this.renderedText = this.renderer.render(); - return this.renderedText.equals(this.expectedText); - } - - @Override - protected void describeMismatchSafely(final Fragment fragment, final Description mismatchDescription) { - mismatchDescription.appendText(this.renderedText); - } - - @Override - public void describeTo(final Description description) { - description.appendText(this.expectedText); - } - - /** - * Factory method for {@link RenderResultMatcher} - * - * @param expectedText text that represents the expected rendering result - * @return the matcher - */ - public static RenderResultMatcher rendersTo(final String expectedText) { - return new RenderResultMatcher(expectedText); - } - - /** - * Factory method for {@link RenderResultMatcher} - * - * @param config configuration settings for the {@link StringRenderer} - * @param expectedText text that represents the expected rendering result - * @return the matcher - */ - public static RenderResultMatcher rendersWithConfigTo(final StringRendererConfig config, - final String expectedText) { - return new RenderResultMatcher(config, expectedText); - } -} \ No newline at end of file diff --git a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java new file mode 100644 index 00000000..03f54d94 --- /dev/null +++ b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java @@ -0,0 +1,65 @@ +package com.exasol.hamcrest; + +import org.hamcrest.Description; + +import com.exasol.sql.Fragment; +import com.exasol.sql.rendering.SqlStatementRenderer; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * This class implements a matcher for the results of rendering SQL statements + * to text. + */ +public class SqlFragmentRenderResultMatcher extends AbstractRenderResultMatcher { + private final SqlStatementRenderer renderer; + + private SqlFragmentRenderResultMatcher(final String expectedText) { + super(expectedText); + this.renderer = new SqlStatementRenderer(); + } + + private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final String expectedText) { + super(expectedText); + this.renderer = new SqlStatementRenderer(config); + } + + /** + * Match the rendered result against original text. + * + * @param text the text to be matched against the original text. + */ + @Override + public boolean matchesSafely(final Fragment fragment) { + ((Fragment) fragment.getRoot()).accept(this.renderer); + this.renderedText = this.renderer.render(); + return this.renderedText.equals(this.expectedText); + } + + @Override + protected void describeMismatchSafely(final Fragment fragment, final Description mismatchDescription) { + mismatchDescription.appendText(this.renderedText); + } + + /** + * Factory method for {@link SqlFragmentRenderResultMatcher} + * + * @param expectedText text that represents the expected rendering result + * @return the matcher + */ + public static SqlFragmentRenderResultMatcher rendersTo(final String expectedText) { + return new SqlFragmentRenderResultMatcher(expectedText); + } + + /** + * Factory method for {@link SqlFragmentRenderResultMatcher} + * + * @param config configuration settings for the + * {@link SqlStatementRenderer} + * @param expectedText text that represents the expected rendering result + * @return the matcher + */ + public static SqlFragmentRenderResultMatcher rendersWithConfigTo(final StringRendererConfig config, + final String expectedText) { + return new SqlFragmentRenderResultMatcher(config, expectedText); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/TestBooleanExpression.java b/src/test/java/com/exasol/sql/expression/TestBooleanExpression.java deleted file mode 100644 index 2b711143..00000000 --- a/src/test/java/com/exasol/sql/expression/TestBooleanExpression.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.exasol.sql.expression; - -import static com.exasol.hamcrest.RenderResultMatcher.rendersTo; -import static org.hamcrest.MatcherAssert.assertThat; - -import org.junit.jupiter.api.Test; - -class TestBooleanExpression { - @Test - void testUnaryNot() { - final BooleanExpression expression = BooleanExpression.not("foo"); - assertThat(expression, rendersTo("NOT foo")); - } -} diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java new file mode 100644 index 00000000..3afefb9e --- /dev/null +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -0,0 +1,30 @@ +package com.exasol.sql.expression.rendering; + +import static com.exasol.hamcrest.BooleanExpressionRenderResultMatcher.rendersTo; +import static com.exasol.sql.expression.BooleanTerm.and; +import static com.exasol.sql.expression.BooleanTerm.not; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.expression.BooleanExpression; + +class TestBooleanExpressionRenderer { + @Test + void testUnaryNotWithLiteral() { + final BooleanExpression expression = not("foo"); + assertThat(expression, rendersTo("NOT foo")); + } + + @Test + void testUnaryNotWithExpression() { + final BooleanExpression expression = not(not("foo")); + assertThat(expression, rendersTo("NOT(NOT foo)")); + } + + @Test + void testAndWithLiterals() { + final BooleanExpression expression = and("a", "b", "c"); + assertThat(expression, rendersTo("a AND b AND c")); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/util/visitor/DummyChildVisitable.java b/src/test/java/com/exasol/util/visitor/DummyChildVisitable.java deleted file mode 100644 index 1083aa80..00000000 --- a/src/test/java/com/exasol/util/visitor/DummyChildVisitable.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.exasol.util.visitor; - -public class DummyChildVisitable implements Visitable { - private boolean visitedHost; - - public void visit(final DummyChildVisitable host) { - this.visitedHost = true; - } - - public boolean visitedHost() { - return this.visitedHost; - } -} diff --git a/src/test/java/com/exasol/util/visitor/DummyChildVisitor.java b/src/test/java/com/exasol/util/visitor/DummyChildVisitor.java deleted file mode 100644 index 6d388f3c..00000000 --- a/src/test/java/com/exasol/util/visitor/DummyChildVisitor.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.exasol.util.visitor; - -public class DummyChildVisitor extends AbstractHierarchicalVisitor { - private boolean visitedHost; - - public void visit(final DummyChildVisitable host) { - this.visitedHost = true; - } - - public boolean visitedHost() { - return this.visitedHost; - } - - @Override - public void visit(final Visitable host) { - throw new UnsupportedOperationException(); - } -} diff --git a/src/test/java/com/exasol/util/visitor/DummyParentVisitable.java b/src/test/java/com/exasol/util/visitor/DummyParentVisitable.java deleted file mode 100644 index 10cdfce4..00000000 --- a/src/test/java/com/exasol/util/visitor/DummyParentVisitable.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.exasol.util.visitor; - -public class DummyParentVisitable implements Visitable { - -} diff --git a/src/test/java/com/exasol/util/visitor/DummyParentVisitor.java b/src/test/java/com/exasol/util/visitor/DummyParentVisitor.java deleted file mode 100644 index f1952083..00000000 --- a/src/test/java/com/exasol/util/visitor/DummyParentVisitor.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.exasol.util.visitor; - -public class DummyParentVisitor extends AbstractHierarchicalVisitor { - private boolean visitedHost; - - public void visit(final DummyParentVisitable host) { - this.visitedHost = true; - } - - public void visit(final Visitable host) { - getRegisteredVisitorForType(host).visit(host); - } - - public boolean visitedHost() { - return this.visitedHost; - } -} \ No newline at end of file diff --git a/src/test/java/com/exasol/util/visitor/DummyVisitable.java b/src/test/java/com/exasol/util/visitor/DummyVisitable.java deleted file mode 100644 index 9fee50d2..00000000 --- a/src/test/java/com/exasol/util/visitor/DummyVisitable.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.exasol.util.visitor; - -public class DummyVisitable implements Visitable { - -} diff --git a/src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java b/src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java deleted file mode 100644 index 44f2a27c..00000000 --- a/src/test/java/com/exasol/util/visitor/TestHierarchicalVisitor.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.exasol.util.visitor; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class TestHierarchicalVisitor { - private DummyParentVisitor parent; - private DummyChildVisitor child; - - @BeforeEach - void beforeEach() { - this.parent = new DummyParentVisitor(); - this.child = new DummyChildVisitor(); - this.parent.register(this.child, DummyChildVisitable.class); - } - - @Test - void testRegisterSubVisitor() { - assertThat(this.parent.getRegisted(), containsInAnyOrder(this.child)); - } - - @Test - void testVisitInParent() { - final DummyParentVisitable host = new DummyParentVisitable(); - this.parent.visit(host); - assertThat(this.parent.visitedHost(), equalTo(true)); - } - - @Test - void testVisitInParentThenDelegate() { - final DummyChildVisitable host = new DummyChildVisitable(); - this.parent.visit(host); - assertThat(this.parent.visitedHost(), equalTo(false)); - assertThat(this.child.visitedHost(), equalTo(true)); - } -} \ No newline at end of file From 02504d01ee13afc413c4073c7fe89cd4f499a108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Fri, 21 Sep 2018 15:46:11 +0200 Subject: [PATCH 06/43] PMI-66: Basic boolean expression handling complete. --- .../expression/AbstractBooleanExpression.java | 9 ++ .../java/com/exasol/sql/expression/And.java | 28 +++-- .../expression/BooleanExpressionVisitor.java | 12 +++ .../exasol/sql/expression/BooleanTerm.java | 38 +++++-- .../com/exasol/sql/expression/Literal.java | 19 ++++ .../java/com/exasol/sql/expression/Not.java | 9 +- .../java/com/exasol/sql/expression/Or.java | 34 ++++++ .../rendering/BooleanExpressionRenderer.java | 101 +++++++++++++----- .../exasol/util/AbstractBottomUpTreeNode.java | 5 + .../com/exasol/util/AbstractTreeNode.java | 5 + src/main/java/com/exasol/util/TreeNode.java | 9 +- .../TestBooleanExpressionRenderer.java | 62 +++++++++-- 12 files changed, 272 insertions(+), 59 deletions(-) create mode 100644 src/main/java/com/exasol/sql/expression/Or.java diff --git a/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java b/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java index e2a3f896..03040d2c 100644 --- a/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java +++ b/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java @@ -25,6 +25,7 @@ public void accept(final BooleanExpressionVisitor visitor) { for (final TreeNode child : this.getChildren()) { ((BooleanExpression) child).accept(visitor); } + dismissConcrete(visitor); } /** @@ -34,4 +35,12 @@ public void accept(final BooleanExpressionVisitor visitor) { * @param visitor visitor to accept */ public abstract void acceptConcrete(final BooleanExpressionVisitor visitor); + + /** + * Sub-classes must override this method so that the visitor knows the type of + * the visited class at compile time. + * + * @param visitor visitor to accept + */ + public abstract void dismissConcrete(final BooleanExpressionVisitor visitor); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/And.java b/src/main/java/com/exasol/sql/expression/And.java index 31160801..7e1ddd5a 100644 --- a/src/main/java/com/exasol/sql/expression/And.java +++ b/src/main/java/com/exasol/sql/expression/And.java @@ -4,25 +4,31 @@ * This class represents */ public class And extends AbstractBooleanExpression { - + /** + * Create a new {@link And} instance + * + * @param expressions boolean expressions to be connected by a logical AND + */ public And(final BooleanExpression... expressions) { super(expressions); } + /** + * Create a new {@link And} instance + * + * @param strings string literals to be connected by a logical AND + */ public And(final String... strings) { - this(mapLiterals(strings)); - } - - private static BooleanExpression[] mapLiterals(final String[] strings) { - final BooleanExpression[] literals = new BooleanExpression[strings.length]; - for (int i = 0; i < strings.length; ++i) { - literals[i] = Literal.of(strings[i]); - } - return literals; + this(Literal.toBooleanExpressions(strings)); } @Override public void acceptConcrete(final BooleanExpressionVisitor visitor) { visitor.visit(this); } -} + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } +} \ 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 03a08964..9668cba2 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java @@ -11,4 +11,16 @@ public interface BooleanExpressionVisitor { void visit(BooleanTerm booleanTerm); void visit(And and); + + void leave(Not not); + + void leave(Literal literal); + + void leave(BooleanTerm booleanTerm); + + void leave(And and); + + void visit(Or or); + + void leave(Or or); } diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 8b2be732..e61f98d6 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -6,36 +6,52 @@ public BooleanTerm(final BooleanExpression nestedExpression) { } public static BooleanExpression not(final String string) { - return create(new Not(string)); - } - - private static BooleanExpression create(final BooleanExpression nestedExpression) { - final BooleanExpression expression = new BooleanTerm(nestedExpression); - return expression; + return new Not(string); } public static BooleanExpression not(final BooleanExpression expression) { - return create(new Not(expression)); + return new Not(expression); } public static BooleanExpression and(final String... strings) { - return create(new And(strings)); + return new And(strings); } public static BooleanExpression and(final BooleanExpression expression, final String string) { - return create(new And(expression, Literal.of(string))); + return new And(expression, Literal.of(string)); } public static BooleanExpression and(final String literal, final BooleanExpression expression) { - return create(new And(Literal.of(literal), expression)); + return new And(Literal.of(literal), expression); } public static BooleanExpression and(final BooleanExpression... expressions) { - return create(new And(expressions)); + return new And(expressions); + } + + public static BooleanExpression or(final String... strings) { + return new Or(strings); + } + + public static BooleanExpression or(final BooleanExpression expression, final String string) { + return new Or(expression, Literal.of(string)); + } + + public static BooleanExpression or(final String literal, final BooleanExpression expression) { + return new Or(Literal.of(literal), expression); + } + + public static BooleanExpression or(final BooleanExpression... expressions) { + return new Or(expressions); } @Override public void acceptConcrete(final BooleanExpressionVisitor visitor) { visitor.visit(this); } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Literal.java b/src/main/java/com/exasol/sql/expression/Literal.java index 747d909f..879212a5 100644 --- a/src/main/java/com/exasol/sql/expression/Literal.java +++ b/src/main/java/com/exasol/sql/expression/Literal.java @@ -26,4 +26,23 @@ public String toString() { public void acceptConcrete(final BooleanExpressionVisitor visitor) { visitor.visit(this); } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } + + /** + * Map an array of {@link String} to and array of BooleanExpressions + * + * @param strings + * @return + */ + public static BooleanExpression[] toBooleanExpressions(final String[] strings) { + final BooleanExpression[] literals = new BooleanExpression[strings.length]; + for (int i = 0; i < strings.length; ++i) { + literals[i] = Literal.of(strings[i]); + } + return literals; + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Not.java b/src/main/java/com/exasol/sql/expression/Not.java index 37f1b195..f67bddde 100644 --- a/src/main/java/com/exasol/sql/expression/Not.java +++ b/src/main/java/com/exasol/sql/expression/Not.java @@ -6,7 +6,7 @@ public class Not extends AbstractBooleanExpression { /** * Create a new instance of a unary {@link Not} from a string literal - * + * * @param string string literal to be negated */ protected Not(final String string) { @@ -15,7 +15,7 @@ protected Not(final String string) { /** * Create a new instance of a unary {@link Not} from a boolean expression - * + * * @param expression boolean expression literal to be negated */ public Not(final BooleanExpression expression) { @@ -26,4 +26,9 @@ public Not(final BooleanExpression expression) { public void acceptConcrete(final BooleanExpressionVisitor visitor) { visitor.visit(this); } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Or.java b/src/main/java/com/exasol/sql/expression/Or.java new file mode 100644 index 00000000..8fdc36bd --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Or.java @@ -0,0 +1,34 @@ +package com.exasol.sql.expression; + +/** + * This class represents + */ +public class Or extends AbstractBooleanExpression { + /** + * Create a new {@link Or} instance + * + * @param expressions boolean expressions to be connected by a logical Or + */ + public Or(final BooleanExpression... expressions) { + super(expressions); + } + + /** + * Create a new {@link Or} instance + * + * @param strings string literals to be connected by a logical Or + */ + public Or(final String... strings) { + this(Literal.toBooleanExpressions(strings)); + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java index 882f9c7e..5e0f7159 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -1,14 +1,14 @@ package com.exasol.sql.expression.rendering; +import java.util.Stack; + import com.exasol.sql.expression.*; import com.exasol.sql.rendering.StringRendererConfig; -import com.exasol.util.TreeNode; public class BooleanExpressionRenderer implements BooleanExpressionVisitor { private final StringRendererConfig config; - private final StringBuilder front = new StringBuilder(); - private final StringBuilder back = new StringBuilder(); - private boolean needSeparator; + private final StringBuilder builder = new StringBuilder(); + private final Stack connectorStack = new Stack<>(); public BooleanExpressionRenderer(final StringRendererConfig config) { this.config = config; @@ -18,55 +18,100 @@ public BooleanExpressionRenderer() { this.config = new StringRendererConfig.Builder().build(); } - private void appendKeyWord(final String keyword) { - appendSeparatorIfNecessary(); - this.front.append(this.config.produceLowerCase() ? keyword : keyword.toUpperCase()); - this.needSeparator = true; + private void appendKeyword(final String keyword) { + this.builder.append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); } @Override public void visit(final Not not) { - appendKeyWord("NOT"); + connect(not); + appendKeyword("NOT"); + startParenthesis(); + } + + @Override + public void leave(final Not not) { + endParenthesis(not); } @Override public void visit(final And and) { + connect(and); + this.connectorStack.push(" AND "); + if (!and.isRoot()) { + startParenthesis(); + } + } + @Override + public void leave(final And and) { + if (!and.isRoot()) { + endParenthesis(and); + } + this.connectorStack.pop(); } @Override - public void visit(final Literal literal) { - if (literal.isChild() && !literal.isFirstSibling()) { - final TreeNode parent = literal.getParent(); - if (parent instanceof And) { - appendKeyWord("AND"); - } + public void visit(final Or or) { + connect(or); + this.connectorStack.push(" OR "); + if (!or.isRoot()) { + startParenthesis(); } + } + + @Override + public void leave(final Or or) { + if (!or.isRoot()) { + endParenthesis(or); + } + this.connectorStack.pop(); + } + + @Override + public void visit(final Literal literal) { + connect(literal); appendLiteral(literal.toString()); } - private void appendLiteral(final String string) { - appendSeparatorIfNecessary(); - this.front.append(string); - this.needSeparator = true; + private void connect(final BooleanExpression expression) { + if (expression.isChild() && !expression.isFirstSibling()) { + appendConnector(); + } + } + + @Override + public void leave(final Literal literal) { + // intentionally empty } - private void appendSeparatorIfNecessary() { - if (this.needSeparator) { - this.front.append(" "); + private void appendConnector() { + if (!this.connectorStack.isEmpty()) { + appendKeyword(this.connectorStack.peek()); } } + private void appendLiteral(final String string) { + this.builder.append(string); + } + @Override public void visit(final BooleanTerm booleanTerm) { - if (booleanTerm.isChild()) { - this.front.append("("); - this.back.append(")"); - this.needSeparator = false; - } + } + + @Override + public void leave(final BooleanTerm booleanTerm) { + } + + private void startParenthesis() { + this.builder.append("("); + } + + private void endParenthesis(final BooleanExpression expression) { + this.builder.append(")"); } public String render() { - return this.front.append(this.back).toString(); + return this.builder.toString(); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java index 469ab0f3..d8ae342b 100644 --- a/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java +++ b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java @@ -104,6 +104,11 @@ public TreeNode getChild(final int index) { return this.children.get(index); } + @Override + public boolean isRoot() { + return (this == getRoot()); + } + @Override public boolean isChild() { return (this.parent != null); diff --git a/src/main/java/com/exasol/util/AbstractTreeNode.java b/src/main/java/com/exasol/util/AbstractTreeNode.java index 7f091fbd..c7c6f0fa 100644 --- a/src/main/java/com/exasol/util/AbstractTreeNode.java +++ b/src/main/java/com/exasol/util/AbstractTreeNode.java @@ -59,6 +59,11 @@ public TreeNode getChild(final int index) { return this.children.get(index); } + @Override + public boolean isRoot() { + return (this == getRoot()); + } + @Override public boolean isChild() { return (this.parent != null); diff --git a/src/main/java/com/exasol/util/TreeNode.java b/src/main/java/com/exasol/util/TreeNode.java index 9894ac08..32d3ccca 100644 --- a/src/main/java/com/exasol/util/TreeNode.java +++ b/src/main/java/com/exasol/util/TreeNode.java @@ -47,8 +47,15 @@ public interface TreeNode { public TreeNode getChild(int index) throws IndexOutOfBoundsException; /** - * Check whether this node is a child node + * Check whether this node is the root of the tree. * + * @return true if this node is the root + */ + public boolean isRoot(); + + /** + * Check whether this node is a child node + * * @return true if the node is a child of another node */ public boolean isChild(); diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java index 3afefb9e..5d58da54 100644 --- a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -1,25 +1,26 @@ package com.exasol.sql.expression.rendering; import static com.exasol.hamcrest.BooleanExpressionRenderResultMatcher.rendersTo; -import static com.exasol.sql.expression.BooleanTerm.and; -import static com.exasol.sql.expression.BooleanTerm.not; +import static com.exasol.hamcrest.BooleanExpressionRenderResultMatcher.rendersWithConfigTo; +import static com.exasol.sql.expression.BooleanTerm.*; import static org.hamcrest.MatcherAssert.assertThat; import org.junit.jupiter.api.Test; import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.rendering.StringRendererConfig; class TestBooleanExpressionRenderer { @Test void testUnaryNotWithLiteral() { - final BooleanExpression expression = not("foo"); - assertThat(expression, rendersTo("NOT foo")); + final BooleanExpression expression = not("a"); + assertThat(expression, rendersTo("NOT(a)")); } @Test void testUnaryNotWithExpression() { - final BooleanExpression expression = not(not("foo")); - assertThat(expression, rendersTo("NOT(NOT foo)")); + final BooleanExpression expression = not(not("a")); + assertThat(expression, rendersTo("NOT(NOT(a))")); } @Test @@ -27,4 +28,53 @@ void testAndWithLiterals() { final BooleanExpression expression = and("a", "b", "c"); assertThat(expression, rendersTo("a AND b AND c")); } + + @Test + void testAndWithLeftLiteralAndRightExpression() { + final BooleanExpression expression = and("a", not("b")); + assertThat(expression, rendersTo("a AND NOT(b)")); + } + + @Test + void testAndWithLeftExpressionAndRightLiteral() { + final BooleanExpression expression = and(not("a"), "b"); + assertThat(expression, rendersTo("NOT(a) AND b")); + } + + @Test + void testOrWithLiterals() { + final BooleanExpression expression = or("a", "b", "c"); + assertThat(expression, rendersTo("a OR b OR c")); + } + + @Test + void testoRWithLeftLiteralAndRightExpression() { + final BooleanExpression expression = or("a", not("b")); + assertThat(expression, rendersTo("a OR NOT(b)")); + } + + @Test + void testOrWithLeftExpressionAndRightLiteral() { + final BooleanExpression expression = or(not("a"), "b"); + assertThat(expression, rendersTo("NOT(a) OR b")); + } + + @Test + void testOrWhitNestedAnd() { + final BooleanExpression expression = or(and(not("a"), "b"), and("c", "d")); + assertThat(expression, rendersTo("(NOT(a) AND b) OR (c AND d)")); + } + + @Test + void testAndWhitNestedOr() { + final BooleanExpression expression = and(or(not("a"), "b"), or("c", "d")); + assertThat(expression, rendersTo("(NOT(a) OR b) AND (c OR d)")); + } + + @Test + void testAndWhitNestedOrInLowercase() { + final BooleanExpression expression = and(or(not("a"), "b"), or("c", "d")); + final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + assertThat(expression, rendersWithConfigTo(config, "(not(a) or b) and (c or d)")); + } } \ No newline at end of file From 047e229dccf0e7f53d8fb940a425aaa62099426e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 24 Sep 2018 12:22:30 +0200 Subject: [PATCH 07/43] PMI-66: Imporved test coverage. --- pom.xml | 6 + .../sql/rendering/SqlStatementRenderer.java | 16 +-- .../exasol/util/AbstractBottomUpTreeNode.java | 22 +-- .../com/exasol/dql/rendering/TestJoin.java | 9 +- .../exasol/util/DummyBottomUpTreeNode.java | 11 ++ .../java/com/exasol/util/DummyTreeNode.java | 11 ++ .../util/TestAbstractBottomUpTreeNode.java | 129 ++++++++++++++++++ .../com/exasol/util/TestAbstractTreeNode.java | 98 +++++++++++++ 8 files changed, 265 insertions(+), 37 deletions(-) create mode 100644 src/test/java/com/exasol/util/DummyBottomUpTreeNode.java create mode 100644 src/test/java/com/exasol/util/DummyTreeNode.java create mode 100644 src/test/java/com/exasol/util/TestAbstractBottomUpTreeNode.java create mode 100644 src/test/java/com/exasol/util/TestAbstractTreeNode.java diff --git a/pom.xml b/pom.xml index 60e682fc..efa0541c 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,12 @@ ${junit.version} test + + org.junit.platform + junit-platform-launcher + ${junit.platform.version} + test + org.hamcrest hamcrest-all diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index b188f79b..38b9ad75 100644 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -5,10 +5,11 @@ import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.dql.*; -import com.exasol.sql.expression.*; +import com.exasol.sql.expression.AbstractBooleanExpression; /** - * The {@link SqlStatementRenderer} turns SQL statement structures in to SQL strings. + * The {@link SqlStatementRenderer} turns SQL statement structures in to SQL + * strings. */ public class SqlStatementRenderer implements FragmentVisitor { private final StringBuilder builder = new StringBuilder(); @@ -104,15 +105,4 @@ public void visit(final Join join) { public void visit(final AbstractBooleanExpression expression) { } - - @Override - public void visit(final Not not) { - appendKeyWord(" not"); - } - - @Override - public void visit(final Literal literal) { - append(" "); - append(literal.toString()); - } } \ No newline at end of file diff --git a/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java index d8ae342b..a71be4f9 100644 --- a/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java +++ b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java @@ -17,16 +17,6 @@ public AbstractBottomUpTreeNode() { this.children = Collections.emptyList(); } - /** - * Create a new instance of a {@link AbstractBottomUpTreeNode} that has one - * child. - */ - public AbstractBottomUpTreeNode(final TreeNode child) { - this.children = new ArrayList(); - this.children.add(child); - assignThisAsParentTo(child); - } - /** * Create a new instance of a {@link AbstractBottomUpTreeNode}. * @@ -54,7 +44,9 @@ private void assignThisAsParentTo(final TreeNode child) { if (existingParent == null) { ((AbstractBottomUpTreeNode) child).parent = this; } else { - assertChildCanAcceptThisAsParent(child, existingParent); + throw new IllegalStateException( + "Tried to link node \"" + child.toString() + "\" in bottom-up tree to parent \"" + this.toString() + + "\" which already has a parent \"" + existingParent + "\""); } } @@ -66,14 +58,6 @@ private void assertChildType(final TreeNode child) { } } - private void assertChildCanAcceptThisAsParent(final TreeNode child, final TreeNode existingParent) { - if (existingParent != this) { - throw new IllegalStateException( - "Tried to link node \"" + child.toString() + "\" in bottom-up tree to parent \"" + this.toString() - + "\" which already has a parent \"" + existingParent + "\""); - } - } - @Override public TreeNode getRoot() { if (getParent() == null) { diff --git a/src/test/java/com/exasol/dql/rendering/TestJoin.java b/src/test/java/com/exasol/dql/rendering/TestJoin.java index 89d4f153..3b9e0731 100644 --- a/src/test/java/com/exasol/dql/rendering/TestJoin.java +++ b/src/test/java/com/exasol/dql/rendering/TestJoin.java @@ -33,7 +33,7 @@ public void testLeftJoin() { } @Test - public void testRigthJoin() { + public void testRightJoin() { assertThat( StatementFactory.getInstance().select().all().from("left_table").rightJoin("right_table", "left_table.foo_id = right_table.foo_id"), @@ -43,10 +43,9 @@ public void testRigthJoin() { @Test public void testFullJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").fullOuterJoin("right_table", + StatementFactory.getInstance().select().all().from("left_table").fullJoin("right_table", "left_table.foo_id = right_table.foo_id"), - rendersTo( - "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + rendersTo("SELECT * FROM left_table FULL JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test @@ -59,7 +58,7 @@ public void testLeftOuterJoin() { } @Test - public void testRigthOuterJoin() { + public void testRightOuterJoin() { assertThat( StatementFactory.getInstance().select().all().from("left_table").rightOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), diff --git a/src/test/java/com/exasol/util/DummyBottomUpTreeNode.java b/src/test/java/com/exasol/util/DummyBottomUpTreeNode.java new file mode 100644 index 00000000..77a4d426 --- /dev/null +++ b/src/test/java/com/exasol/util/DummyBottomUpTreeNode.java @@ -0,0 +1,11 @@ +package com.exasol.util; + +public class DummyBottomUpTreeNode extends AbstractBottomUpTreeNode { + public DummyBottomUpTreeNode() { + super(); + } + + public DummyBottomUpTreeNode(final TreeNode... children) { + super(children); + } +} diff --git a/src/test/java/com/exasol/util/DummyTreeNode.java b/src/test/java/com/exasol/util/DummyTreeNode.java new file mode 100644 index 00000000..f0bfd5a5 --- /dev/null +++ b/src/test/java/com/exasol/util/DummyTreeNode.java @@ -0,0 +1,11 @@ +package com.exasol.util; + +public class DummyTreeNode extends AbstractTreeNode { + public DummyTreeNode() { + super(); + } + + public DummyTreeNode(final TreeNode parent) { + super(parent); + } +} diff --git a/src/test/java/com/exasol/util/TestAbstractBottomUpTreeNode.java b/src/test/java/com/exasol/util/TestAbstractBottomUpTreeNode.java new file mode 100644 index 00000000..52e8ed04 --- /dev/null +++ b/src/test/java/com/exasol/util/TestAbstractBottomUpTreeNode.java @@ -0,0 +1,129 @@ +package com.exasol.util; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestAbstractBottomUpTreeNode { + private TreeNode node; + + @BeforeEach + void beforeEach() { + this.node = new DummyBottomUpTreeNode(); + } + + @Test + void testIsRootOnRootNode() { + assertTrue(this.node.isRoot()); + } + + @Test + void testIsChildOnRootNode() { + assertFalse(this.node.isChild()); + } + + @Test + void testIsFirstSiblingOnRootNode() { + assertFalse(this.node.isFirstSibling()); + } + + @Test + void testIsRootOnChild() { + new DummyBottomUpTreeNode(this.node); + assertFalse(this.node.isRoot()); + } + + @Test + void testIsChildOnChild() { + new DummyBottomUpTreeNode(this.node); + assertTrue(this.node.isChild()); + } + + @Test + void testIsFirstSiblingOnChild() { + new DummyBottomUpTreeNode(this.node); + assertTrue(this.node.isFirstSibling()); + } + + @Test + void testIsFirstSiblingOnFirstChild() { + new DummyBottomUpTreeNode(this.node, new DummyBottomUpTreeNode()); + assertTrue(this.node.isFirstSibling()); + } + + @Test + void testIsFirstSiblingOnSecondChild() { + new DummyBottomUpTreeNode(new DummyBottomUpTreeNode(), this.node); + assertFalse(this.node.isFirstSibling()); + } + + @Test + void testAddingChildAfterConstructurThrowsExpection() { + assertThrows(UnsupportedOperationException.class, () -> this.node.addChild(new DummyBottomUpTreeNode())); + } + + @Test + void testGetChildren() { + final TreeNode otherNode = new DummyBottomUpTreeNode(); + final TreeNode parent = new DummyBottomUpTreeNode(this.node, otherNode); + assertThat(parent.getChildren(), contains(this.node, otherNode)); + } + + @Test + void testAddingChildToTwoParentsThrowsException() { + new DummyBottomUpTreeNode(this.node); + assertThrows(IllegalStateException.class, () -> new DummyBottomUpTreeNode(this.node)); + } + + @Test + void testAddingWrongChildTypeThrowsException() { + final TreeNode wrongChild = new WrongNodeType(); + assertThrows(IllegalArgumentException.class, () -> new DummyBottomUpTreeNode(wrongChild)); + } + + private static class WrongNodeType implements TreeNode { + @Override + public TreeNode getRoot() { + return null; + } + + @Override + public TreeNode getParent() { + return null; + } + + @Override + public void addChild(final TreeNode child) { + } + + @Override + public List getChildren() { + return null; + } + + @Override + public TreeNode getChild(final int index) throws IndexOutOfBoundsException { + return null; + } + + @Override + public boolean isRoot() { + return false; + } + + @Override + public boolean isChild() { + return false; + } + + @Override + public boolean isFirstSibling() { + return false; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/util/TestAbstractTreeNode.java b/src/test/java/com/exasol/util/TestAbstractTreeNode.java new file mode 100644 index 00000000..629c5e82 --- /dev/null +++ b/src/test/java/com/exasol/util/TestAbstractTreeNode.java @@ -0,0 +1,98 @@ +package com.exasol.util; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class TestAbstractTreeNode { + private TreeNode node; + + @BeforeEach + void beforeEach() { + this.node = new DummyTreeNode(); + } + + @Test + void testIsRootOnRootNode() { + assertTrue(this.node.isRoot()); + } + + @Test + void testIsChildOnRootNode() { + assertFalse(this.node.isChild()); + } + + @Test + void testIsFirstSiblingOnRootNode() { + assertFalse(this.node.isFirstSibling()); + } + + @Test + void testIsRootOnChild() { + final TreeNode child = new DummyTreeNode(this.node); + this.node.addChild(child); + assertFalse(child.isRoot()); + } + + @Test + void testIsChildOnChild() { + final TreeNode child = new DummyTreeNode(this.node); + this.node.addChild(child); + assertTrue(child.isChild()); + } + + @Test + void testIsFirstSiblingOnChild() { + final TreeNode child = new DummyTreeNode(this.node); + this.node.addChild(child); + assertTrue(child.isFirstSibling()); + } + + @Test + void testIsFirstSiblingOnFirstChild() { + final TreeNode child = new DummyTreeNode(this.node); + final TreeNode otherChild = new DummyTreeNode(this.node); + this.node.addChild(child); + this.node.addChild(otherChild); + assertTrue(child.isFirstSibling()); + } + + @Test + void testIsFirstSiblingOnSecondChild() { + final TreeNode child = new DummyTreeNode(this.node); + final TreeNode otherChild = new DummyTreeNode(this.node); + this.node.addChild(child); + this.node.addChild(otherChild); + assertFalse(otherChild.isFirstSibling()); + } + + @Test + void testGetChildren() { + final TreeNode child = new DummyTreeNode(this.node); + final TreeNode otherChild = new DummyTreeNode(this.node); + this.node.addChild(child); + this.node.addChild(otherChild); + assertThat(this.node.getChildren(), contains(child, otherChild)); + } + + @Test + void testGetChild() { + final TreeNode child = new DummyTreeNode(this.node); + final TreeNode otherChild = new DummyTreeNode(this.node); + this.node.addChild(child); + this.node.addChild(otherChild); + assertThat(this.node.getChild(1), equalTo(otherChild)); + } + + @Test + void testGetParent() { + final TreeNode child = new DummyTreeNode(this.node); + this.node.addChild(child); + assertThat(child.getParent(), equalTo(this.node)); + } +} \ No newline at end of file From 544501cebe5cda46ce95a3bce0ad26410362637b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 24 Sep 2018 13:27:28 +0200 Subject: [PATCH 08/43] PMI-66: Raised test coverage to 100%. --- .classpath | 38 +++++++------------ .../java/com/exasol/sql/FragmentVisitor.java | 9 +---- .../sql/dql/BooleanValueExpression.java | 38 +++++++++++++++++++ src/main/java/com/exasol/sql/dql/Select.java | 34 +++++++++++------ .../com/exasol/sql/dql/ValueExpression.java | 18 +++++++++ .../expression/BooleanExpressionVisitor.java | 4 -- .../exasol/sql/expression/BooleanTerm.java | 16 ++------ .../rendering/BooleanExpressionRenderer.java | 8 ---- .../sql/rendering/SqlStatementRenderer.java | 13 +++---- .../com/exasol/dql/rendering/TestSelect.java | 6 +++ 10 files changed, 108 insertions(+), 76 deletions(-) create mode 100644 src/main/java/com/exasol/sql/dql/BooleanValueExpression.java create mode 100644 src/main/java/com/exasol/sql/dql/ValueExpression.java diff --git a/.classpath b/.classpath index 962da8d7..c3b4b716 100644 --- a/.classpath +++ b/.classpath @@ -1,27 +1,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index eae9b03c..3dfe4232 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -1,7 +1,6 @@ package com.exasol.sql; import com.exasol.sql.dql.*; -import com.exasol.sql.expression.*; /** * This interface represents a visitor for SQL statement fragments. @@ -13,15 +12,9 @@ public interface FragmentVisitor { public void visit(FromClause fromClause); - public void visit(TableReference tableReference); - public void visit(Table table); public void visit(Join join); - public void visit(AbstractBooleanExpression booleanExpression); - - public void visit(Not not); - - public void visit(Literal literal); + public void visit(BooleanValueExpression booleanValueExpression); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java new file mode 100644 index 00000000..772526ce --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java @@ -0,0 +1,38 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.Fragment; +import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.expression.BooleanExpression; + +/** + * Boolean value expression + */ +public class BooleanValueExpression extends ValueExpression { + private final BooleanExpression expression; + + /** + * Create a new instance of a {@link BooleanValueExpression} + * + * @param parent parent fragment + * @param expression nested boolean expression + */ + public BooleanValueExpression(final Fragment parent, final BooleanExpression expression) { + super(parent); + this.expression = expression; + + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } + + /** + * Get the boolean expression nested in this value expression + * + * @return nested boolean expression + */ + public BooleanExpression getExpression() { + return this.expression; + } +} diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 7dbbc770..bc2dc56b 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -1,34 +1,35 @@ package com.exasol.sql.dql; import com.exasol.sql.*; +import com.exasol.sql.expression.BooleanExpression; /** * This class implements an SQL {@link Select} statement */ public class Select extends AbstractFragment implements SqlStatement { - public Select(final Fragment parent) { - super(parent); - } - + /** + * Create a new instance of a {@link Select} + */ public Select() { super(); } - @Override - public String toString() { - return "SELECT"; - } - /** - * Create a wildcard field for all involved fields. + * Add a wildcard field for all involved fields. * - * @return this instance for fluent programming + * @return this instance for fluent programming */ public Select all() { addChild(Field.all(this)); return this; } + /** + * Add one or more named fields. + * + * @param names field name + * @return this instance for fluent programming + */ public Select field(final String... names) { for (final String name : names) { addChild(new Field(this, name)); @@ -36,6 +37,17 @@ public Select field(final String... names) { return this; } + /** + * Add a boolean value expression + * + * @param expression boolean value expression + * @return this instance for fluent programming + */ + public Select value(final BooleanExpression expression) { + addChild(new BooleanValueExpression(this, expression)); + return this; + } + @Override public void acceptConcrete(final FragmentVisitor visitor) { visitor.visit(this); diff --git a/src/main/java/com/exasol/sql/dql/ValueExpression.java b/src/main/java/com/exasol/sql/dql/ValueExpression.java new file mode 100644 index 00000000..d9584c88 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/ValueExpression.java @@ -0,0 +1,18 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.Fragment; + +/** + * Abstract base class for all types of value expressions + */ +public abstract class ValueExpression extends AbstractFragment { + /** + * Create a new instance of a {@link ValueExpression} + * + * @param parent parent fragement + */ + public ValueExpression(final Fragment parent) { + super(parent); + } +} \ 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 9668cba2..d2b55684 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java @@ -8,16 +8,12 @@ public interface BooleanExpressionVisitor { void visit(Literal literal); - void visit(BooleanTerm booleanTerm); - void visit(And and); void leave(Not not); void leave(Literal literal); - void leave(BooleanTerm booleanTerm); - void leave(And and); void visit(Or or); diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index e61f98d6..9fad266f 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -1,8 +1,8 @@ package com.exasol.sql.expression; -public class BooleanTerm extends AbstractBooleanExpression { - public BooleanTerm(final BooleanExpression nestedExpression) { - super(nestedExpression); +public abstract class BooleanTerm extends AbstractBooleanExpression { + private BooleanTerm() { + super(); } public static BooleanExpression not(final String string) { @@ -44,14 +44,4 @@ public static BooleanExpression or(final String literal, final BooleanExpression public static BooleanExpression or(final BooleanExpression... expressions) { return new Or(expressions); } - - @Override - public void acceptConcrete(final BooleanExpressionVisitor visitor) { - visitor.visit(this); - } - - @Override - public void dismissConcrete(final BooleanExpressionVisitor visitor) { - visitor.leave(this); - } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java index 5e0f7159..c3e5b55e 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -95,14 +95,6 @@ private void appendLiteral(final String string) { this.builder.append(string); } - @Override - public void visit(final BooleanTerm booleanTerm) { - } - - @Override - public void leave(final BooleanTerm booleanTerm) { - } - private void startParenthesis() { this.builder.append("("); } diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index 38b9ad75..a7864756 100644 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -5,7 +5,7 @@ import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.dql.*; -import com.exasol.sql.expression.AbstractBooleanExpression; +import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; /** * The {@link SqlStatementRenderer} turns SQL statement structures in to SQL @@ -72,10 +72,6 @@ public void visit(final FromClause fromClause) { appendKeyWord(" from"); } - @Override - public void visit(final TableReference tableReference) { - } - @Override public void visit(final Table table) { appendCommaWhenNeeded(table); @@ -102,7 +98,10 @@ public void visit(final Join join) { } @Override - public void visit(final AbstractBooleanExpression expression) { - + public void visit(final BooleanValueExpression value) { + final BooleanExpressionRenderer subRenderer = new BooleanExpressionRenderer(); + value.getExpression().accept(subRenderer); + append(" "); + append(subRenderer.render()); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestSelect.java b/src/test/java/com/exasol/dql/rendering/TestSelect.java index 2efecec4..d473c156 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSelect.java +++ b/src/test/java/com/exasol/dql/rendering/TestSelect.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import com.exasol.sql.dql.StatementFactory; +import com.exasol.sql.expression.BooleanTerm; import com.exasol.sql.rendering.StringRendererConfig; class TestSelect { @@ -65,4 +66,9 @@ void testSelectFromMultipleTableAs() { StatementFactory.getInstance().select().all().fromTableAs("table1", "t1").fromTableAs("table2", "t2"), rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); } + + @Test + void testSelectEmbeddedBooleanExpression() { + assertThat(StatementFactory.getInstance().select().value(BooleanTerm.not("a")), rendersTo("SELECT NOT(a)")); + } } \ No newline at end of file From 556c92a6ad55a76ea3dc90c8918fe5b1449c4f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 26 Sep 2018 14:43:40 +0200 Subject: [PATCH 09/43] PMI-86: LIMIT clause now supports both count and offset. --- README.md | 7 +- doc/system_requirements.md | 19 ++++- .../java/com/exasol/sql/FragmentVisitor.java | 2 + .../sql/{dql => }/StatementFactory.java | 4 +- .../java/com/exasol/sql/dql/FromClause.java | 12 +++ .../java/com/exasol/sql/dql/LimitClause.java | 78 +++++++++++++++++++ .../sql/rendering/SqlStatementRenderer.java | 54 ++++++++++--- .../sql/rendering/StringRendererConfig.java | 2 +- .../com/exasol/dql/rendering/TestJoin.java | 2 +- .../com/exasol/dql/rendering/TestLimit.java | 22 ++++++ .../com/exasol/dql/rendering/TestSelect.java | 2 +- .../rendering/TestSqlStatementRenderer.java | 17 ++++ .../.root/.indexes/properties.version | 1 + .../org.eclipse.core.resources.prefs | 2 + 14 files changed, 205 insertions(+), 19 deletions(-) rename src/main/java/com/exasol/sql/{dql => }/StatementFactory.java (92%) create mode 100644 src/main/java/com/exasol/sql/dql/LimitClause.java create mode 100644 src/test/java/com/exasol/dql/rendering/TestLimit.java create mode 100644 src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java create mode 100644 workspace/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.version create mode 100644 workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs diff --git a/README.md b/README.md index aa1b2e0c..53541900 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,17 @@ ## Usage ```java -SqlStatement statement = StatementFactory.getInstance().select().all().from("foo.bar"); +import com.exasol.sql.StatementFactory; +import com.exasol.sql.SqlStatement; +import com.exasol.sql.rendering.SqlStatementRenderer; SqlStatement statement = StatementFactory.getInstance() .select() .field("name") .from("bar") .join("zoo").on("zoo.bar_id").eq("bar.id") - - +String statementText = SqlStatementRenderer.render(statement); ``` ## Development diff --git a/doc/system_requirements.md b/doc/system_requirements.md index ae4b6d15..b95ee9db 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -1,2 +1,19 @@ * Upper case / lower case -* One line / pretty \ No newline at end of file +* One line / pretty + +SQL features: + +--- + +SELECT +* Fields +* Asterisk ("*") + +FROM + +( INNER / ( LEFT / RIGHT / FULL ) OUTER ) JOIN +* ON + +LIMIT +* offset +* count diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index 3dfe4232..33fad8b1 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -17,4 +17,6 @@ public interface FragmentVisitor { public void visit(Join join); public void visit(BooleanValueExpression booleanValueExpression); + + public void visit(LimitClause limitClause); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/StatementFactory.java b/src/main/java/com/exasol/sql/StatementFactory.java similarity index 92% rename from src/main/java/com/exasol/sql/dql/StatementFactory.java rename to src/main/java/com/exasol/sql/StatementFactory.java index 3751a088..865ea05c 100644 --- a/src/main/java/com/exasol/sql/dql/StatementFactory.java +++ b/src/main/java/com/exasol/sql/StatementFactory.java @@ -1,4 +1,6 @@ -package com.exasol.sql.dql; +package com.exasol.sql; + +import com.exasol.sql.dql.Select; /** * The {@link StatementFactory} implements an factory for SQL statements. diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index f5d44d69..b1e377f2 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -73,4 +73,16 @@ public FromClause fullOuterJoin(final String name, final String specification) { addChild(new Join(this, JoinType.FULL_OUTER, name, specification)); return this; } + + public LimitClause limit(final int count) { + final LimitClause limitClause = new LimitClause(this, count); + addChild(limitClause); + return limitClause; + } + + public LimitClause limit(final int offset, final int count) { + final LimitClause limitClause = new LimitClause(this, offset, count); + addChild(limitClause); + return limitClause; + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java new file mode 100644 index 00000000..e406ab32 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -0,0 +1,78 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +/** + * This class represents the limit clause of an SQL statement. It lets you + * choose offset and / or count of rows to be handed back in the result. + */ +public class LimitClause extends AbstractFragment { + private final int count; + private final int offset; + + /** + * Create a new instance of a {@link LimitClause} + * + * @param parent parent SQL statement fragment + * + * @param count maximum number of rows to be handed back + */ + public LimitClause(final Fragment parent, final int count) { + this(parent, 0, count); + } + + /** + * Create a new instance of a {@link LimitClause} + * + * @param parent parent SQL statement fragment + * @param offset first row to be handed back + * + * @param count maximum number of rows to be handed back + */ + public LimitClause(final Fragment parent, final int offset, final int count) { + super(parent); + this.offset = offset; + this.count = count; + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } + + /** + * Get the offset row for the limit + * + * @return first row which should be handed back + */ + public int getOffset() { + return this.offset; + } + + /** + * Get the maximum number of rows to be handed back + * + * @return maximum number of rows + */ + public int getCount() { + return this.count; + } + + /** + * Check if the limit clause has an offset + * + * @return true if the limit clause has an offset + */ + public boolean hasOffset() { + return this.offset > 0; + } + + /** + * Check if the limit clause has a count + * + * @return true if the limit clause has a count + */ + public boolean hasCount() { + return this.count > 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index a7864756..bbcc1684 100644 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -43,11 +43,11 @@ public String render() { @Override public void visit(final Select select) { - appendKeyWord("select"); + appendKeyWord("SELECT"); } - private void appendKeyWord(final String keyWord) { - append(this.config.produceLowerCase() ? keyWord : keyWord.toUpperCase()); + private void appendKeyWord(final String keyword) { + append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); } private StringBuilder append(final String string) { @@ -57,10 +57,14 @@ private StringBuilder append(final String string) { @Override public void visit(final Field field) { appendCommaWhenNeeded(field); - append(" "); + appendSpace(); append(field.getName()); } + private void appendSpace() { + append(" "); + } + private void appendCommaWhenNeeded(final Fragment fragment) { if (!fragment.isFirstSibling()) { append(","); @@ -69,17 +73,17 @@ private void appendCommaWhenNeeded(final Fragment fragment) { @Override public void visit(final FromClause fromClause) { - appendKeyWord(" from"); + appendKeyWord(" FROM"); } @Override public void visit(final Table table) { appendCommaWhenNeeded(table); - append(" "); + appendSpace(); append(table.getName()); final Optional as = table.getAs(); if (as.isPresent()) { - appendKeyWord(" as "); + appendKeyWord(" AS "); append(as.get()); } } @@ -88,12 +92,12 @@ public void visit(final Table table) { public void visit(final Join join) { final JoinType type = join.getType(); if (type != JoinType.DEFAULT) { - append(" "); + appendSpace(); appendKeyWord(type.toString()); } - appendKeyWord(" join "); + appendKeyWord(" JOIN "); append(join.getName()); - appendKeyWord(" on "); + appendKeyWord(" ON "); append(join.getSpecification()); } @@ -101,7 +105,35 @@ public void visit(final Join join) { public void visit(final BooleanValueExpression value) { final BooleanExpressionRenderer subRenderer = new BooleanExpressionRenderer(); value.getExpression().accept(subRenderer); - append(" "); + appendSpace(); append(subRenderer.render()); } + + @Override + public void visit(final LimitClause limit) { + appendKeyWord(" LIMIT "); + if (limit.hasCount()) { + append(limit.getCount()); + } + if (limit.hasOffset()) { + appendKeyWord(" OFFSET "); + append(limit.getOffset()); + } + } + + private void append(final int number) { + this.builder.append(number); + } + + /** + * Create a renderer for the given {@link Fragment} and render it. + * + * @param fragment SQL statement fragment to be rendered + * @return rendered statement + */ + public static String render(final Fragment fragment) { + final SqlStatementRenderer renderer = new SqlStatementRenderer(); + ((Fragment) fragment.getRoot()).accept(renderer); + return renderer.render(); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java index 591ed3f3..2ab8bf7d 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java +++ b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java @@ -1,6 +1,6 @@ package com.exasol.sql.rendering; -import com.exasol.sql.dql.StatementFactory; +import com.exasol.sql.StatementFactory; /** * This class implements a parameter object containing the configuration options diff --git a/src/test/java/com/exasol/dql/rendering/TestJoin.java b/src/test/java/com/exasol/dql/rendering/TestJoin.java index 3b9e0731..fed3eb20 100644 --- a/src/test/java/com/exasol/dql/rendering/TestJoin.java +++ b/src/test/java/com/exasol/dql/rendering/TestJoin.java @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Test; -import com.exasol.sql.dql.StatementFactory; +import com.exasol.sql.StatementFactory; class TestJoin { @Test diff --git a/src/test/java/com/exasol/dql/rendering/TestLimit.java b/src/test/java/com/exasol/dql/rendering/TestLimit.java new file mode 100644 index 00000000..f0ed4df4 --- /dev/null +++ b/src/test/java/com/exasol/dql/rendering/TestLimit.java @@ -0,0 +1,22 @@ +package com.exasol.dql.rendering; + +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; + +class TestLimit { + @Test + void testLimitCountAfterFrom() { + assertThat(StatementFactory.getInstance().select().all().from("t").limit(1), + rendersTo("SELECT * FROM t LIMIT 1")); + } + + @Test + void testLimitOffsetCountAfterFrom() { + assertThat(StatementFactory.getInstance().select().all().from("t").limit(2, 3), + rendersTo("SELECT * FROM t LIMIT 3 OFFSET 2")); + } +} diff --git a/src/test/java/com/exasol/dql/rendering/TestSelect.java b/src/test/java/com/exasol/dql/rendering/TestSelect.java index d473c156..472d980b 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSelect.java +++ b/src/test/java/com/exasol/dql/rendering/TestSelect.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; -import com.exasol.sql.dql.StatementFactory; +import com.exasol.sql.StatementFactory; import com.exasol.sql.expression.BooleanTerm; import com.exasol.sql.rendering.StringRendererConfig; diff --git a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java b/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java new file mode 100644 index 00000000..0771ae54 --- /dev/null +++ b/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java @@ -0,0 +1,17 @@ +package com.exasol.dql.rendering; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; +import com.exasol.sql.rendering.SqlStatementRenderer; + +class TestSqlStatementRenderer { + @Test + void testCreateAndRender() { + assertThat(SqlStatementRenderer.render(StatementFactory.getInstance().select().all().from("foo")), + equalTo("SELECT * FROM foo")); + } +} \ No newline at end of file diff --git a/workspace/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.version b/workspace/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.version new file mode 100644 index 00000000..6b2aaa76 --- /dev/null +++ b/workspace/.metadata/.plugins/org.eclipse.core.resources/.root/.indexes/properties.version @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs b/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..dffc6b51 --- /dev/null +++ b/workspace/.metadata/.plugins/org.eclipse.core.runtime/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +version=1 From a793f903d6f92523737da2ffb61d504fa697af87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 26 Sep 2018 15:56:16 +0200 Subject: [PATCH 10/43] PMI-86: Simplified double linking. --- .../java/com/exasol/sql/AbstractFragment.java | 4 - .../sql/dql/BooleanValueExpression.java | 8 +- src/main/java/com/exasol/sql/dql/Field.java | 11 +- .../java/com/exasol/sql/dql/FromClause.java | 148 +++++++++++++----- src/main/java/com/exasol/sql/dql/Join.java | 9 +- .../java/com/exasol/sql/dql/LimitClause.java | 20 +-- src/main/java/com/exasol/sql/dql/Select.java | 12 +- src/main/java/com/exasol/sql/dql/Table.java | 20 ++- .../com/exasol/sql/dql/ValueExpression.java | 7 +- .../com/exasol/util/AbstractTreeNode.java | 26 +-- src/main/java/com/exasol/util/TreeNode.java | 9 +- .../java/com/exasol/util/DummyTreeNode.java | 4 - .../com/exasol/util/TestAbstractTreeNode.java | 24 +-- 13 files changed, 191 insertions(+), 111 deletions(-) diff --git a/src/main/java/com/exasol/sql/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java index 2d9d55a4..64c47657 100644 --- a/src/main/java/com/exasol/sql/AbstractFragment.java +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -14,10 +14,6 @@ protected AbstractFragment() { super(); } - protected AbstractFragment(final Fragment parent) { - super(parent); - } - @Override public void accept(final FragmentVisitor visitor) { acceptConcrete(visitor); diff --git a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java index 772526ce..e95d16eb 100644 --- a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java +++ b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java @@ -1,6 +1,5 @@ package com.exasol.sql.dql; -import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.expression.BooleanExpression; @@ -13,11 +12,10 @@ public class BooleanValueExpression extends ValueExpression { /** * Create a new instance of a {@link BooleanValueExpression} * - * @param parent parent fragment * @param expression nested boolean expression */ - public BooleanValueExpression(final Fragment parent, final BooleanExpression expression) { - super(parent); + public BooleanValueExpression(final BooleanExpression expression) { + super(); this.expression = expression; } @@ -29,7 +27,7 @@ protected void acceptConcrete(final FragmentVisitor visitor) { /** * Get the boolean expression nested in this value expression - * + * * @return nested boolean expression */ public BooleanExpression getExpression() { diff --git a/src/main/java/com/exasol/sql/dql/Field.java b/src/main/java/com/exasol/sql/dql/Field.java index 98a38961..89770a2a 100644 --- a/src/main/java/com/exasol/sql/dql/Field.java +++ b/src/main/java/com/exasol/sql/dql/Field.java @@ -1,12 +1,13 @@ package com.exasol.sql.dql; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; public class Field extends AbstractFragment implements FieldDefinition { private final String name; - protected Field(final Fragment parent, final String name) { - super(parent); + protected Field(final String name) { + super(); this.name = name; } @@ -14,8 +15,8 @@ public String getName() { return this.name; } - public static Field all(final Fragment parent) { - return new Field(parent, "*"); + public static Field all() { + return new Field("*"); } @Override diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index b1e377f2..bd2a0087 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -1,88 +1,166 @@ package com.exasol.sql.dql; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; +/** + * This class represents the FROM clause of an SQL SELECT statement. + */ public class FromClause extends AbstractFragment { - public FromClause(final Fragment parent) { - super(parent); - } - - public static FromClause table(final Fragment parent, final String name) { - final FromClause fromClause = new FromClause(parent); - fromClause.addChild(new Table(fromClause, name)); - return fromClause; - } - - public static FromClause tableAs(final Fragment parent, final String name, final String as) { - final FromClause fromClause = new FromClause(parent); - fromClause.addChild(new Table(fromClause, name, as)); - return fromClause; - } - + /** + * Create a new instance of a {@link FromClause} + */ + public FromClause() { + super(); + } + + /** + * Create a {@link FromClause} from a table name + * + * @param name table name + * @return new instance + */ public FromClause from(final String name) { - addChild(new Table(this, name)); + addChild(new Table(name)); return this; } - public Fragment fromTableAs(final String name, final String as) { - addChild(new Table(this, name, as)); + /** + * Create a {@link FromClause} from a table name and an alias + * + * @param name table name + * @param as table alias + * @return new instance + */ + public FromClause fromTableAs(final String name, final String as) { + addChild(new Table(name, as)); return this; } - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } - + /** + * Create a new {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause join(final String name, final String specification) { - addChild(new Join(this, JoinType.DEFAULT, name, specification)); + addChild(new Join(JoinType.DEFAULT, name, specification)); return this; } + /** + * Create a new inner {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause innerJoin(final String name, final String specification) { - addChild(new Join(this, JoinType.INNER, name, specification)); + addChild(new Join(JoinType.INNER, name, specification)); return this; } + /** + * Create a new left {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause leftJoin(final String name, final String specification) { - addChild(new Join(this, JoinType.LEFT, name, specification)); + addChild(new Join(JoinType.LEFT, name, specification)); return this; } + /** + * Create a new right {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause rightJoin(final String name, final String specification) { - addChild(new Join(this, JoinType.RIGHT, name, specification)); + addChild(new Join(JoinType.RIGHT, name, specification)); return this; } + /** + * Create a new full {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause fullJoin(final String name, final String specification) { - addChild(new Join(this, JoinType.FULL, name, specification)); + addChild(new Join(JoinType.FULL, name, specification)); return this; } + /** + * Create a new left outer {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause leftOuterJoin(final String name, final String specification) { - addChild(new Join(this, JoinType.LEFT_OUTER, name, specification)); + addChild(new Join(JoinType.LEFT_OUTER, name, specification)); return this; } + /** + * Create a new right outer {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause rightOuterJoin(final String name, final String specification) { - addChild(new Join(this, JoinType.RIGHT_OUTER, name, specification)); + addChild(new Join(JoinType.RIGHT_OUTER, name, specification)); return this; } + /** + * Create a new full outer {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ public FromClause fullOuterJoin(final String name, final String specification) { - addChild(new Join(this, JoinType.FULL_OUTER, name, specification)); + addChild(new Join(JoinType.FULL_OUTER, name, specification)); return this; } + /** + * Create a new full outer {@link LimitClause} + * + * @param count maximum number of rows to be included in query result + * @return new instance + */ public LimitClause limit(final int count) { - final LimitClause limitClause = new LimitClause(this, count); + final LimitClause limitClause = new LimitClause(count); addChild(limitClause); return limitClause; } + /** + * Create a new full outer {@link LimitClause} + * + * @param offset index of the first row in the query result + * @param count maximum number of rows to be included in query result + * @return new instance + */ + public LimitClause limit(final int offset, final int count) { - final LimitClause limitClause = new LimitClause(this, offset, count); + final LimitClause limitClause = new LimitClause(offset, count); addChild(limitClause); return limitClause; } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Join.java b/src/main/java/com/exasol/sql/dql/Join.java index 7573974e..7eaadf2f 100644 --- a/src/main/java/com/exasol/sql/dql/Join.java +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -13,13 +13,12 @@ public class Join extends AbstractFragment implements Fragment { /** * Create a new {@link Join} instance * - * @param parent parent {@link Fragment} - * @param type type of join (e.g. INNER, LEFT or RIGHT OUTER) - * @param name name of the table to be joined + * @param type type of join (e.g. INNER, LEFT or RIGHT OUTER) + * @param name name of the table to be joined * @param specification join specification (e.g. ON or USING) */ - public Join(final Fragment parent, final JoinType type, final String name, final String specification) { - super(parent); + public Join(final JoinType type, final String name, final String specification) { + super(); this.type = type; this.name = name; this.specification = specification; diff --git a/src/main/java/com/exasol/sql/dql/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java index e406ab32..9e35e48c 100644 --- a/src/main/java/com/exasol/sql/dql/LimitClause.java +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -1,6 +1,7 @@ package com.exasol.sql.dql; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; /** * This class represents the limit clause of an SQL statement. It lets you @@ -13,24 +14,23 @@ public class LimitClause extends AbstractFragment { /** * Create a new instance of a {@link LimitClause} * - * @param parent parent SQL statement fragment + * @param offset index of the first row to be included in the query result * - * @param count maximum number of rows to be handed back + * @param count maximum number of rows to be included in the query result */ - public LimitClause(final Fragment parent, final int count) { - this(parent, 0, count); + public LimitClause(final int count) { + this(0, count); } /** * Create a new instance of a {@link LimitClause} * - * @param parent parent SQL statement fragment - * @param offset first row to be handed back + * @param offset index of the first row to be included in the query result * - * @param count maximum number of rows to be handed back + * @param count maximum number of rows to be included in the query result */ - public LimitClause(final Fragment parent, final int offset, final int count) { - super(parent); + public LimitClause(final int offset, final int count) { + super(); this.offset = offset; this.count = count; } diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index bc2dc56b..2f384302 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -20,7 +20,7 @@ public Select() { * @return this instance for fluent programming */ public Select all() { - addChild(Field.all(this)); + addChild(Field.all()); return this; } @@ -32,7 +32,7 @@ public Select all() { */ public Select field(final String... names) { for (final String name : names) { - addChild(new Field(this, name)); + addChild(new Field(name)); } return this; } @@ -44,7 +44,7 @@ public Select field(final String... names) { * @return this instance for fluent programming */ public Select value(final BooleanExpression expression) { - addChild(new BooleanValueExpression(this, expression)); + addChild(new BooleanValueExpression(expression)); return this; } @@ -60,7 +60,7 @@ public void acceptConcrete(final FragmentVisitor visitor) { * @return the FROM clause */ public FromClause from(final String name) { - final FromClause from = FromClause.table(this, name); + final FromClause from = new FromClause().from(name); addChild(from); return from; } @@ -70,11 +70,11 @@ public FromClause from(final String name) { * its name * * @param name table reference name - * @param as table correlation name + * @param as table correlation name * @return the FROM clause */ public FromClause fromTableAs(final String name, final String as) { - final FromClause from = FromClause.tableAs(this, name, as); + final FromClause from = new FromClause().fromTableAs(name, as); addChild(from); return from; } diff --git a/src/main/java/com/exasol/sql/dql/Table.java b/src/main/java/com/exasol/sql/dql/Table.java index 6b8223a3..486142f3 100644 --- a/src/main/java/com/exasol/sql/dql/Table.java +++ b/src/main/java/com/exasol/sql/dql/Table.java @@ -2,7 +2,8 @@ import java.util.Optional; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; /** * This class represents a {@link Table} in an SQL Statement @@ -14,17 +15,22 @@ public class Table extends AbstractFragment implements TableReference { /** * Create a new {@link Table} * - * @param parent parent SQL fragment - * @param name table name + * @param name table name */ - public Table(final Fragment parent, final String name) { - super(parent); + public Table(final String name) { + super(); this.name = name; this.as = Optional.empty(); } - public Table(final Fragment parent, final String name, final String as) { - super(parent); + /** + * Create a new {@link Table} with a name and an alias + * + * @param name table name + * @param as table alias + */ + public Table(final String name, final String as) { + super(); this.name = name; this.as = Optional.of(as); } diff --git a/src/main/java/com/exasol/sql/dql/ValueExpression.java b/src/main/java/com/exasol/sql/dql/ValueExpression.java index d9584c88..3ec57fd5 100644 --- a/src/main/java/com/exasol/sql/dql/ValueExpression.java +++ b/src/main/java/com/exasol/sql/dql/ValueExpression.java @@ -1,7 +1,6 @@ package com.exasol.sql.dql; import com.exasol.sql.AbstractFragment; -import com.exasol.sql.Fragment; /** * Abstract base class for all types of value expressions @@ -9,10 +8,8 @@ public abstract class ValueExpression extends AbstractFragment { /** * Create a new instance of a {@link ValueExpression} - * - * @param parent parent fragement */ - public ValueExpression(final Fragment parent) { - super(parent); + public ValueExpression() { + super(); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/util/AbstractTreeNode.java b/src/main/java/com/exasol/util/AbstractTreeNode.java index c7c6f0fa..02227d82 100644 --- a/src/main/java/com/exasol/util/AbstractTreeNode.java +++ b/src/main/java/com/exasol/util/AbstractTreeNode.java @@ -7,8 +7,8 @@ * This is an abstract base class for nodes in a tree structure. */ public abstract class AbstractTreeNode implements TreeNode { - private final TreeNode root; - private final TreeNode parent; + private TreeNode root; + private TreeNode parent; private final List children = new ArrayList<>(); /** @@ -16,20 +16,25 @@ public abstract class AbstractTreeNode implements TreeNode { * tree. */ public AbstractTreeNode() { - this(null); + this.root = this; + this.parent = null; } /** - * Create a new instance of a {@link AbstractTreeNode}. + * Link to a parent node * - * @param parent the parent to which this node will be linked as a child or - * null if the current node is the root of the tree. + * @param parent the parent to which this node will be linked as a child + * + * @throws IllegalArgumentException if parent is null or parent and + * child are identical */ - public AbstractTreeNode(final TreeNode parent) { - this.parent = parent; - if (this.parent == null) { - this.root = this; + public void setParent(final TreeNode parent) throws IllegalArgumentException { + if (parent == null) { + throw new IllegalArgumentException("Parent tree node cannot be NULL."); + } else if (parent == this) { + throw new IllegalArgumentException("Parent tree node cannot be the same as child tree node."); } else { + this.parent = parent; this.root = this.parent.getRoot(); } } @@ -47,6 +52,7 @@ public TreeNode getParent() { @Override public void addChild(final TreeNode child) { this.children.add(child); + ((AbstractTreeNode) child).setParent(this); } @Override diff --git a/src/main/java/com/exasol/util/TreeNode.java b/src/main/java/com/exasol/util/TreeNode.java index 32d3ccca..3ac7d172 100644 --- a/src/main/java/com/exasol/util/TreeNode.java +++ b/src/main/java/com/exasol/util/TreeNode.java @@ -23,6 +23,9 @@ public interface TreeNode { /** * Add a child node below this node. Children are registered in the order in * which they are added. + *

+ * Important: this also automatically creates a link in the + * opposite direction. All implementations must adhere to this rule. * * @param child child node */ @@ -42,13 +45,13 @@ public interface TreeNode { * @param index position in the list of siblings * @return child node at position * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || - * index >= size()) + * index >= size()) */ public TreeNode getChild(int index) throws IndexOutOfBoundsException; /** * Check whether this node is the root of the tree. - * + * * @return true if this node is the root */ public boolean isRoot(); @@ -66,4 +69,4 @@ public interface TreeNode { * @return true if the child is the first in the list of siblings */ public boolean isFirstSibling(); -} +} \ No newline at end of file diff --git a/src/test/java/com/exasol/util/DummyTreeNode.java b/src/test/java/com/exasol/util/DummyTreeNode.java index f0bfd5a5..9ef5afb7 100644 --- a/src/test/java/com/exasol/util/DummyTreeNode.java +++ b/src/test/java/com/exasol/util/DummyTreeNode.java @@ -4,8 +4,4 @@ public class DummyTreeNode extends AbstractTreeNode { public DummyTreeNode() { super(); } - - public DummyTreeNode(final TreeNode parent) { - super(parent); - } } diff --git a/src/test/java/com/exasol/util/TestAbstractTreeNode.java b/src/test/java/com/exasol/util/TestAbstractTreeNode.java index 629c5e82..83c25746 100644 --- a/src/test/java/com/exasol/util/TestAbstractTreeNode.java +++ b/src/test/java/com/exasol/util/TestAbstractTreeNode.java @@ -34,29 +34,29 @@ void testIsFirstSiblingOnRootNode() { @Test void testIsRootOnChild() { - final TreeNode child = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); this.node.addChild(child); assertFalse(child.isRoot()); } @Test void testIsChildOnChild() { - final TreeNode child = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); this.node.addChild(child); assertTrue(child.isChild()); } @Test void testIsFirstSiblingOnChild() { - final TreeNode child = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); this.node.addChild(child); assertTrue(child.isFirstSibling()); } @Test void testIsFirstSiblingOnFirstChild() { - final TreeNode child = new DummyTreeNode(this.node); - final TreeNode otherChild = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); + final TreeNode otherChild = new DummyTreeNode(); this.node.addChild(child); this.node.addChild(otherChild); assertTrue(child.isFirstSibling()); @@ -64,8 +64,8 @@ void testIsFirstSiblingOnFirstChild() { @Test void testIsFirstSiblingOnSecondChild() { - final TreeNode child = new DummyTreeNode(this.node); - final TreeNode otherChild = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); + final TreeNode otherChild = new DummyTreeNode(); this.node.addChild(child); this.node.addChild(otherChild); assertFalse(otherChild.isFirstSibling()); @@ -73,8 +73,8 @@ void testIsFirstSiblingOnSecondChild() { @Test void testGetChildren() { - final TreeNode child = new DummyTreeNode(this.node); - final TreeNode otherChild = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); + final TreeNode otherChild = new DummyTreeNode(); this.node.addChild(child); this.node.addChild(otherChild); assertThat(this.node.getChildren(), contains(child, otherChild)); @@ -82,8 +82,8 @@ void testGetChildren() { @Test void testGetChild() { - final TreeNode child = new DummyTreeNode(this.node); - final TreeNode otherChild = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); + final TreeNode otherChild = new DummyTreeNode(); this.node.addChild(child); this.node.addChild(otherChild); assertThat(this.node.getChild(1), equalTo(otherChild)); @@ -91,7 +91,7 @@ void testGetChild() { @Test void testGetParent() { - final TreeNode child = new DummyTreeNode(this.node); + final TreeNode child = new DummyTreeNode(); this.node.addChild(child); assertThat(child.getParent(), equalTo(this.node)); } From ec275e69903e38d2c4be20fdede7962313387d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 26 Sep 2018 16:18:49 +0200 Subject: [PATCH 11/43] PMI-86: Added test coverage for LIMIT. --- src/main/java/com/exasol/sql/dql/FromClause.java | 1 - .../java/com/exasol/sql/dql/LimitClause.java | 9 --------- .../sql/rendering/SqlStatementRenderer.java | 6 ++---- .../java/com/exasol/dql/rendering/TestLimit.java | 4 ++-- .../com/exasol/util/TestAbstractTreeNode.java | 16 ++++++++++++++-- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index bd2a0087..38b8254d 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -152,7 +152,6 @@ public LimitClause limit(final int count) { * @param count maximum number of rows to be included in query result * @return new instance */ - public LimitClause limit(final int offset, final int count) { final LimitClause limitClause = new LimitClause(offset, count); addChild(limitClause); diff --git a/src/main/java/com/exasol/sql/dql/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java index 9e35e48c..741d15b9 100644 --- a/src/main/java/com/exasol/sql/dql/LimitClause.java +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -66,13 +66,4 @@ public int getCount() { public boolean hasOffset() { return this.offset > 0; } - - /** - * Check if the limit clause has a count - * - * @return true if the limit clause has a count - */ - public boolean hasCount() { - return this.count > 0; - } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index bbcc1684..71dbf2e1 100644 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -112,13 +112,11 @@ public void visit(final BooleanValueExpression value) { @Override public void visit(final LimitClause limit) { appendKeyWord(" LIMIT "); - if (limit.hasCount()) { - append(limit.getCount()); - } if (limit.hasOffset()) { - appendKeyWord(" OFFSET "); append(limit.getOffset()); + appendKeyWord(", "); } + append(limit.getCount()); } private void append(final int number) { diff --git a/src/test/java/com/exasol/dql/rendering/TestLimit.java b/src/test/java/com/exasol/dql/rendering/TestLimit.java index f0ed4df4..02d6d7dc 100644 --- a/src/test/java/com/exasol/dql/rendering/TestLimit.java +++ b/src/test/java/com/exasol/dql/rendering/TestLimit.java @@ -17,6 +17,6 @@ void testLimitCountAfterFrom() { @Test void testLimitOffsetCountAfterFrom() { assertThat(StatementFactory.getInstance().select().all().from("t").limit(2, 3), - rendersTo("SELECT * FROM t LIMIT 3 OFFSET 2")); + rendersTo("SELECT * FROM t LIMIT 2, 3")); } -} +} \ No newline at end of file diff --git a/src/test/java/com/exasol/util/TestAbstractTreeNode.java b/src/test/java/com/exasol/util/TestAbstractTreeNode.java index 83c25746..31c20636 100644 --- a/src/test/java/com/exasol/util/TestAbstractTreeNode.java +++ b/src/test/java/com/exasol/util/TestAbstractTreeNode.java @@ -3,8 +3,7 @@ import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -95,4 +94,17 @@ void testGetParent() { this.node.addChild(child); assertThat(child.getParent(), equalTo(this.node)); } + + @Test + void testSetParentToNullThrowsException() { + assertThrows(IllegalArgumentException.class, () -> new DummyTreeNode().setParent(null)); + } + + @Test + void testSetParentToSelfThrowsException() { + assertThrows(IllegalArgumentException.class, () -> { + final DummyTreeNode abstractNode = new DummyTreeNode(); + abstractNode.setParent(abstractNode); + }); + } } \ No newline at end of file From dd775703c2a4814ad1d6bb1d16db4679806b45bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Thu, 27 Sep 2018 09:15:00 +0200 Subject: [PATCH 12/43] PMI-86: Added lauch configuration for "mvn package" --- README.md | 5 ++--- .../sql-statement-builder mvn package.launch | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 launch/sql-statement-builder mvn package.launch diff --git a/README.md b/README.md index 53541900..2bbfd1a6 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,8 @@ import com.exasol.sql.rendering.SqlStatementRenderer; SqlStatement statement = StatementFactory.getInstance() .select() - .field("name") - .from("bar") - .join("zoo").on("zoo.bar_id").eq("bar.id") + .field("firstname", "lastname") + .from("person"); String statementText = SqlStatementRenderer.render(statement); ``` diff --git a/launch/sql-statement-builder mvn package.launch b/launch/sql-statement-builder mvn package.launch new file mode 100644 index 00000000..e2007574 --- /dev/null +++ b/launch/sql-statement-builder mvn package.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + From 6b6b54e5088b97742427fa6f2aeac99132a612d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Thu, 27 Sep 2018 14:33:14 +0200 Subject: [PATCH 13/43] PMI-86: Added build configuration for Travis CI. --- .travis.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..9a89ad5f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: java + +jdk: + - openjdk8 + - oraclejdk10 + - openjdk10 + +before_script: + - version=$(grep -oP '(?<=^ )[^<]*' pom.xml) + +script: + - mvn clean install \ No newline at end of file From eca869ee3e9e7c2eeafe63244c6ef7282ae15c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Thu, 27 Sep 2018 14:42:29 +0200 Subject: [PATCH 14/43] PMI-86: Corrected indentation of "grep" in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9a89ad5f..861ccb97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ jdk: - openjdk10 before_script: - - version=$(grep -oP '(?<=^ )[^<]*' pom.xml) + - version=$(grep -oP '(?<=^ )[^<]*' pom.xml) script: - mvn clean install \ No newline at end of file From 2635239fd2a2d8963300424d1f547f333b4f6cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Thu, 27 Sep 2018 14:59:00 +0200 Subject: [PATCH 15/43] PMI-86: Added Travis CI badge. Outlined milestone plan. --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bbfd1a6..760004fa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,17 @@ # sql-statement-builder +[![Build Status](https://travis-ci.com/EXASOL/sql-statement-builder.svg?branch=develop)](https://travis-ci.com/EXASOL/sql-statement-builder) + +The Exasol SQL Statement Builder abstracts programmatic creation of SQL statements and is intended to replace ubiquitous string concatenation solutions which make the code hard to read and are prone to error and security risks. + +Goals: + +1. Foster clean and readable code +1. Allow for thorough validation of dynamic parts +1. Detect as many errors as possible at *compile time* +1. Don't repeat yourself (DRY) +1. Allow extension for different SQL dialects + ## Usage ```java @@ -29,4 +41,35 @@ The list below show all build time dependencies in alphabetical order. Note that | [Equals Verifier](https://github.com/jqno/equalsverifier) | Automatic contract checker for `equals()` and `hash()` | Apache License 2.0 | | [Hamcrest](http://hamcrest.org/) | Advanced matchers for JUnit | GNU BSD-3-Clause | | [JUnit 5](https://junit.org/junit5/) | Unit testing framework | Eclipse Public License 1.0 | -| [Mockito](http://site.mockito.org/) | Mocking framework | MIT License | \ No newline at end of file +| [Mockito](http://site.mockito.org/) | Mocking framework | MIT License | + +### Planned Milestones + +The milestones listed below are a rough outline and might be subject to change depending on which constructs are needed more. The plan will be updated accordingly. + +#### M1 + +* Basic support for Data Query Language (DQL) statement constructs (SELECT, FROM, JOIN, WHERE) +* Rendering to string +* Exasol Dialect only + +#### M2 + +* Validation for constructs from M1 + +(Later milestones will always include validation of the newly learned milestones) + +#### M3 + +* Scalar functions + +#### M4 + +* Sub-Selects including validation + +#### Later Milstones (very coarse) + +* Data Manipulation Language (DML) statements +* Data Definition Language (DDL) statements +* Support for Standard SQL +* Support for other dialects (help welcome!) \ No newline at end of file From e65b759b28cf45d52e26feaf8947ffdf2cad5de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 1 Oct 2018 10:46:26 +0200 Subject: [PATCH 16/43] PMI-86: Added basic comparison features. --- doc/design.md | 6 ++ doc/system_requirements.md | 96 ++++++++++++++++++- .../sql/dql/BooleanValueExpression.java | 3 +- .../expression/BooleanExpressionVisitor.java | 22 +++-- .../exasol/sql/expression/BooleanTerm.java | 5 + .../com/exasol/sql/expression/Comparison.java | 51 ++++++++++ .../sql/expression/ComparisonOperator.java | 43 +++++++++ .../rendering/BooleanExpressionRenderer.java | 14 +++ .../expression/TestComparisonOperator.java | 24 +++++ .../TestBooleanExpressionRenderer.java | 7 ++ 10 files changed, 259 insertions(+), 12 deletions(-) create mode 100644 doc/design.md create mode 100644 src/main/java/com/exasol/sql/expression/Comparison.java create mode 100644 src/main/java/com/exasol/sql/expression/ComparisonOperator.java create mode 100644 src/test/java/com/exasol/sql/expression/TestComparisonOperator.java diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 00000000..b02cf87e --- /dev/null +++ b/doc/design.md @@ -0,0 +1,6 @@ +# Software Architetural Design -- Exasol SQL Statement Builder + + +## Forwarded Requirements + +* dsn --> impl, utest : req~comparison-operations~1 \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md index b95ee9db..163ac9e1 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -1,7 +1,101 @@ +# System Requirement Specification -- Exasol SQL Statement Builder + +## Introduction + +The Exasol SQL Statement Builder (ESB) is a [Java](https://java.com) library that allows you to define SQL statements in a [internal Domain-specific language (DSL)](https://en.wikipedia.org/wiki/Domain-specific_language#Usage_patterns). This means it uses standard Java language features to create a DSL. + +The project uses agile software development process, so this document contains only the portion of requirement necessary for the current iteration. + +### Goals + +The goals of the ESB are: + +* Abstraction from the SQL text representation +* Compile-time error detection (where possible) +* Extensibility (for different SQL dialects, validators and renderers) +* User friendly API + +### Terms and Abbreviations + +* ESB: Exasol SQL Statement Builder +* Renderer: extension that turns the abstract representation into a different one (most notably an SQL string) +* Validator: extension that validates a statements structure and content + +## Features + +### Statement Definition +`feat~statement-definition~1` + +The ESB allows users to define SQL statements in abstract form. + +Needs: req + +### SQL String Rendering +`feat~sql-string-rendering~1` + +The ESB renders abstract SQL statements into strings. + +Rationale: + +The SQL strings are necessary input for executing queries (e.g. with JDBC). + +Needs: req + +### Compile-time Error Checking +`feat~compile-time-error-checking~1` + +ESB reports detectable errors at compile-time. + +Rationale: + +Making sure at compile time that illegal constructs do not compile make the resulting code safer and reduces debugging efforts. + +Needs: req + +## Functional Requirements + +### SQL String Rendering + +#### Configurable Case Rendering +`req~rendering.sql.configurable-case~1` + +Users can choose whether the keywords in the SQL string should be rendered in upper case and lower case. + +Rationale: + +While keyword case is mostly an esthetic point, different users still have different preferences. + +Covers: + +* [feat~sql-string-rendering~1](#sql-string-rendering) + +Needs: dsn + * Upper case / lower case * One line / pretty -SQL features: +##### Comparison Operations +`req~comparison-operations~1` + +ESB supports the following comparison operations: + + operation = left-operand operator right-operand + + left-operand = field-reference / literal + + operator = equal-operator / unequal-operator / greater-operator / less-than-operator / + greater-or-equal-operator / less-than-or-equal-operator + + right-operand = field-reference / literal + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + + +### TODO --- diff --git a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java index e95d16eb..8d5d21e9 100644 --- a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java +++ b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java @@ -17,7 +17,6 @@ public class BooleanValueExpression extends ValueExpression { public BooleanValueExpression(final BooleanExpression expression) { super(); this.expression = expression; - } @Override @@ -33,4 +32,4 @@ protected void acceptConcrete(final FragmentVisitor visitor) { public BooleanExpression getExpression() { return this.expression; } -} +} \ 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 d2b55684..279574a2 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java @@ -4,19 +4,23 @@ * Visitor interface for a {@link BooleanTerm} */ public interface BooleanExpressionVisitor { - void visit(Not not); + public void visit(Not not); - void visit(Literal literal); + public void visit(Literal literal); - void visit(And and); + public void visit(And and); - void leave(Not not); + public void leave(Not not); - void leave(Literal literal); + public void leave(Literal literal); - void leave(And and); + public void leave(And and); - void visit(Or or); + public void visit(Or or); - void leave(Or or); -} + public void leave(Or or); + + public void visit(Comparison comparison); + + public void leave(Comparison comparison); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 9fad266f..8223cab8 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -44,4 +44,9 @@ public static BooleanExpression or(final String literal, final BooleanExpression public static BooleanExpression or(final BooleanExpression... expressions) { return new Or(expressions); } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression eq(final String left, final String right) { + return new Comparison(ComparisonOperator.EQUAL, Literal.of(left), Literal.of(right)); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Comparison.java b/src/main/java/com/exasol/sql/expression/Comparison.java new file mode 100644 index 00000000..0a6381af --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Comparison.java @@ -0,0 +1,51 @@ +package com.exasol.sql.expression; + +//[impl->dsn~comparison-operations~1] +public class Comparison extends AbstractBooleanExpression { + private final ComparisonOperator operator; + private final Literal leftOperand; + private final Literal rightOperand; + + public Comparison(final ComparisonOperator equal, final Literal leftOperand, final Literal rightOperand) { + this.operator = equal; + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } + + /** + * Get the left-hand side operator of the comparison + * + * @return left operator + */ + public AbstractBooleanExpression getLeftOperand() { + return this.leftOperand; + } + + /** + * Get the right-hand side operator of the comparison + * + * @return right operator + */ + public AbstractBooleanExpression getRightOperand() { + return this.rightOperand; + } + + /** + * Get the comparison operator + * + * @return comparison operator + */ + public ComparisonOperator getOperator() { + return this.operator; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/ComparisonOperator.java b/src/main/java/com/exasol/sql/expression/ComparisonOperator.java new file mode 100644 index 00000000..b3637a4f --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/ComparisonOperator.java @@ -0,0 +1,43 @@ +package com.exasol.sql.expression; + +import java.beans.Expression; + +/** + * This enum represents the different types of {@link Comparison}s that can be + * used in {@link Expression}s. + */ +public enum ComparisonOperator { + // [impl->dsn~comparison-operations~1] + EQUAL("="), NOT_EQUAL("<>"), GREATER(">"), GREATER_OR_EQUAL(">="), LESS_THAN("<"), LESS_THAN_OR_EQUAL("<="); + + private final String operatorSymbol; + + private ComparisonOperator(final String operatorSymbol) { + this.operatorSymbol = operatorSymbol; + } + + /** + * Returns the operator symbol that represents the comparison. + * + * @return operator symbol + */ + @Override + public String toString() { + return this.operatorSymbol; + } + + /** + * Get the {@link ComparisonOperator} for the provided symbol + * + * @param operatorSymbol symbol that represents the operator + * @return operator + */ + public static ComparisonOperator ofSymbol(final String operatorSymbol) { + for (final ComparisonOperator operator : ComparisonOperator.values()) { + if (operator.operatorSymbol.equals(operatorSymbol)) { + return operator; + } + } + throw new IllegalArgumentException("Unknown comparison operator \"" + operatorSymbol + "\""); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java index c3e5b55e..9e7b55ea 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -106,4 +106,18 @@ private void endParenthesis(final BooleanExpression expression) { public String render() { return this.builder.toString(); } + + @Override + public void visit(final Comparison comparison) { + comparison.getLeftOperand().accept(this); + this.builder.append(" "); + this.builder.append(comparison.getOperator().toString()); + this.builder.append(" "); + comparison.getRightOperand().accept(this); + } + + @Override + public void leave(final Comparison comparison) { + // intentionally empty + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java new file mode 100644 index 00000000..819b28d3 --- /dev/null +++ b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java @@ -0,0 +1,24 @@ +package com.exasol.sql.expression; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class TestComparisonOperator { + @Test + void testToString() { + assertThat(ComparisonOperator.EQUAL.toString(), equalTo("=")); + } + + @Test + void testOfSymbol() { + assertThat(ComparisonOperator.ofSymbol("<>"), equalTo(ComparisonOperator.NOT_EQUAL)); + } + + @Test + void testOfUnknownSymbolThrowsException() { + assertThrows(IllegalArgumentException.class, () -> ComparisonOperator.ofSymbol("ยง")); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java index 5d58da54..5e4fd644 100644 --- a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -77,4 +77,11 @@ void testAndWhitNestedOrInLowercase() { final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); assertThat(expression, rendersWithConfigTo(config, "(not(a) or b) and (c or d)")); } + + // [utest->dsn~comparison-operations~1] + @Test + void testComparison() { + final BooleanExpression expression = eq("a", "b"); + assertThat(expression, rendersTo("a = b")); + } } \ No newline at end of file From 26b9d7d86dc3c3265d4ca8e1e0470fb2ad94c48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 1 Oct 2018 11:19:25 +0200 Subject: [PATCH 17/43] PMI-86: Added tests and implementation for other comparisons than "equals". Added factory method that takes operator symbol. --- .../exasol/sql/expression/BooleanTerm.java | 30 +++++++++++++++++++ .../sql/expression/ComparisonOperator.java | 2 +- .../TestBooleanExpressionRenderer.java | 20 +++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 8223cab8..88a53c53 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -45,8 +45,38 @@ public static BooleanExpression or(final BooleanExpression... expressions) { return new Or(expressions); } + // [impl->dsn~comparison-operations~1] + public static BooleanExpression compare(final String left, final String operatorSymbol, final String right) { + return new Comparison(ComparisonOperator.ofSymbol(operatorSymbol), Literal.of(left), Literal.of(right)); + } + // [impl->dsn~comparison-operations~1] public static BooleanExpression eq(final String left, final String right) { return new Comparison(ComparisonOperator.EQUAL, Literal.of(left), Literal.of(right)); } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression ne(final String left, final String right) { + return new Comparison(ComparisonOperator.NOT_EQUAL, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression lt(final String left, final String right) { + return new Comparison(ComparisonOperator.LESS_THAN, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression gt(final String left, final String right) { + return new Comparison(ComparisonOperator.GREATER_THAN, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression le(final String left, final String right) { + return new Comparison(ComparisonOperator.LESS_THAN_OR_EQUAL, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression ge(final String left, final String right) { + return new Comparison(ComparisonOperator.GREATER_THAN_OR_EQUAL, Literal.of(left), Literal.of(right)); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/ComparisonOperator.java b/src/main/java/com/exasol/sql/expression/ComparisonOperator.java index b3637a4f..034ab5e8 100644 --- a/src/main/java/com/exasol/sql/expression/ComparisonOperator.java +++ b/src/main/java/com/exasol/sql/expression/ComparisonOperator.java @@ -8,7 +8,7 @@ */ public enum ComparisonOperator { // [impl->dsn~comparison-operations~1] - EQUAL("="), NOT_EQUAL("<>"), GREATER(">"), GREATER_OR_EQUAL(">="), LESS_THAN("<"), LESS_THAN_OR_EQUAL("<="); + EQUAL("="), NOT_EQUAL("<>"), GREATER_THAN(">"), GREATER_THAN_OR_EQUAL(">="), LESS_THAN("<"), LESS_THAN_OR_EQUAL("<="); private final String operatorSymbol; diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java index 5e4fd644..77a889c8 100644 --- a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -4,6 +4,7 @@ import static com.exasol.hamcrest.BooleanExpressionRenderResultMatcher.rendersWithConfigTo; import static com.exasol.sql.expression.BooleanTerm.*; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import org.junit.jupiter.api.Test; @@ -80,8 +81,21 @@ void testAndWhitNestedOrInLowercase() { // [utest->dsn~comparison-operations~1] @Test - void testComparison() { - final BooleanExpression expression = eq("a", "b"); - assertThat(expression, rendersTo("a = b")); + void testComparisonFromSymbol() { + final BooleanExpression expression = compare("a", ">=", "b"); + assertThat(expression, rendersTo("a >= b")); + } + + // [utest->dsn~comparison-operations~1] + @Test + void testComparisonOperators() { + assertAll( // + () -> assertThat("equal", eq("a", "b"), rendersTo("a = b")), // + () -> assertThat("not equal", ne("a", "b"), rendersTo("a <> b")), // + () -> assertThat("not equal", lt("a", "b"), rendersTo("a < b")), // + () -> assertThat("not equal", gt("a", "b"), rendersTo("a > b")), // + () -> assertThat("not equal", le("a", "b"), rendersTo("a <= b")), // + () -> assertThat("not equal", ge("a", "b"), rendersTo("a >= b")) // + ); } } \ No newline at end of file From 4543c785bb231ee33005c223c86425809a05514a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 1 Oct 2018 13:07:19 +0200 Subject: [PATCH 18/43] PMI-86: Added where clause. --- .../java/com/exasol/sql/FragmentVisitor.java | 2 ++ .../java/com/exasol/sql/dql/FromClause.java | 13 ++++++++ .../java/com/exasol/sql/dql/WhereClause.java | 32 +++++++++++++++++++ .../sql/rendering/SqlStatementRenderer.java | 17 ++++++++-- .../com/exasol/dql/rendering/TestWhere.java | 17 ++++++++++ 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/exasol/sql/dql/WhereClause.java create mode 100644 src/test/java/com/exasol/dql/rendering/TestWhere.java diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index 33fad8b1..9205d86b 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -19,4 +19,6 @@ public interface FragmentVisitor { public void visit(BooleanValueExpression booleanValueExpression); public void visit(LimitClause limitClause); + + public void visit(WhereClause whereClause); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index 38b8254d..54385435 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -2,6 +2,7 @@ import com.exasol.sql.AbstractFragment; import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.expression.BooleanExpression; /** * This class represents the FROM clause of an SQL SELECT statement. @@ -158,6 +159,18 @@ public LimitClause limit(final int offset, final int count) { return limitClause; } + /** + * Create a new {@link WhereClause} + * + * @param expression boolean expression that defines the filter criteria + * @return new instance + */ + public WhereClause where(final BooleanExpression expression) { + final WhereClause whereClause = new WhereClause(expression); + addChild(whereClause); + return whereClause; + } + @Override protected void acceptConcrete(final FragmentVisitor visitor) { visitor.visit(this); diff --git a/src/main/java/com/exasol/sql/dql/WhereClause.java b/src/main/java/com/exasol/sql/dql/WhereClause.java new file mode 100644 index 00000000..0dd52de0 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/WhereClause.java @@ -0,0 +1,32 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.expression.BooleanExpression; + +public class WhereClause extends AbstractFragment { + private final BooleanExpression expression; + + /** + * Create a new instance of a WhereClause + * + * @param expression boolean expression that defines the filter criteria + */ + public WhereClause(final BooleanExpression expression) { + this.expression = expression; + } + + @Override + protected void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); + } + + /** + * Get the boolean expression defining the filter criteria + * + * @return boolean expression + */ + public BooleanExpression getExpression() { + return this.expression; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index 71dbf2e1..5892ca60 100644 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -5,6 +5,7 @@ import com.exasol.sql.Fragment; import com.exasol.sql.FragmentVisitor; import com.exasol.sql.dql.*; +import com.exasol.sql.expression.BooleanExpression; import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; /** @@ -103,10 +104,14 @@ public void visit(final Join join) { @Override public void visit(final BooleanValueExpression value) { - final BooleanExpressionRenderer subRenderer = new BooleanExpressionRenderer(); - value.getExpression().accept(subRenderer); appendSpace(); - append(subRenderer.render()); + appendRenderedExpression(value.getExpression()); + } + + private void appendRenderedExpression(final BooleanExpression expression) { + final BooleanExpressionRenderer expressionRenderer = new BooleanExpressionRenderer(); + expression.accept(expressionRenderer); + append(expressionRenderer.render()); } @Override @@ -123,6 +128,12 @@ private void append(final int number) { this.builder.append(number); } + @Override + public void visit(final WhereClause whereClause) { + appendKeyWord(" WHERE "); + appendRenderedExpression(whereClause.getExpression()); + } + /** * Create a renderer for the given {@link Fragment} and render it. * diff --git a/src/test/java/com/exasol/dql/rendering/TestWhere.java b/src/test/java/com/exasol/dql/rendering/TestWhere.java new file mode 100644 index 00000000..e12903bd --- /dev/null +++ b/src/test/java/com/exasol/dql/rendering/TestWhere.java @@ -0,0 +1,17 @@ +package com.exasol.dql.rendering; + +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; +import static com.exasol.sql.expression.BooleanTerm.eq; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; + +class TestWhere { + @Test + public void testWhere() { + assertThat(StatementFactory.getInstance().select().all().from("person").where(eq("firstname", "Jane")), + rendersTo("SELECT * FROM person WHERE firstname = Jane")); + } +} \ No newline at end of file From 9de5b4d000101a911930ac0b2daf5fe1a994f787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 1 Oct 2018 13:32:24 +0200 Subject: [PATCH 19/43] PMI-86: Gave rendering tests a clearer name. --- .../dql/rendering/{TestJoin.java => TestJoinRendering.java} | 2 +- .../dql/rendering/{TestLimit.java => TestLimitRendering.java} | 2 +- .../dql/rendering/{TestSelect.java => TestSelectRendering.java} | 2 +- .../dql/rendering/{TestWhere.java => TestWhereRendering.java} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/test/java/com/exasol/dql/rendering/{TestJoin.java => TestJoinRendering.java} (99%) rename src/test/java/com/exasol/dql/rendering/{TestLimit.java => TestLimitRendering.java} (95%) rename src/test/java/com/exasol/dql/rendering/{TestSelect.java => TestSelectRendering.java} (98%) rename src/test/java/com/exasol/dql/rendering/{TestWhere.java => TestWhereRendering.java} (95%) diff --git a/src/test/java/com/exasol/dql/rendering/TestJoin.java b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java similarity index 99% rename from src/test/java/com/exasol/dql/rendering/TestJoin.java rename to src/test/java/com/exasol/dql/rendering/TestJoinRendering.java index fed3eb20..4f5872bc 100644 --- a/src/test/java/com/exasol/dql/rendering/TestJoin.java +++ b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java @@ -7,7 +7,7 @@ import com.exasol.sql.StatementFactory; -class TestJoin { +class TestJoinRendering { @Test public void testJoin() { assertThat( diff --git a/src/test/java/com/exasol/dql/rendering/TestLimit.java b/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java similarity index 95% rename from src/test/java/com/exasol/dql/rendering/TestLimit.java rename to src/test/java/com/exasol/dql/rendering/TestLimitRendering.java index 02d6d7dc..a8e7b6a8 100644 --- a/src/test/java/com/exasol/dql/rendering/TestLimit.java +++ b/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java @@ -7,7 +7,7 @@ import com.exasol.sql.StatementFactory; -class TestLimit { +class TestLimitRendering { @Test void testLimitCountAfterFrom() { assertThat(StatementFactory.getInstance().select().all().from("t").limit(1), diff --git a/src/test/java/com/exasol/dql/rendering/TestSelect.java b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java similarity index 98% rename from src/test/java/com/exasol/dql/rendering/TestSelect.java rename to src/test/java/com/exasol/dql/rendering/TestSelectRendering.java index 472d980b..44eebde9 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSelect.java +++ b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java @@ -11,7 +11,7 @@ import com.exasol.sql.expression.BooleanTerm; import com.exasol.sql.rendering.StringRendererConfig; -class TestSelect { +class TestSelectRendering { @Test void testGetParentReturnsNull() { assertThat(StatementFactory.getInstance().select().getParent(), nullValue()); diff --git a/src/test/java/com/exasol/dql/rendering/TestWhere.java b/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java similarity index 95% rename from src/test/java/com/exasol/dql/rendering/TestWhere.java rename to src/test/java/com/exasol/dql/rendering/TestWhereRendering.java index e12903bd..10e12999 100644 --- a/src/test/java/com/exasol/dql/rendering/TestWhere.java +++ b/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java @@ -8,7 +8,7 @@ import com.exasol.sql.StatementFactory; -class TestWhere { +class TestWhereRendering { @Test public void testWhere() { assertThat(StatementFactory.getInstance().select().all().from("person").where(eq("firstname", "Jane")), From 86c05066ad089391a2d60494ce9d66ab515ba6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 2 Oct 2018 13:20:03 +0200 Subject: [PATCH 20/43] PMI-86: Added capability to access FROM after it was created. --- README.md | 5 +-- doc/design.md | 28 +++++++++++- doc/system_requirements.md | 45 ++++++++++++++++++- .../java/com/exasol/sql/dql/FromClause.java | 4 +- src/main/java/com/exasol/sql/dql/Select.java | 44 +++++++++--------- src/test/java/com/exasol/dql/TestSelect.java | 20 +++++++++ .../dql/rendering/TestJoinRendering.java | 16 +++---- .../dql/rendering/TestLimitRendering.java | 4 +- .../dql/rendering/TestSelectRendering.java | 10 ++--- .../rendering/TestSqlStatementRenderer.java | 2 +- .../dql/rendering/TestWhereRendering.java | 2 +- 11 files changed, 134 insertions(+), 46 deletions(-) create mode 100644 src/test/java/com/exasol/dql/TestSelect.java diff --git a/README.md b/README.md index 760004fa..3fa252e1 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,8 @@ import com.exasol.sql.SqlStatement; import com.exasol.sql.rendering.SqlStatementRenderer; SqlStatement statement = StatementFactory.getInstance() - .select() - .field("firstname", "lastname") - .from("person"); + .select().field("firstname", "lastname") + .from().table("person"); String statementText = SqlStatementRenderer.render(statement); ``` diff --git a/doc/design.md b/doc/design.md index b02cf87e..fce9b13b 100644 --- a/doc/design.md +++ b/doc/design.md @@ -1,5 +1,31 @@ -# Software Architetural Design -- Exasol SQL Statement Builder +# Software Architectural Design -- Exasol SQL Statement Builder +## Building Block View + +### Select Statement +`dsn~dql-statement~1` + +The Data Query Language (DQL) building block is responsible for managing `SELECT` statements. + +## Runtime View + +### Building Select Statements + +##### Accessing the Clauses That Make Up a SELECT Statement +`dsn~select-statement.accessing-clauses~1` + +The DQL statement component allows getting the following clauses, provided that they already exist: + +* `FROM` clause +* `WHERE` clause + +Covers: + +* `req~statement-structure.step-wise~1` + +Needs: impl, utest, itest + +Tags: Select Statement Builder ## Forwarded Requirements diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 163ac9e1..4843c5f8 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -21,6 +21,30 @@ The goals of the ESB are: * Renderer: extension that turns the abstract representation into a different one (most notably an SQL string) * Validator: extension that validates a statements structure and content +### Notation + +#### Augmented Backusโ€“Naur Form (ABNF) + +This document uses Augmented Backusโ€“Naur Form (ABNF) for syntax definitions. + +#### ABNF Terminals + +This subsection list the ABNF terminals used in this document. Terminals are ABNF rules that cannot be split further down, like string literals for example. + +##### Operator Terminals + + EQUAL-OPERATOR = "=" + + NOT-EQUAL-OPERATOR = "<>" + + LESS-THAN-OPERATOR = "<" + + LESS-THAN-OR-EQUAL-OPERATOR = "<=" + + GREATER-THAN-OPERATOR = ">" + + GREATER-THAN-OR-EQUAL-OPERATOR = ">=" + ## Features ### Statement Definition @@ -54,6 +78,23 @@ Needs: req ## Functional Requirements +### Statement Structure + +##### Building the Statement Structure Step-wise +`req~statement-structure.step-wise~1` + +ESB lets users build the statement structure step-by-step. + +Rationale: + +This is necessary since complex statements are usually build as a result of multi-layered decision trees and parts of the statements are constructed in different places. + +Covers: + +* [feat~statment-definition~1](#statement-definition) + +Needs: dsn + ### SQL String Rendering #### Configurable Case Rendering @@ -74,7 +115,7 @@ Needs: dsn * Upper case / lower case * One line / pretty -##### Comparison Operations +#### Comparison Operations `req~comparison-operations~1` ESB supports the following comparison operations: @@ -83,7 +124,7 @@ ESB supports the following comparison operations: left-operand = field-reference / literal - operator = equal-operator / unequal-operator / greater-operator / less-than-operator / + operator = equal-operator / not-equal-operator / greater-operator / less-than-operator / greater-or-equal-operator / less-than-or-equal-operator right-operand = field-reference / literal diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index 54385435..62b41545 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -21,7 +21,7 @@ public FromClause() { * @param name table name * @return new instance */ - public FromClause from(final String name) { + public FromClause table(final String name) { addChild(new Table(name)); return this; } @@ -33,7 +33,7 @@ public FromClause from(final String name) { * @param as table alias * @return new instance */ - public FromClause fromTableAs(final String name, final String as) { + public FromClause tableAs(final String name, final String as) { addChild(new Table(name, as)); return this; } diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 2f384302..c30980e0 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -7,6 +7,9 @@ * This class implements an SQL {@link Select} statement */ public class Select extends AbstractFragment implements SqlStatement { + private FromClause from; + private WhereClause where; + /** * Create a new instance of a {@link Select} */ @@ -48,34 +51,33 @@ public Select value(final BooleanExpression expression) { return this; } - @Override - public void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } - /** - * Add a {@link FromClause} to the statement with a table identified by its name + * Get the {@link FromClause} of this select statement * - * @param name table reference name - * @return the FROM clause + * @return from clause */ - public FromClause from(final String name) { - final FromClause from = new FromClause().from(name); - addChild(from); - return from; + public synchronized FromClause from() { + if (this.from == null) { + this.from = new FromClause(); + addChild(this.from); + } + return this.from; } /** - * Add a {@link FromClause} to the statement with an aliased table identified by - * its name + * Get the {@link WhereClause} of this select statement * - * @param name table reference name - * @param as table correlation name - * @return the FROM clause + * @return from clause */ - public FromClause fromTableAs(final String name, final String as) { - final FromClause from = new FromClause().fromTableAs(name, as); - addChild(from); - return from; + public synchronized WhereClause where() { + if (this.where == null) { + throw new IllegalStateException("Tried to access a WHERE clause before it was constructed."); + } + return this.where; + } + + @Override + public void acceptConcrete(final FragmentVisitor visitor) { + visitor.visit(this); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/TestSelect.java new file mode 100644 index 00000000..991f0d36 --- /dev/null +++ b/src/test/java/com/exasol/dql/TestSelect.java @@ -0,0 +1,20 @@ +package com.exasol.dql; + +import static org.hamcrest.CoreMatchers.sameInstance; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; +import com.exasol.sql.dql.FromClause; +import com.exasol.sql.dql.Select; + +class TestSelect { + // [impl->req~statement-structure.step-wise~1] + @Test + void testGetFrom() { + final Select select = StatementFactory.getInstance().select().all(); + final FromClause from = select.from().table("persons"); + assertThat(select.from(), sameInstance(from)); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java index 4f5872bc..c0e529e2 100644 --- a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java @@ -11,7 +11,7 @@ class TestJoinRendering { @Test public void testJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").join("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").join("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @@ -19,7 +19,7 @@ public void testJoin() { @Test public void testInnerJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").innerJoin("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").innerJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo("SELECT * FROM left_table INNER JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @@ -27,7 +27,7 @@ public void testInnerJoin() { @Test public void testLeftJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").leftJoin("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").leftJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo("SELECT * FROM left_table LEFT JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @@ -35,7 +35,7 @@ public void testLeftJoin() { @Test public void testRightJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").rightJoin("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").rightJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo("SELECT * FROM left_table RIGHT JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @@ -43,7 +43,7 @@ public void testRightJoin() { @Test public void testFullJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").fullJoin("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").fullJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo("SELECT * FROM left_table FULL JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @@ -51,7 +51,7 @@ public void testFullJoin() { @Test public void testLeftOuterJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").leftOuterJoin("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").leftOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo( "SELECT * FROM left_table LEFT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); @@ -60,7 +60,7 @@ public void testLeftOuterJoin() { @Test public void testRightOuterJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").rightOuterJoin("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").rightOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo( "SELECT * FROM left_table RIGHT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); @@ -69,7 +69,7 @@ public void testRightOuterJoin() { @Test public void testFullOuterJoin() { assertThat( - StatementFactory.getInstance().select().all().from("left_table").fullOuterJoin("right_table", + StatementFactory.getInstance().select().all().from().table("left_table").fullOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo( "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); diff --git a/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java b/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java index a8e7b6a8..19b148f3 100644 --- a/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java @@ -10,13 +10,13 @@ class TestLimitRendering { @Test void testLimitCountAfterFrom() { - assertThat(StatementFactory.getInstance().select().all().from("t").limit(1), + assertThat(StatementFactory.getInstance().select().all().from().table("t").limit(1), rendersTo("SELECT * FROM t LIMIT 1")); } @Test void testLimitOffsetCountAfterFrom() { - assertThat(StatementFactory.getInstance().select().all().from("t").limit(2, 3), + assertThat(StatementFactory.getInstance().select().all().from().table("t").limit(2, 3), rendersTo("SELECT * FROM t LIMIT 2, 3")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java index 44eebde9..a13c564e 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java @@ -45,25 +45,25 @@ void testSelectChainOfFieldNames() { @Test void testSelectFromTable() { - assertThat(StatementFactory.getInstance().select().all().from("table"), rendersTo("SELECT * FROM table")); + assertThat(StatementFactory.getInstance().select().all().from().table("persons"), + rendersTo("SELECT * FROM persons")); } @Test void testSelectFromMultipleTable() { - assertThat(StatementFactory.getInstance().select().all().from("table1").from("table2"), + assertThat(StatementFactory.getInstance().select().all().from().table("table1").table("table2"), rendersTo("SELECT * FROM table1, table2")); } @Test void testSelectFromTableAs() { - assertThat(StatementFactory.getInstance().select().all().fromTableAs("table", "t"), + assertThat(StatementFactory.getInstance().select().all().from().tableAs("table", "t"), rendersTo("SELECT * FROM table AS t")); } @Test void testSelectFromMultipleTableAs() { - assertThat( - StatementFactory.getInstance().select().all().fromTableAs("table1", "t1").fromTableAs("table2", "t2"), + assertThat(StatementFactory.getInstance().select().all().from().tableAs("table1", "t1").tableAs("table2", "t2"), rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); } diff --git a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java b/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java index 0771ae54..5eaf9613 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java +++ b/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java @@ -11,7 +11,7 @@ class TestSqlStatementRenderer { @Test void testCreateAndRender() { - assertThat(SqlStatementRenderer.render(StatementFactory.getInstance().select().all().from("foo")), + assertThat(SqlStatementRenderer.render(StatementFactory.getInstance().select().all().from().table("foo")), equalTo("SELECT * FROM foo")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java b/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java index 10e12999..9b61aea7 100644 --- a/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java @@ -11,7 +11,7 @@ class TestWhereRendering { @Test public void testWhere() { - assertThat(StatementFactory.getInstance().select().all().from("person").where(eq("firstname", "Jane")), + assertThat(StatementFactory.getInstance().select().all().from().table("person").where(eq("firstname", "Jane")), rendersTo("SELECT * FROM person WHERE firstname = Jane")); } } \ No newline at end of file From 2d2233e4d633a8de8bf3c6e744b28df8a52f7f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Fri, 5 Oct 2018 12:15:59 +0200 Subject: [PATCH 21/43] PMI-86: Replaced underlying generic tree from SQL fragments by dedicated object structure. --- doc/design.md | 3 +- doc/system_requirements.md | 10 ++ model/diagrams/class/cl_fragments.plantuml | 2 +- .../cl_select_fragment_hierarchy.plantuml | 11 +++ model/diagrams/exasol.skin | 4 +- .../java/com/exasol/sql/AbstractFragment.java | 30 +++--- src/main/java/com/exasol/sql/Fragment.java | 21 ++++- .../sql/dql/BooleanValueExpression.java | 16 ++-- src/main/java/com/exasol/sql/dql/Field.java | 31 ++++--- .../java/com/exasol/sql/dql/FromClause.java | 77 ++++++++-------- src/main/java/com/exasol/sql/dql/Join.java | 9 +- .../java/com/exasol/sql/dql/LimitClause.java | 29 +++--- src/main/java/com/exasol/sql/dql/Select.java | 91 +++++++++++++++---- src/main/java/com/exasol/sql/dql/Table.java | 17 ++-- .../com/exasol/sql/dql/ValueExpression.java | 15 ++- .../java/com/exasol/sql/dql/WhereClause.java | 27 +++--- .../exasol/sql/expression/BooleanTerm.java | 28 ++++++ .../sql/rendering/SqlStatementRenderer.java | 29 ++++-- src/test/java/com/exasol/dql/TestSelect.java | 31 ++++++- .../dql/rendering/TestJoinRendering.java | 86 +++++++++--------- .../dql/rendering/TestLimitRendering.java | 16 +++- .../dql/rendering/TestSelectRendering.java | 46 +++++----- .../rendering/TestSqlStatementRenderer.java | 13 ++- .../dql/rendering/TestWhereRendering.java | 14 ++- .../SqlFragmentRenderResultMatcher.java | 5 +- .../sql/expression/TestBooleanTerm.java | 78 ++++++++++++++++ .../TestBooleanExpressionRenderer.java | 11 +++ 27 files changed, 521 insertions(+), 229 deletions(-) create mode 100644 model/diagrams/class/cl_select_fragment_hierarchy.plantuml create mode 100644 src/test/java/com/exasol/sql/expression/TestBooleanTerm.java diff --git a/doc/design.md b/doc/design.md index fce9b13b..05e8af3d 100644 --- a/doc/design.md +++ b/doc/design.md @@ -29,4 +29,5 @@ Tags: Select Statement Builder ## Forwarded Requirements -* dsn --> impl, utest : req~comparison-operations~1 \ No newline at end of file +* dsn --> impl, utest : req~comparison-operations~1 +* dsn --> impl, utest : req~boolean-operators~1 \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 4843c5f8..e06960b2 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -135,6 +135,16 @@ Covers: Needs: dsn +##### Boolean Operators +`req~boolean-operators~1` + +ESB supports the following boolean operators: `AND`, `OR` and `NOT` + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn ### TODO diff --git a/model/diagrams/class/cl_fragments.plantuml b/model/diagrams/class/cl_fragments.plantuml index 0dbc4c44..1f6fef13 100644 --- a/model/diagrams/class/cl_fragments.plantuml +++ b/model/diagrams/class/cl_fragments.plantuml @@ -1,5 +1,5 @@ @startuml -!include ../exasol.skin +'!include ../exasol.skin together { interface Fragment <> diff --git a/model/diagrams/class/cl_select_fragment_hierarchy.plantuml b/model/diagrams/class/cl_select_fragment_hierarchy.plantuml new file mode 100644 index 00000000..6b924d3a --- /dev/null +++ b/model/diagrams/class/cl_select_fragment_hierarchy.plantuml @@ -0,0 +1,11 @@ +@startuml +'!include ../exasol.skin + +Select *-- "*" Field +Select *-- "0..1" From +Select *-- "0..1" LimitClause +Select *-- "0..1" WhereClause +From *-- "*" Table +WhereClause *-- BooleanExpression +BooleanExpression *-- "0..1" BooleanExpression +@enduml \ No newline at end of file diff --git a/model/diagrams/exasol.skin b/model/diagrams/exasol.skin index aaf51dc4..f3d4df42 100644 --- a/model/diagrams/exasol.skin +++ b/model/diagrams/exasol.skin @@ -1,8 +1,8 @@ hide empty methods hide empty attributes skinparam style strictuml -skinparam classAttributeIconSize 0 -!pragma horizontalLineBetweenDifferentPackageAllowed +'skinparam classAttributeIconSize 0 +'!pragma horizontalLineBetweenDifferentPackageAllowed skinparam Arrow { Color 093e52 diff --git a/src/main/java/com/exasol/sql/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java index 64c47657..cad0fba9 100644 --- a/src/main/java/com/exasol/sql/AbstractFragment.java +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -1,26 +1,22 @@ package com.exasol.sql; -import com.exasol.util.AbstractTreeNode; -import com.exasol.util.TreeNode; - /** - * This class provides an abstract base for SQL statement fragments. It also - * keeps track of the relationships to other fragments. - * - * @param the type of the concrete class implementing the missing parts. + * Common base class for SQL statement fragments */ -public abstract class AbstractFragment extends AbstractTreeNode implements Fragment { - protected AbstractFragment() { - super(); +public abstract class AbstractFragment implements Fragment { + protected final SqlStatement rootStatement; + + /** + * Create an instance of an SQL fragment + * + * @param rootStatement root SQL statement this fragment belongs to. + */ + public AbstractFragment(final SqlStatement rootStatement) { + this.rootStatement = rootStatement; } @Override - public void accept(final FragmentVisitor visitor) { - acceptConcrete(visitor); - for (final TreeNode child : this.getChildren()) { - ((Fragment) child).accept(visitor); - } + public Fragment getRoot() { + return this.rootStatement; } - - protected abstract void acceptConcrete(final FragmentVisitor visitor); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/Fragment.java b/src/main/java/com/exasol/sql/Fragment.java index 2269bf73..e68ec8a8 100644 --- a/src/main/java/com/exasol/sql/Fragment.java +++ b/src/main/java/com/exasol/sql/Fragment.java @@ -1,7 +1,22 @@ package com.exasol.sql; -import com.exasol.util.TreeNode; - -public interface Fragment extends TreeNode { +/** + * This is the common interface for all fragments of SQL statements. Fragments + * can be clauses like the WHERE clause of an SELECT statement but also lower + * level concepts like boolean expressions. + */ +public interface Fragment { + /** + * Accept a visitor (e.g. a renderer or validator) + * + * @param visitor visitor to accept + */ public void accept(FragmentVisitor visitor); + + /** + * Get the root statement of this SQL fragment + * + * @return the root fragement + */ + public Fragment getRoot(); } diff --git a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java index 8d5d21e9..c224e95f 100644 --- a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java +++ b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java @@ -1,6 +1,7 @@ package com.exasol.sql.dql; import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.SqlStatement; import com.exasol.sql.expression.BooleanExpression; /** @@ -12,18 +13,14 @@ public class BooleanValueExpression extends ValueExpression { /** * Create a new instance of a {@link BooleanValueExpression} * + * @param root SQL statement this expression belongs to * @param expression nested boolean expression */ - public BooleanValueExpression(final BooleanExpression expression) { - super(); + public BooleanValueExpression(final SqlStatement root, final BooleanExpression expression) { + super(root); this.expression = expression; } - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } - /** * Get the boolean expression nested in this value expression * @@ -32,4 +29,9 @@ protected void acceptConcrete(final FragmentVisitor visitor) { public BooleanExpression getExpression() { return this.expression; } + + @Override + public void accept(final FragmentVisitor visitor) { + visitor.visit(this); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Field.java b/src/main/java/com/exasol/sql/dql/Field.java index 89770a2a..2d0f49f8 100644 --- a/src/main/java/com/exasol/sql/dql/Field.java +++ b/src/main/java/com/exasol/sql/dql/Field.java @@ -1,26 +1,35 @@ package com.exasol.sql.dql; -import com.exasol.sql.AbstractFragment; -import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.*; -public class Field extends AbstractFragment implements FieldDefinition { +/** + * This class represents a table field in an SQL statement. + */ +public class Field extends AbstractFragment { private final String name; - protected Field(final String name) { - super(); + /** + * Create a new instance of a {@link Field} + * + * @param root root SQL statement + * @param name field name + */ + protected Field(final SqlStatement root, final String name) { + super(root); this.name = name; } + /** + * Get the field name + * + * @return field name + */ public String getName() { return this.name; } - public static Field all() { - return new Field("*"); - } - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { + public void accept(final FragmentVisitor visitor) { visitor.visit(this); } -} +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index 62b41545..9d16fe9c 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -1,18 +1,25 @@ package com.exasol.sql.dql; -import com.exasol.sql.AbstractFragment; -import com.exasol.sql.FragmentVisitor; +import java.util.ArrayList; +import java.util.List; + +import com.exasol.sql.*; import com.exasol.sql.expression.BooleanExpression; /** * This class represents the FROM clause of an SQL SELECT statement. */ public class FromClause extends AbstractFragment { + private final List tables = new ArrayList<>(); + private final List joins = new ArrayList<>(); + /** * Create a new instance of a {@link FromClause} + * + * @param root root SQL statement this FROM clause belongs to */ - public FromClause() { - super(); + public FromClause(final SqlStatement root) { + super(root); } /** @@ -22,7 +29,7 @@ public FromClause() { * @return new instance */ public FromClause table(final String name) { - addChild(new Table(name)); + this.tables.add(new Table(this.rootStatement, name)); return this; } @@ -34,7 +41,7 @@ public FromClause table(final String name) { * @return new instance */ public FromClause tableAs(final String name, final String as) { - addChild(new Table(name, as)); + this.tables.add(new Table(this.rootStatement, name, as)); return this; } @@ -46,7 +53,7 @@ public FromClause tableAs(final String name, final String as) { * @return parent FROM clause */ public FromClause join(final String name, final String specification) { - addChild(new Join(JoinType.DEFAULT, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.DEFAULT, name, specification)); return this; } @@ -58,7 +65,7 @@ public FromClause join(final String name, final String specification) { * @return parent FROM clause */ public FromClause innerJoin(final String name, final String specification) { - addChild(new Join(JoinType.INNER, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.INNER, name, specification)); return this; } @@ -70,7 +77,7 @@ public FromClause innerJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause leftJoin(final String name, final String specification) { - addChild(new Join(JoinType.LEFT, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.LEFT, name, specification)); return this; } @@ -82,7 +89,7 @@ public FromClause leftJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause rightJoin(final String name, final String specification) { - addChild(new Join(JoinType.RIGHT, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.RIGHT, name, specification)); return this; } @@ -94,7 +101,7 @@ public FromClause rightJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause fullJoin(final String name, final String specification) { - addChild(new Join(JoinType.FULL, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.FULL, name, specification)); return this; } @@ -106,7 +113,7 @@ public FromClause fullJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause leftOuterJoin(final String name, final String specification) { - addChild(new Join(JoinType.LEFT_OUTER, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.LEFT_OUTER, name, specification)); return this; } @@ -118,7 +125,7 @@ public FromClause leftOuterJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause rightOuterJoin(final String name, final String specification) { - addChild(new Join(JoinType.RIGHT_OUTER, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.RIGHT_OUTER, name, specification)); return this; } @@ -130,49 +137,39 @@ public FromClause rightOuterJoin(final String name, final String specification) * @return parent FROM clause */ public FromClause fullOuterJoin(final String name, final String specification) { - addChild(new Join(JoinType.FULL_OUTER, name, specification)); + this.joins.add(new Join(this.rootStatement, JoinType.FULL_OUTER, name, specification)); return this; } /** - * Create a new full outer {@link LimitClause} - * - * @param count maximum number of rows to be included in query result - * @return new instance + * @see com.exasol.sql.dql.Select#limit(int) */ - public LimitClause limit(final int count) { - final LimitClause limitClause = new LimitClause(count); - addChild(limitClause); - return limitClause; + public Select limit(final int count) { + return ((Select) this.rootStatement).limit(count); } /** - * Create a new full outer {@link LimitClause} - * - * @param offset index of the first row in the query result - * @param count maximum number of rows to be included in query result - * @return new instance + * @see com.exasol.sql.dql.Select#limit(int,int) */ - public LimitClause limit(final int offset, final int count) { - final LimitClause limitClause = new LimitClause(offset, count); - addChild(limitClause); - return limitClause; + public Select limit(final int offset, final int count) { + return ((Select) this.rootStatement).limit(offset, count); } /** - * Create a new {@link WhereClause} - * - * @param expression boolean expression that defines the filter criteria - * @return new instance + * @see com.exasol.sql.dql.Select#where(BooleanExpression) */ - public WhereClause where(final BooleanExpression expression) { - final WhereClause whereClause = new WhereClause(expression); - addChild(whereClause); - return whereClause; + public Select where(final BooleanExpression expression) { + return ((Select) this.rootStatement).where(expression); } @Override - protected void acceptConcrete(final FragmentVisitor visitor) { + public void accept(final FragmentVisitor visitor) { visitor.visit(this); + for (final Table table : this.tables) { + table.accept(visitor); + } + for (final Join join : this.joins) { + join.accept(visitor); + } } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Join.java b/src/main/java/com/exasol/sql/dql/Join.java index 7eaadf2f..b0d60510 100644 --- a/src/main/java/com/exasol/sql/dql/Join.java +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -13,12 +13,13 @@ public class Join extends AbstractFragment implements Fragment { /** * Create a new {@link Join} instance * + * @param root SQL statement this JOIN belongs to * @param type type of join (e.g. INNER, LEFT or RIGHT OUTER) * @param name name of the table to be joined * @param specification join specification (e.g. ON or USING) */ - public Join(final JoinType type, final String name, final String specification) { - super(); + public Join(final SqlStatement root, final JoinType type, final String name, final String specification) { + super(root); this.type = type; this.name = name; this.specification = specification; @@ -52,7 +53,7 @@ public String getSpecification() { } @Override - protected void acceptConcrete(final FragmentVisitor visitor) { + public void accept(final FragmentVisitor visitor) { visitor.visit(this); } -} +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java index 741d15b9..e60a1cae 100644 --- a/src/main/java/com/exasol/sql/dql/LimitClause.java +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -1,11 +1,10 @@ package com.exasol.sql.dql; -import com.exasol.sql.AbstractFragment; -import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.*; /** - * This class represents the limit clause of an SQL statement. It lets you - * choose offset and / or count of rows to be handed back in the result. + * This class represents the limit clause of an SQL statement. It lets you choose offset and / or count of rows to be + * handed back in the result. */ public class LimitClause extends AbstractFragment { private final int count; @@ -14,32 +13,31 @@ public class LimitClause extends AbstractFragment { /** * Create a new instance of a {@link LimitClause} * + * @param root SQL statement this LIMIT clause belongs to + * * @param offset index of the first row to be included in the query result * * @param count maximum number of rows to be included in the query result */ - public LimitClause(final int count) { - this(0, count); + public LimitClause(final SqlStatement root, final int count) { + this(root, 0, count); } /** * Create a new instance of a {@link LimitClause} * + * @param root SQL statement this LIMIT clause belongs to + * * @param offset index of the first row to be included in the query result * * @param count maximum number of rows to be included in the query result */ - public LimitClause(final int offset, final int count) { - super(); + public LimitClause(final SqlStatement root, final int offset, final int count) { + super(root); this.offset = offset; this.count = count; } - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } - /** * Get the offset row for the limit * @@ -66,4 +64,9 @@ public int getCount() { public boolean hasOffset() { return this.offset > 0; } + + @Override + public void accept(final FragmentVisitor visitor) { + visitor.visit(this); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index c30980e0..9156b8bd 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -1,5 +1,8 @@ package com.exasol.sql.dql; +import java.util.ArrayList; +import java.util.List; + import com.exasol.sql.*; import com.exasol.sql.expression.BooleanExpression; @@ -7,14 +10,16 @@ * This class implements an SQL {@link Select} statement */ public class Select extends AbstractFragment implements SqlStatement { - private FromClause from; - private WhereClause where; + private final List fields = new ArrayList<>(); + private FromClause fromClause = null; + private WhereClause whereClause = null; + private LimitClause limitClause = null; /** * Create a new instance of a {@link Select} */ public Select() { - super(); + super(null); } /** @@ -23,7 +28,7 @@ public Select() { * @return this instance for fluent programming */ public Select all() { - addChild(Field.all()); + this.fields.add(new Field(this, "*")); return this; } @@ -35,33 +40,67 @@ public Select all() { */ public Select field(final String... names) { for (final String name : names) { - addChild(new Field(name)); + this.fields.add(new Field(this, name)); } return this; } /** - * Add a boolean value expression + * Get the {@link FromClause} of this select statement * - * @param expression boolean value expression - * @return this instance for fluent programming + * @return from clause + */ + public synchronized FromClause from() { + if (this.fromClause == null) { + this.fromClause = new FromClause(this); + } + return this.fromClause; + } + + /** + * Create a new full outer {@link LimitClause} + * + * @param count maximum number of rows to be included in query result + * @return new instance + * @throws IllegalStateException if a limit clause already exists */ - public Select value(final BooleanExpression expression) { - addChild(new BooleanValueExpression(expression)); + public synchronized Select limit(final int count) { + if (this.limitClause != null) { + throw new IllegalStateException( + "Tried to create a LIMIT clause in a SELECT statement that already had one."); + } + this.limitClause = new LimitClause(this, count); return this; } /** - * Get the {@link FromClause} of this select statement + * Create a new full outer {@link LimitClause} * - * @return from clause + * @param offset index of the first row in the query result + * @param count maximum number of rows to be included in query result + * @return this as; /** - * Create a new {@link Table} + * Create a new {@link Table} with a name and an alias * + * @param root SQL statement this table belongs to * @param name table name */ - public Table(final String name) { - super(); + public Table(final SqlStatement root, final String name) { + super(root); this.name = name; this.as = Optional.empty(); } @@ -26,11 +26,12 @@ public Table(final String name) { /** * Create a new {@link Table} with a name and an alias * + * @param root SQL statement this table belongs to * @param name table name * @param as table alias */ - public Table(final String name, final String as) { - super(); + public Table(final SqlStatement root, final String name, final String as) { + super(root); this.name = name; this.as = Optional.of(as); } @@ -54,7 +55,7 @@ public Optional getAs() { } @Override - protected void acceptConcrete(final FragmentVisitor visitor) { + public void accept(final FragmentVisitor visitor) { visitor.visit(this); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/ValueExpression.java b/src/main/java/com/exasol/sql/dql/ValueExpression.java index 3ec57fd5..2a6d59db 100644 --- a/src/main/java/com/exasol/sql/dql/ValueExpression.java +++ b/src/main/java/com/exasol/sql/dql/ValueExpression.java @@ -1,15 +1,24 @@ package com.exasol.sql.dql; -import com.exasol.sql.AbstractFragment; +import com.exasol.sql.*; /** * Abstract base class for all types of value expressions */ -public abstract class ValueExpression extends AbstractFragment { +public abstract class ValueExpression extends AbstractFragment implements Fragment { /** * Create a new instance of a {@link ValueExpression} */ public ValueExpression() { - super(); + super(null); + } + + /** + * Create a new instance of a {@link ValueExpression} + * + * @param root root SQL statement this expression belongs to + */ + public ValueExpression(final SqlStatement root) { + super(root); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/WhereClause.java b/src/main/java/com/exasol/sql/dql/WhereClause.java index 0dd52de0..18f7c9bc 100644 --- a/src/main/java/com/exasol/sql/dql/WhereClause.java +++ b/src/main/java/com/exasol/sql/dql/WhereClause.java @@ -1,26 +1,26 @@ package com.exasol.sql.dql; -import com.exasol.sql.AbstractFragment; -import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.*; import com.exasol.sql.expression.BooleanExpression; +/** + * This class represents the where clause of an SQL statement. It contains the filter criteria in form of a + * {@link BooleanExpression}. + */ public class WhereClause extends AbstractFragment { private final BooleanExpression expression; /** - * Create a new instance of a WhereClause - * - * @param expression boolean expression that defines the filter criteria + * Create a new instance of a {@link WhereClause} + * + * @param root SQL statement this WHERE clause belongs to + * @param expression */ - public WhereClause(final BooleanExpression expression) { + public WhereClause(final SqlStatement root, final BooleanExpression expression) { + super(root); this.expression = expression; } - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } - /** * Get the boolean expression defining the filter criteria * @@ -29,4 +29,9 @@ protected void acceptConcrete(final FragmentVisitor visitor) { public BooleanExpression getExpression() { return this.expression; } + + @Override + public void accept(final FragmentVisitor visitor) { + visitor.visit(this); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 88a53c53..431a5b4a 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -79,4 +79,32 @@ public static BooleanExpression le(final String left, final String right) { public static BooleanExpression ge(final String left, final String right) { return new Comparison(ComparisonOperator.GREATER_THAN_OR_EQUAL, Literal.of(left), Literal.of(right)); } + + /** + * Create a logical operation from an operator name and a list of operands + * + * @param operator name of the operator + * @param expressions operands + * @return instance of either {@link And}, {@link Or} or {@link Not} + * @throws IllegalArgumentException + */ + public static BooleanExpression operation(final String operator, final BooleanExpression... expressions) + throws IllegalArgumentException { + switch (operator.toLowerCase()) { + case "and": + return new And(expressions); + case "or": + return new Or(expressions); + case "not": + if (expressions.length == 1) { + return new Not(expressions[0]); + } else { + throw new IllegalArgumentException( + "Logical \"not\" must have exactly one operand. Got " + expressions.length + "."); + } + default: + throw new IllegalArgumentException( + "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/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index 5892ca60..5dcc6357 100644 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -15,6 +15,7 @@ public class SqlStatementRenderer implements FragmentVisitor { private final StringBuilder builder = new StringBuilder(); private final StringRendererConfig config; + private Fragment lastVisited; /** * Create a new {@link SqlStatementRenderer} using the default @@ -45,6 +46,7 @@ public String render() { @Override public void visit(final Select select) { appendKeyWord("SELECT"); + setLastVisited(select); } private void appendKeyWord(final String keyword) { @@ -60,6 +62,11 @@ public void visit(final Field field) { appendCommaWhenNeeded(field); appendSpace(); append(field.getName()); + setLastVisited(field); + } + + private void setLastVisited(final Fragment fragment) { + this.lastVisited = fragment; } private void appendSpace() { @@ -67,7 +74,7 @@ private void appendSpace() { } private void appendCommaWhenNeeded(final Fragment fragment) { - if (!fragment.isFirstSibling()) { + if (this.lastVisited.getClass().equals(fragment.getClass())) { append(","); } } @@ -75,6 +82,7 @@ private void appendCommaWhenNeeded(final Fragment fragment) { @Override public void visit(final FromClause fromClause) { appendKeyWord(" FROM"); + setLastVisited(fromClause); } @Override @@ -87,6 +95,7 @@ public void visit(final Table table) { appendKeyWord(" AS "); append(as.get()); } + setLastVisited(table); } @Override @@ -100,12 +109,14 @@ public void visit(final Join join) { append(join.getName()); appendKeyWord(" ON "); append(join.getSpecification()); + setLastVisited(join); } @Override public void visit(final BooleanValueExpression value) { appendSpace(); appendRenderedExpression(value.getExpression()); + setLastVisited(value); } private void appendRenderedExpression(final BooleanExpression expression) { @@ -114,6 +125,13 @@ private void appendRenderedExpression(final BooleanExpression expression) { append(expressionRenderer.render()); } + @Override + public void visit(final WhereClause whereClause) { + appendKeyWord(" WHERE "); + appendRenderedExpression(whereClause.getExpression()); + setLastVisited(whereClause); + } + @Override public void visit(final LimitClause limit) { appendKeyWord(" LIMIT "); @@ -122,18 +140,13 @@ public void visit(final LimitClause limit) { appendKeyWord(", "); } append(limit.getCount()); + setLastVisited(limit); } private void append(final int number) { this.builder.append(number); } - @Override - public void visit(final WhereClause whereClause) { - appendKeyWord(" WHERE "); - appendRenderedExpression(whereClause.getExpression()); - } - /** * Create a renderer for the given {@link Fragment} and render it. * @@ -142,7 +155,7 @@ public void visit(final WhereClause whereClause) { */ public static String render(final Fragment fragment) { final SqlStatementRenderer renderer = new SqlStatementRenderer(); - ((Fragment) fragment.getRoot()).accept(renderer); + fragment.accept(renderer); return renderer.render(); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/TestSelect.java index 991f0d36..a71c34ab 100644 --- a/src/test/java/com/exasol/dql/TestSelect.java +++ b/src/test/java/com/exasol/dql/TestSelect.java @@ -1,20 +1,45 @@ package com.exasol.dql; +import static com.exasol.sql.expression.BooleanTerm.eq; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; import com.exasol.sql.dql.FromClause; import com.exasol.sql.dql.Select; +import com.exasol.sql.expression.BooleanExpression; class TestSelect { + private Select select; + + @BeforeEach + void beforeEach() { + this.select = StatementFactory.getInstance().select(); + } + // [impl->req~statement-structure.step-wise~1] @Test void testGetFrom() { - final Select select = StatementFactory.getInstance().select().all(); - final FromClause from = select.from().table("persons"); - assertThat(select.from(), sameInstance(from)); + this.select.all(); + final FromClause from = this.select.from().table("persons"); + assertThat(this.select.from(), sameInstance(from)); + } + + // [impl->req~statement-structure.step-wise~1] + @Test + void testGetWhere() { + final BooleanExpression expression = eq("firstname", "Jane"); + this.select.all().from().table("persons").where(expression); + assertThat(this.select.where().getExpression(), sameInstance(expression)); + } + + // [impl->req~statement-structure.step-wise~1] + @Test + void testGetNonExistingFromThrowsException() { + assertThrows(IllegalStateException.class, () -> this.select.from()); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java index c0e529e2..06db5f05 100644 --- a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java @@ -3,75 +3,75 @@ import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; +import com.exasol.sql.dql.FromClause; +import com.exasol.sql.dql.Select; class TestJoinRendering { + private Select select; + private FromClause leftTable; + + @BeforeEach() + void beforeEach() { + this.select = StatementFactory.getInstance().select(); + this.leftTable = this.select.all().from().table("left_table"); + } + @Test - public void testJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").join("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testJoin() { + this.leftTable.join("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id"); + } + + private void assertRendersTo(final String expectedText) { + assertThat(this.select, rendersTo(expectedText)); } @Test - public void testInnerJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").innerJoin("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo("SELECT * FROM left_table INNER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testInnerJoin() { + this.leftTable.innerJoin("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo("SELECT * FROM left_table INNER JOIN right_table ON left_table.foo_id = right_table.foo_id"); } @Test - public void testLeftJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").leftJoin("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo("SELECT * FROM left_table LEFT JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testLeftJoin() { + this.leftTable.leftJoin("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo("SELECT * FROM left_table LEFT JOIN right_table ON left_table.foo_id = right_table.foo_id"); } @Test - public void testRightJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").rightJoin("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo("SELECT * FROM left_table RIGHT JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testRightJoin() { + this.leftTable.rightJoin("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo("SELECT * FROM left_table RIGHT JOIN right_table ON left_table.foo_id = right_table.foo_id"); } @Test - public void testFullJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").fullJoin("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo("SELECT * FROM left_table FULL JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testFullJoin() { + this.leftTable.fullJoin("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo("SELECT * FROM left_table FULL JOIN right_table ON left_table.foo_id = right_table.foo_id"); } @Test - public void testLeftOuterJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").leftOuterJoin("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo( - "SELECT * FROM left_table LEFT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testLeftOuterJoin() { + this.leftTable.leftOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo( + "SELECT * FROM left_table LEFT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"); } @Test - public void testRightOuterJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").rightOuterJoin("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo( - "SELECT * FROM left_table RIGHT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testRightOuterJoin() { + this.leftTable.rightOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo( + "SELECT * FROM left_table RIGHT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"); } @Test - public void testFullOuterJoin() { - assertThat( - StatementFactory.getInstance().select().all().from().table("left_table").fullOuterJoin("right_table", - "left_table.foo_id = right_table.foo_id"), - rendersTo( - "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); + void testFullOuterJoin() { + this.leftTable.fullOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"); + assertRendersTo( + "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java b/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java index 19b148f3..8191d47f 100644 --- a/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java @@ -3,20 +3,28 @@ import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; +import com.exasol.sql.dql.Select; class TestLimitRendering { + private Select select; + + @BeforeEach + void beforeEach() { + this.select = StatementFactory.getInstance().select(); + this.select.all().from().table("t"); + } + @Test void testLimitCountAfterFrom() { - assertThat(StatementFactory.getInstance().select().all().from().table("t").limit(1), - rendersTo("SELECT * FROM t LIMIT 1")); + assertThat(this.select.limit(1), rendersTo("SELECT * FROM t LIMIT 1")); } @Test void testLimitOffsetCountAfterFrom() { - assertThat(StatementFactory.getInstance().select().all().from().table("t").limit(2, 3), - rendersTo("SELECT * FROM t LIMIT 2, 3")); + assertThat(this.select.limit(2, 3), rendersTo("SELECT * FROM t LIMIT 2, 3")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java index a13c564e..8da4f086 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java @@ -2,73 +2,73 @@ import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersWithConfigTo; -import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; -import com.exasol.sql.expression.BooleanTerm; +import com.exasol.sql.dql.Select; import com.exasol.sql.rendering.StringRendererConfig; class TestSelectRendering { - @Test - void testGetParentReturnsNull() { - assertThat(StatementFactory.getInstance().select().getParent(), nullValue()); + private Select select; + + @BeforeEach + void beforeEach() { + this.select = StatementFactory.getInstance().select(); } @Test void testEmptySelect() { - assertThat(StatementFactory.getInstance().select(), rendersTo("SELECT")); + assertThat(this.select, rendersTo("SELECT")); } @Test void testEmptySelectLowerCase() { final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); - assertThat(StatementFactory.getInstance().select(), rendersWithConfigTo(config, "select")); + assertThat(this.select, rendersWithConfigTo(config, "select")); } @Test void testSelectAll() { - assertThat(StatementFactory.getInstance().select().all(), rendersTo("SELECT *")); + this.select.all(); + assertThat(this.select, rendersTo("SELECT *")); } @Test void testSelectFieldNames() { - assertThat(StatementFactory.getInstance().select().field("a", "b"), rendersTo("SELECT a, b")); + this.select.field("a", "b"); + assertThat(this.select, rendersTo("SELECT a, b")); } @Test void testSelectChainOfFieldNames() { - assertThat(StatementFactory.getInstance().select().field("a", "b").field("c"), rendersTo("SELECT a, b, c")); + this.select.field("a", "b").field("c"); + assertThat(this.select, rendersTo("SELECT a, b, c")); } @Test void testSelectFromTable() { - assertThat(StatementFactory.getInstance().select().all().from().table("persons"), - rendersTo("SELECT * FROM persons")); + this.select.all().from().table("persons"); + assertThat(this.select, rendersTo("SELECT * FROM persons")); } @Test void testSelectFromMultipleTable() { - assertThat(StatementFactory.getInstance().select().all().from().table("table1").table("table2"), - rendersTo("SELECT * FROM table1, table2")); + this.select.all().from().table("table1").table("table2"); + assertThat(this.select, rendersTo("SELECT * FROM table1, table2")); } @Test void testSelectFromTableAs() { - assertThat(StatementFactory.getInstance().select().all().from().tableAs("table", "t"), - rendersTo("SELECT * FROM table AS t")); + this.select.all().from().tableAs("table", "t"); + assertThat(this.select, rendersTo("SELECT * FROM table AS t")); } @Test void testSelectFromMultipleTableAs() { - assertThat(StatementFactory.getInstance().select().all().from().tableAs("table1", "t1").tableAs("table2", "t2"), - rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); - } - - @Test - void testSelectEmbeddedBooleanExpression() { - assertThat(StatementFactory.getInstance().select().value(BooleanTerm.not("a")), rendersTo("SELECT NOT(a)")); + this.select.all().from().tableAs("table1", "t1").tableAs("table2", "t2"); + assertThat(this.select, rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java b/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java index 5eaf9613..1aa28ef6 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java +++ b/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java @@ -3,15 +3,24 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; +import com.exasol.sql.dql.Select; import com.exasol.sql.rendering.SqlStatementRenderer; class TestSqlStatementRenderer { + private Select select; + + @BeforeEach + void beforeEach() { + select = StatementFactory.getInstance().select(); + } + @Test void testCreateAndRender() { - assertThat(SqlStatementRenderer.render(StatementFactory.getInstance().select().all().from().table("foo")), - equalTo("SELECT * FROM foo")); + this.select.all().from().table("foo"); + assertThat(SqlStatementRenderer.render(this.select), equalTo("SELECT * FROM foo")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java b/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java index 9b61aea7..5647e795 100644 --- a/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java @@ -4,14 +4,24 @@ import static com.exasol.sql.expression.BooleanTerm.eq; import static org.hamcrest.MatcherAssert.assertThat; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; +import com.exasol.sql.dql.Select; class TestWhereRendering { + private Select select; + + @BeforeEach + void beforeEach() { + this.select = StatementFactory.getInstance().select(); + this.select.all().from().table("person"); + } + @Test - public void testWhere() { - assertThat(StatementFactory.getInstance().select().all().from().table("person").where(eq("firstname", "Jane")), + void testWhere() { + assertThat(this.select.where(eq("firstname", "Jane")), rendersTo("SELECT * FROM person WHERE firstname = Jane")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java index 03f54d94..96688f89 100644 --- a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java +++ b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java @@ -30,7 +30,7 @@ private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final */ @Override public boolean matchesSafely(final Fragment fragment) { - ((Fragment) fragment.getRoot()).accept(this.renderer); + fragment.accept(this.renderer); this.renderedText = this.renderer.render(); return this.renderedText.equals(this.expectedText); } @@ -53,8 +53,7 @@ public static SqlFragmentRenderResultMatcher rendersTo(final String expectedText /** * Factory method for {@link SqlFragmentRenderResultMatcher} * - * @param config configuration settings for the - * {@link SqlStatementRenderer} + * @param config configuration settings for the {@link SqlStatementRenderer} * @param expectedText text that represents the expected rendering result * @return the matcher */ diff --git a/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java new file mode 100644 index 00000000..d85d98eb --- /dev/null +++ b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java @@ -0,0 +1,78 @@ +package com.exasol.sql.expression; + +import static com.exasol.sql.expression.BooleanTerm.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class TestBooleanTerm { + // [utest->dsn~boolean-operators~1] + @Test + void testOperationYieldsAnd() { + final BooleanExpression term = BooleanTerm.operation("and", not("a"), not("b")); + assertThat(term, instanceOf(And.class)); + } + + // [utest->dsn~boolean-operators~1] + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromUpperCaseYieldsAnd() { + assertThat(BooleanTerm.operation("AND", not("a"), not("b")), instanceOf(And.class)); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationYieldsOr() { + assertThat(BooleanTerm.operation("or", not("a"), not("b")), instanceOf(Or.class)); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromUpperCaseYieldsOr() { + assertThat(BooleanTerm.operation("OR", not("a"), not("b")), instanceOf(Or.class)); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationYieldsNot() { + assertThat(BooleanTerm.operation("not", not("a")), instanceOf(Not.class)); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromUpperCaseYieldsNot() { + assertThat(BooleanTerm.operation("NOT", not("a")), instanceOf(Not.class)); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromUnknownOperatorThrowsException() { + assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("illegal", not("a"))); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromNotWithMoreOrLessThanOneOperandThrowsException() { + assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("not", not("a"), not("b"))); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromUpperCaseUnknownOperatorThrowsException() { + assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("ILLEGAL", not("a"))); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromUpperCaseNotWithMoreOrLessThanOneOperandThrowsException() { + assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("NOT", not("a"), not("b"))); + } + + // [utest->dsn~boolean-operators~1] + @Test + void testOperationFromNullOperatorThrowsException() { + assertThrows(NullPointerException.class, () -> BooleanTerm.operation(null, not("a"), not("b"))); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java index 77a889c8..002720e0 100644 --- a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -12,66 +12,77 @@ import com.exasol.sql.rendering.StringRendererConfig; class TestBooleanExpressionRenderer { + // [utest->dsn~boolean-operators~1] @Test void testUnaryNotWithLiteral() { final BooleanExpression expression = not("a"); assertThat(expression, rendersTo("NOT(a)")); } + // [utest->dsn~boolean-operators~1] @Test void testUnaryNotWithExpression() { final BooleanExpression expression = not(not("a")); assertThat(expression, rendersTo("NOT(NOT(a))")); } + // [utest->dsn~boolean-operators~1] @Test void testAndWithLiterals() { final BooleanExpression expression = and("a", "b", "c"); assertThat(expression, rendersTo("a AND b AND c")); } + // [utest->dsn~boolean-operators~1] @Test void testAndWithLeftLiteralAndRightExpression() { final BooleanExpression expression = and("a", not("b")); assertThat(expression, rendersTo("a AND NOT(b)")); } + // [utest->dsn~boolean-operators~1] @Test void testAndWithLeftExpressionAndRightLiteral() { final BooleanExpression expression = and(not("a"), "b"); assertThat(expression, rendersTo("NOT(a) AND b")); } + // [utest->dsn~boolean-operators~1] @Test void testOrWithLiterals() { final BooleanExpression expression = or("a", "b", "c"); assertThat(expression, rendersTo("a OR b OR c")); } + // [utest->dsn~boolean-operators~1] @Test void testoRWithLeftLiteralAndRightExpression() { final BooleanExpression expression = or("a", not("b")); assertThat(expression, rendersTo("a OR NOT(b)")); } + // [utest->dsn~boolean-operators~1] @Test void testOrWithLeftExpressionAndRightLiteral() { final BooleanExpression expression = or(not("a"), "b"); assertThat(expression, rendersTo("NOT(a) OR b")); } + // [utest->dsn~boolean-operators~1] @Test void testOrWhitNestedAnd() { final BooleanExpression expression = or(and(not("a"), "b"), and("c", "d")); assertThat(expression, rendersTo("(NOT(a) AND b) OR (c AND d)")); } + // [utest->dsn~boolean-operators~1] @Test void testAndWhitNestedOr() { final BooleanExpression expression = and(or(not("a"), "b"), or("c", "d")); assertThat(expression, rendersTo("(NOT(a) OR b) AND (c OR d)")); } + // [utest->dsn~boolean-operators~1] @Test void testAndWhitNestedOrInLowercase() { final BooleanExpression expression = and(or(not("a"), "b"), or("c", "d")); From 0d3eaf23f67904358e70b5fd1b351ae38aa48a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Fri, 5 Oct 2018 14:40:25 +0200 Subject: [PATCH 22/43] PMI-86: Test coverage as close to 100% as possible. --- .../java/com/exasol/sql/AbstractFragment.java | 2 +- .../java/com/exasol/sql/FragmentVisitor.java | 2 - .../sql/dql/BooleanValueExpression.java | 37 ------------------ .../java/com/exasol/sql/dql/FromClause.java | 22 ----------- src/main/java/com/exasol/sql/dql/Select.java | 12 ------ .../exasol/sql/expression/BooleanTerm.java | 2 +- .../sql/rendering/SqlStatementRenderer.java | 13 +------ src/test/java/com/exasol/dql/TestSelect.java | 27 +++---------- .../dql/rendering/TestJoinRendering.java | 39 ++++++++----------- .../dql/rendering/TestSelectRendering.java | 22 ++++------- .../SqlFragmentRenderResultMatcher.java | 5 +-- 11 files changed, 36 insertions(+), 147 deletions(-) delete mode 100644 src/main/java/com/exasol/sql/dql/BooleanValueExpression.java diff --git a/src/main/java/com/exasol/sql/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java index cad0fba9..2cde0b3f 100644 --- a/src/main/java/com/exasol/sql/AbstractFragment.java +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -17,6 +17,6 @@ public AbstractFragment(final SqlStatement rootStatement) { @Override public Fragment getRoot() { - return this.rootStatement; + return (this.rootStatement == null) ? this : this.rootStatement; } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index 9205d86b..0261a928 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -16,8 +16,6 @@ public interface FragmentVisitor { public void visit(Join join); - public void visit(BooleanValueExpression booleanValueExpression); - public void visit(LimitClause limitClause); public void visit(WhereClause whereClause); diff --git a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java deleted file mode 100644 index c224e95f..00000000 --- a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.exasol.sql.dql; - -import com.exasol.sql.FragmentVisitor; -import com.exasol.sql.SqlStatement; -import com.exasol.sql.expression.BooleanExpression; - -/** - * Boolean value expression - */ -public class BooleanValueExpression extends ValueExpression { - private final BooleanExpression expression; - - /** - * Create a new instance of a {@link BooleanValueExpression} - * - * @param root SQL statement this expression belongs to - * @param expression nested boolean expression - */ - public BooleanValueExpression(final SqlStatement root, final BooleanExpression expression) { - super(root); - this.expression = expression; - } - - /** - * Get the boolean expression nested in this value expression - * - * @return nested boolean expression - */ - public BooleanExpression getExpression() { - return this.expression; - } - - @Override - public void accept(final FragmentVisitor visitor) { - visitor.visit(this); - } -} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index 9d16fe9c..deeaf9f8 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -4,7 +4,6 @@ import java.util.List; import com.exasol.sql.*; -import com.exasol.sql.expression.BooleanExpression; /** * This class represents the FROM clause of an SQL SELECT statement. @@ -141,27 +140,6 @@ public FromClause fullOuterJoin(final String name, final String specification) { return this; } - /** - * @see com.exasol.sql.dql.Select#limit(int) - */ - public Select limit(final int count) { - return ((Select) this.rootStatement).limit(count); - } - - /** - * @see com.exasol.sql.dql.Select#limit(int,int) - */ - public Select limit(final int offset, final int count) { - return ((Select) this.rootStatement).limit(offset, count); - } - - /** - * @see com.exasol.sql.dql.Select#where(BooleanExpression) - */ - public Select where(final BooleanExpression expression) { - return ((Select) this.rootStatement).where(expression); - } - @Override public void accept(final FragmentVisitor visitor) { visitor.visit(this); diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 9156b8bd..ff8e2846 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -103,18 +103,6 @@ public synchronized Select where(final BooleanExpression expression) { return this; } - /** - * Get the {@link WhereClause} of this select statement - * - * @return from clause - */ - public synchronized WhereClause where() { - if (this.whereClause == null) { - throw new IllegalStateException("Tried to access a WHERE clause before it was constructed."); - } - return this.whereClause; - } - @Override public void accept(final FragmentVisitor visitor) { visitor.visit(this); diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 431a5b4a..aac57644 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -86,7 +86,7 @@ public static BooleanExpression ge(final String left, final String right) { * @param operator name of the operator * @param expressions operands * @return instance of either {@link And}, {@link Or} or {@link Not} - * @throws IllegalArgumentException + * @throws IllegalArgumentException if the operator is unknown or null */ public static BooleanExpression operation(final String operator, final BooleanExpression... expressions) throws IllegalArgumentException { diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java index 5dcc6357..ee57b8a6 100644 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -9,8 +9,7 @@ import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; /** - * The {@link SqlStatementRenderer} turns SQL statement structures in to SQL - * strings. + * The {@link SqlStatementRenderer} turns SQL statement structures in to SQL strings. */ public class SqlStatementRenderer implements FragmentVisitor { private final StringBuilder builder = new StringBuilder(); @@ -18,8 +17,7 @@ public class SqlStatementRenderer implements FragmentVisitor { private Fragment lastVisited; /** - * Create a new {@link SqlStatementRenderer} using the default - * {@link StringRendererConfig}. + * Create a new {@link SqlStatementRenderer} using the default {@link StringRendererConfig}. */ public SqlStatementRenderer() { this(new StringRendererConfig.Builder().build()); @@ -112,13 +110,6 @@ public void visit(final Join join) { setLastVisited(join); } - @Override - public void visit(final BooleanValueExpression value) { - appendSpace(); - appendRenderedExpression(value.getExpression()); - setLastVisited(value); - } - private void appendRenderedExpression(final BooleanExpression expression) { final BooleanExpressionRenderer expressionRenderer = new BooleanExpressionRenderer(); expression.accept(expressionRenderer); diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/dql/TestSelect.java index a71c34ab..37a50427 100644 --- a/src/test/java/com/exasol/dql/TestSelect.java +++ b/src/test/java/com/exasol/dql/TestSelect.java @@ -1,17 +1,12 @@ package com.exasol.dql; -import static com.exasol.sql.expression.BooleanTerm.eq; -import static org.hamcrest.CoreMatchers.sameInstance; -import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; -import com.exasol.sql.dql.FromClause; import com.exasol.sql.dql.Select; -import com.exasol.sql.expression.BooleanExpression; class TestSelect { private Select select; @@ -21,25 +16,15 @@ void beforeEach() { this.select = StatementFactory.getInstance().select(); } - // [impl->req~statement-structure.step-wise~1] @Test - void testGetFrom() { - this.select.all(); - final FromClause from = this.select.from().table("persons"); - assertThat(this.select.from(), sameInstance(from)); + void testLimitTwiceThrowsException() { + this.select.limit(1); + assertThrows(IllegalStateException.class, () -> this.select.limit(2)); } - // [impl->req~statement-structure.step-wise~1] @Test - void testGetWhere() { - final BooleanExpression expression = eq("firstname", "Jane"); - this.select.all().from().table("persons").where(expression); - assertThat(this.select.where().getExpression(), sameInstance(expression)); - } - - // [impl->req~statement-structure.step-wise~1] - @Test - void testGetNonExistingFromThrowsException() { - assertThrows(IllegalStateException.class, () -> this.select.from()); + void testLimitWithOffsetTwiceThrowsException() { + this.select.limit(1, 2); + assertThrows(IllegalStateException.class, () -> this.select.limit(2, 3)); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java index 06db5f05..8980d91a 100644 --- a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java @@ -22,56 +22,49 @@ void beforeEach() { @Test void testJoin() { - this.leftTable.join("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id"); - } - - private void assertRendersTo(final String expectedText) { - assertThat(this.select, rendersTo(expectedText)); + assertThat(this.leftTable.join("right_table", "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test void testInnerJoin() { - this.leftTable.innerJoin("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo("SELECT * FROM left_table INNER JOIN right_table ON left_table.foo_id = right_table.foo_id"); + assertThat(this.leftTable.innerJoin("right_table", "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table INNER JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test void testLeftJoin() { - this.leftTable.leftJoin("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo("SELECT * FROM left_table LEFT JOIN right_table ON left_table.foo_id = right_table.foo_id"); + assertThat(this.leftTable.leftJoin("right_table", "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table LEFT JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test void testRightJoin() { - this.leftTable.rightJoin("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo("SELECT * FROM left_table RIGHT JOIN right_table ON left_table.foo_id = right_table.foo_id"); + assertThat(this.leftTable.rightJoin("right_table", "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table RIGHT JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test void testFullJoin() { - this.leftTable.fullJoin("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo("SELECT * FROM left_table FULL JOIN right_table ON left_table.foo_id = right_table.foo_id"); + assertThat(this.leftTable.fullJoin("right_table", "left_table.foo_id = right_table.foo_id"), + rendersTo("SELECT * FROM left_table FULL JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test void testLeftOuterJoin() { - this.leftTable.leftOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo( - "SELECT * FROM left_table LEFT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"); + assertThat(this.leftTable.leftOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo( + "SELECT * FROM left_table LEFT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test void testRightOuterJoin() { - this.leftTable.rightOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo( - "SELECT * FROM left_table RIGHT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"); + assertThat(this.leftTable.rightOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo( + "SELECT * FROM left_table RIGHT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); } @Test void testFullOuterJoin() { - this.leftTable.fullOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"); - assertRendersTo( - "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"); + assertThat(this.leftTable.fullOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo( + "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java index 8da4f086..9a40405c 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java @@ -32,43 +32,37 @@ void testEmptySelectLowerCase() { @Test void testSelectAll() { - this.select.all(); - assertThat(this.select, rendersTo("SELECT *")); + assertThat(this.select.all(), rendersTo("SELECT *")); } @Test void testSelectFieldNames() { - this.select.field("a", "b"); - assertThat(this.select, rendersTo("SELECT a, b")); + assertThat(this.select.field("a", "b"), rendersTo("SELECT a, b")); } @Test void testSelectChainOfFieldNames() { - this.select.field("a", "b").field("c"); - assertThat(this.select, rendersTo("SELECT a, b, c")); + assertThat(this.select.field("a", "b").field("c"), rendersTo("SELECT a, b, c")); } @Test void testSelectFromTable() { - this.select.all().from().table("persons"); - assertThat(this.select, rendersTo("SELECT * FROM persons")); + assertThat(this.select.all().from().table("persons"), rendersTo("SELECT * FROM persons")); } @Test void testSelectFromMultipleTable() { - this.select.all().from().table("table1").table("table2"); - assertThat(this.select, rendersTo("SELECT * FROM table1, table2")); + assertThat(this.select.all().from().table("table1").table("table2"), rendersTo("SELECT * FROM table1, table2")); } @Test void testSelectFromTableAs() { - this.select.all().from().tableAs("table", "t"); - assertThat(this.select, rendersTo("SELECT * FROM table AS t")); + assertThat(this.select.all().from().tableAs("table", "t"), rendersTo("SELECT * FROM table AS t")); } @Test void testSelectFromMultipleTableAs() { - this.select.all().from().tableAs("table1", "t1").tableAs("table2", "t2"); - assertThat(this.select, rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); + assertThat(this.select.all().from().tableAs("table1", "t1").tableAs("table2", "t2"), + rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java index 96688f89..b20e1388 100644 --- a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java +++ b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java @@ -7,8 +7,7 @@ import com.exasol.sql.rendering.StringRendererConfig; /** - * This class implements a matcher for the results of rendering SQL statements - * to text. + * This class implements a matcher for the results of rendering SQL statements to text. */ public class SqlFragmentRenderResultMatcher extends AbstractRenderResultMatcher { private final SqlStatementRenderer renderer; @@ -30,7 +29,7 @@ private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final */ @Override public boolean matchesSafely(final Fragment fragment) { - fragment.accept(this.renderer); + fragment.getRoot().accept(this.renderer); this.renderedText = this.renderer.render(); return this.renderedText.equals(this.expectedText); } From ff4e0a3795cc2ba40e478833d67a180e45aa1d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 8 Oct 2018 09:57:13 +0200 Subject: [PATCH 23/43] PMI-86: Added comparison creation from operator enum. --- doc/design.md | 32 +++++++++++++++++-- .../exasol/sql/expression/BooleanTerm.java | 7 +++- .../sql/expression/TestBooleanTerm.java | 12 +++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/doc/design.md b/doc/design.md index 05e8af3d..57eb7fd8 100644 --- a/doc/design.md +++ b/doc/design.md @@ -11,7 +11,7 @@ The Data Query Language (DQL) building block is responsible for managing `SELECT ### Building Select Statements -##### Accessing the Clauses That Make Up a SELECT Statement +#### Accessing the Clauses That Make Up a SELECT Statement `dsn~select-statement.accessing-clauses~1` The DQL statement component allows getting the following clauses, provided that they already exist: @@ -23,10 +23,38 @@ Covers: * `req~statement-structure.step-wise~1` -Needs: impl, utest, itest +Needs: impl, utest Tags: Select Statement Builder +##### Constructing Boolean Comparison Operations From Operator Strings +`dsn~boolean-operation.comparison.constructing-from-strings~1` + +The Boolean Expression builder allows creating expression objects from a string representing the comparison operator (options listed below) and a list of operands. + +* `>` +* `<` +* `=` +* `>=` +* `<=` +* `<>` + +Covers: + +* `req~boolean-operators~1` + +Needs: impl, utest + +##### Constructing Boolean Comparison Operations From Operator Enumeration +`dsn~boolean-operation.comparison.constructing-from-enum~1` + +The Boolean Expression builder allows creating expression objects from a enumeration of comparison operators. +Covers: + +* `req~boolean-operators~1` + +Needs: impl, utest + ## Forwarded Requirements * dsn --> impl, utest : req~comparison-operations~1 diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index aac57644..7ce434df 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -45,11 +45,16 @@ public static BooleanExpression or(final BooleanExpression... expressions) { return new Or(expressions); } - // [impl->dsn~comparison-operations~1] + // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] public static BooleanExpression compare(final String left, final String operatorSymbol, final String right) { return new Comparison(ComparisonOperator.ofSymbol(operatorSymbol), Literal.of(left), Literal.of(right)); } + // [dsn~boolean-operation.comparison.constructing-from-enum~1] + public static Object compare(final String left, final ComparisonOperator operator, final String right) { + return new Comparison(operator, Literal.of(left), Literal.of(right)); + } + // [impl->dsn~comparison-operations~1] public static BooleanExpression eq(final String left, final String right) { return new Comparison(ComparisonOperator.EQUAL, Literal.of(left), Literal.of(right)); diff --git a/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java index d85d98eb..9c7e77d3 100644 --- a/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java +++ b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java @@ -75,4 +75,16 @@ void testOperationFromUpperCaseNotWithMoreOrLessThanOneOperandThrowsException() void testOperationFromNullOperatorThrowsException() { assertThrows(NullPointerException.class, () -> BooleanTerm.operation(null, not("a"), not("b"))); } + + // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] + @Test + void testOperationFromComparisonOperatorString() { + assertThat(BooleanTerm.compare("a", "<>", "b"), instanceOf(Comparison.class)); + } + + // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] + @Test + void testOperationFromComparisonOperatorEnum() { + assertThat(BooleanTerm.compare("a", ComparisonOperator.NOT_EQUAL, "b"), instanceOf(Comparison.class)); + } } \ No newline at end of file From 9944ca95c4255b4a8b84a448a4b1a498f9a60f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 8 Oct 2018 12:25:46 +0200 Subject: [PATCH 24/43] PMI-86: Fixed operator connection and parenthesis for nested comparison. --- src/main/java/com/exasol/sql/expression/BooleanTerm.java | 2 +- .../expression/rendering/BooleanExpressionRenderer.java | 8 +++++++- .../rendering/TestBooleanExpressionRenderer.java | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 7ce434df..0ae285e4 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -51,7 +51,7 @@ public static BooleanExpression compare(final String left, final String operator } // [dsn~boolean-operation.comparison.constructing-from-enum~1] - public static Object compare(final String left, final ComparisonOperator operator, final String right) { + public static BooleanExpression compare(final String left, final ComparisonOperator operator, final String right) { return new Comparison(operator, Literal.of(left), Literal.of(right)); } diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java index 9e7b55ea..14657429 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -109,6 +109,10 @@ public String render() { @Override public void visit(final Comparison comparison) { + connect(comparison); + if (!comparison.isRoot()) { + startParenthesis(); + } comparison.getLeftOperand().accept(this); this.builder.append(" "); this.builder.append(comparison.getOperator().toString()); @@ -118,6 +122,8 @@ public void visit(final Comparison comparison) { @Override public void leave(final Comparison comparison) { - // intentionally empty + if (!comparison.isRoot()) { + endParenthesis(comparison); + } } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java index 002720e0..5e8129a2 100644 --- a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.expression.ComparisonOperator; import com.exasol.sql.rendering.StringRendererConfig; class TestBooleanExpressionRenderer { @@ -33,6 +34,14 @@ void testAndWithLiterals() { assertThat(expression, rendersTo("a AND b AND c")); } + // [utest->dsn~boolean-operators~1] + @Test + void testAndNestedComparisons() { + final BooleanExpression expression = and(compare("a", ComparisonOperator.EQUAL, "b"), + compare("c", ComparisonOperator.NOT_EQUAL, "d")); + assertThat(expression, rendersTo("(a = b) AND (c <> d)")); + } + // [utest->dsn~boolean-operators~1] @Test void testAndWithLeftLiteralAndRightExpression() { From 35cb3e902bc05155e2a681aa1159693a9c59fdc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 9 Oct 2018 09:07:40 +0200 Subject: [PATCH 25/43] PMI-86: Separated visitors and object structure for SELECT and INSERT. --- doc/design.md | 27 +++- doc/system_requirements.md | 102 +++++++++--- .../java/com/exasol/sql/AbstractFragment.java | 10 +- .../java/com/exasol/sql/{dql => }/Field.java | 8 +- src/main/java/com/exasol/sql/Fragment.java | 16 +- .../java/com/exasol/sql/FragmentVisitor.java | 12 -- .../java/com/exasol/sql/GenericFragment.java | 14 ++ .../java/com/exasol/sql/StatementFactory.java | 11 ++ .../java/com/exasol/sql/{dql => }/Table.java | 10 +- .../java/com/exasol/sql/TableReference.java | 4 + src/main/java/com/exasol/sql/dml/Insert.java | 56 +++++++ .../java/com/exasol/sql/dml/InsertFields.java | 45 ++++++ .../com/exasol/sql/dml/InsertFragment.java | 15 ++ .../com/exasol/sql/dml/InsertVisitor.java | 11 ++ .../sql/dml/rendering/InsertRenderer.java | 47 ++++++ .../com/exasol/sql/dql/FieldDefinition.java | 7 - .../java/com/exasol/sql/dql/FromClause.java | 26 +-- src/main/java/com/exasol/sql/dql/Join.java | 9 +- .../java/com/exasol/sql/dql/LimitClause.java | 7 +- src/main/java/com/exasol/sql/dql/Select.java | 4 +- .../com/exasol/sql/dql/SelectFragment.java | 15 ++ .../com/exasol/sql/dql/SelectVisitor.java | 15 ++ .../com/exasol/sql/dql/TableReference.java | 6 - .../java/com/exasol/sql/dql/WhereClause.java | 9 +- .../sql/dql/rendering/SelectRenderer.java | 109 +++++++++++++ .../rendering/AbstractFragmentRenderer.java | 56 +++++++ .../sql/rendering/FragmentRenderer.java | 10 ++ .../sql/rendering/SqlStatementRenderer.java | 152 ------------------ .../sql/rendering/StringRendererConfig.java | 15 +- .../BooleanExpressionRenderResultMatcher.java | 4 +- .../SqlFragmentRenderResultMatcher.java | 29 +++- .../java/com/exasol/sql/dml/TestInsert.java | 32 ++++ .../dml/rendering/TestInsertRendering.java | 32 ++++ .../com/exasol/{ => sql}/dql/TestSelect.java | 2 +- .../dql/rendering/TestJoinRendering.java | 2 +- .../dql/rendering/TestLimitRendering.java | 2 +- .../dql/rendering/TestSelectRendering.java | 22 +-- .../rendering/TestSqlStatementRenderer.java | 9 +- .../dql/rendering/TestWhereRendering.java | 2 +- 39 files changed, 674 insertions(+), 290 deletions(-) rename src/main/java/com/exasol/sql/{dql => }/Field.java (76%) create mode 100644 src/main/java/com/exasol/sql/GenericFragment.java rename src/main/java/com/exasol/sql/{dql => }/Table.java (85%) create mode 100644 src/main/java/com/exasol/sql/TableReference.java create mode 100644 src/main/java/com/exasol/sql/dml/Insert.java create mode 100644 src/main/java/com/exasol/sql/dml/InsertFields.java create mode 100644 src/main/java/com/exasol/sql/dml/InsertFragment.java create mode 100644 src/main/java/com/exasol/sql/dml/InsertVisitor.java create mode 100644 src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java delete mode 100644 src/main/java/com/exasol/sql/dql/FieldDefinition.java create mode 100644 src/main/java/com/exasol/sql/dql/SelectFragment.java create mode 100644 src/main/java/com/exasol/sql/dql/SelectVisitor.java delete mode 100644 src/main/java/com/exasol/sql/dql/TableReference.java create mode 100644 src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java create mode 100644 src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java create mode 100644 src/main/java/com/exasol/sql/rendering/FragmentRenderer.java delete mode 100644 src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java create mode 100644 src/test/java/com/exasol/sql/dml/TestInsert.java create mode 100644 src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java rename src/test/java/com/exasol/{ => sql}/dql/TestSelect.java (96%) rename src/test/java/com/exasol/{ => sql}/dql/rendering/TestJoinRendering.java (98%) rename src/test/java/com/exasol/{ => sql}/dql/rendering/TestLimitRendering.java (95%) rename src/test/java/com/exasol/{ => sql}/dql/rendering/TestSelectRendering.java (75%) rename src/test/java/com/exasol/{ => sql}/dql/rendering/TestSqlStatementRenderer.java (59%) rename src/test/java/com/exasol/{ => sql}/dql/rendering/TestWhereRendering.java (95%) diff --git a/doc/design.md b/doc/design.md index 57eb7fd8..bf237a2a 100644 --- a/doc/design.md +++ b/doc/design.md @@ -27,7 +27,9 @@ Needs: impl, utest Tags: Select Statement Builder -##### Constructing Boolean Comparison Operations From Operator Strings +### Building Boolean Expressions + +#### Constructing Boolean Comparison Operations From Operator Strings `dsn~boolean-operation.comparison.constructing-from-strings~1` The Boolean Expression builder allows creating expression objects from a string representing the comparison operator (options listed below) and a list of operands. @@ -45,7 +47,7 @@ Covers: Needs: impl, utest -##### Constructing Boolean Comparison Operations From Operator Enumeration +#### Constructing Boolean Comparison Operations From Operator Enumeration `dsn~boolean-operation.comparison.constructing-from-enum~1` The Boolean Expression builder allows creating expression objects from a enumeration of comparison operators. @@ -55,7 +57,22 @@ Covers: Needs: impl, utest -## Forwarded Requirements +#### Forwarded Requirements + +* `dsn --> impl, utest : req~comparison-operations~1` +* `dsn --> impl, utest : req~boolean-operators~1` + +### Building INSERT Statements + +#### Forwarded Requirements + +* `dsn --> impl, utest: req~insert-statements~1` +* `dsn --> impl, utest: req~values-as-insert-source~1` + +### Rendering Statements + +#### Forwarded Requirements -* dsn --> impl, utest : req~comparison-operations~1 -* dsn --> impl, utest : req~boolean-operators~1 \ No newline at end of file +* `dsn --> req~rendering.sql.configurable-case~1` +* `dsn --> impl, utest: req~rendering.sql.select~1` +* `dsn --> impl, utest: req~rendering.sql.insert~1` diff --git a/doc/system_requirements.md b/doc/system_requirements.md index e06960b2..9e7a0562 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -31,6 +31,14 @@ This document uses Augmented Backusโ€“Naur Form (ABNF) for syntax definitions. This subsection list the ABNF terminals used in this document. Terminals are ABNF rules that cannot be split further down, like string literals for example. +##### General Terminals + + COMMA = "," + + L-BRACKET = "(" + + R-BRACKET = ")" + ##### Operator Terminals EQUAL-OPERATOR = "=" @@ -80,7 +88,7 @@ Needs: req ### Statement Structure -##### Building the Statement Structure Step-wise +#### Building the Statement Structure Step-wise `req~statement-structure.step-wise~1` ESB lets users build the statement structure step-by-step. @@ -95,6 +103,73 @@ Covers: Needs: dsn +### General SQL Construction + +#### Comparison Operations +`req~comparison-operations~1` + +ESB supports the following comparison operations: + + operation = left-operand operator right-operand + + left-operand = field-reference / literal + + operator = equal-operator / not-equal-operator / greater-operator / less-than-operator / + greater-or-equal-operator / less-than-or-equal-operator + + right-operand = field-reference / literal + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +#### Boolean Operators +`req~boolean-operators~1` + +ESB supports the following boolean operators: `AND`, `OR` and `NOT` + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +### Data Manipulation Language + +#### INSERT Statements +`req~insert-statements~1` + +ESB supports the following insert statement: + + insert-statement = "INSERT INTO" table-reference [insert-columns] + insert-source + + table-reference = table [AS table-alias] + + insert-columns = L-BRACKET column *( COMMA column ) R-BRACKET + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +#### Values as INSERT SOURCE +`req~values-as-insert-source~1` + +ESB supports a list of explicit values as INSERT source: + + insert-source =/ "VALUES" L-BRACKET ( value-expression / + "DEFAULT" ) R-BRACKET + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + ### SQL String Rendering #### Configurable Case Rendering @@ -115,34 +190,25 @@ Needs: dsn * Upper case / lower case * One line / pretty -#### Comparison Operations -`req~comparison-operations~1` +#### SELECT Statement Rendering +`req~rendering.sql.select~1` -ESB supports the following comparison operations: - - operation = left-operand operator right-operand - - left-operand = field-reference / literal - - operator = equal-operator / not-equal-operator / greater-operator / less-than-operator / - greater-or-equal-operator / less-than-or-equal-operator - - right-operand = field-reference / literal +ESB renders abstract `SELECT` statements into SQL query strings. Covers: -* [feat~statement-definition~1](#statement-definition) +* [feat~sql-string-rendering~1](#sql-string-rendering) Needs: dsn -##### Boolean Operators -`req~boolean-operators~1` +#### INSERT Statement Rendering +`req~rendering.sql.insert~1` -ESB supports the following boolean operators: `AND`, `OR` and `NOT` +ESB renders abstract `INSERT` statements into SQL data manipulation language strings. Covers: -* [feat~statement-definition~1](#statement-definition) +* [feat~sql-string-rendering~1](#sql-string-rendering) Needs: dsn diff --git a/src/main/java/com/exasol/sql/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java index 2cde0b3f..374af2d6 100644 --- a/src/main/java/com/exasol/sql/AbstractFragment.java +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -4,19 +4,19 @@ * Common base class for SQL statement fragments */ public abstract class AbstractFragment implements Fragment { - protected final SqlStatement rootStatement; + private final Fragment root; /** * Create an instance of an SQL fragment * - * @param rootStatement root SQL statement this fragment belongs to. + * @param root root SQL statement this fragment belongs to. */ - public AbstractFragment(final SqlStatement rootStatement) { - this.rootStatement = rootStatement; + public AbstractFragment(final Fragment root) { + this.root = root; } @Override public Fragment getRoot() { - return (this.rootStatement == null) ? this : this.rootStatement; + return (this.root == null) ? this : this.root; } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Field.java b/src/main/java/com/exasol/sql/Field.java similarity index 76% rename from src/main/java/com/exasol/sql/dql/Field.java rename to src/main/java/com/exasol/sql/Field.java index 2d0f49f8..939ec369 100644 --- a/src/main/java/com/exasol/sql/dql/Field.java +++ b/src/main/java/com/exasol/sql/Field.java @@ -1,11 +1,9 @@ -package com.exasol.sql.dql; - -import com.exasol.sql.*; +package com.exasol.sql; /** * This class represents a table field in an SQL statement. */ -public class Field extends AbstractFragment { +public class Field extends AbstractFragment implements GenericFragment { private final String name; /** @@ -14,7 +12,7 @@ public class Field extends AbstractFragment { * @param root root SQL statement * @param name field name */ - protected Field(final SqlStatement root, final String name) { + public Field(final Fragment root, final String name) { super(root); this.name = name; } diff --git a/src/main/java/com/exasol/sql/Fragment.java b/src/main/java/com/exasol/sql/Fragment.java index e68ec8a8..deaa007c 100644 --- a/src/main/java/com/exasol/sql/Fragment.java +++ b/src/main/java/com/exasol/sql/Fragment.java @@ -1,22 +1,14 @@ package com.exasol.sql; /** - * This is the common interface for all fragments of SQL statements. Fragments - * can be clauses like the WHERE clause of an SELECT statement but also lower - * level concepts like boolean expressions. + * This is the common interface for all fragments of SQL statements. Fragments can be clauses like the WHERE clause of + * an SELECT statement but also lower level concepts like boolean expressions. */ public interface Fragment { - /** - * Accept a visitor (e.g. a renderer or validator) - * - * @param visitor visitor to accept - */ - public void accept(FragmentVisitor visitor); - /** * Get the root statement of this SQL fragment - * - * @return the root fragement + * + * @return the root fragment */ public Fragment getRoot(); } diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index 0261a928..e0816758 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -1,22 +1,10 @@ package com.exasol.sql; -import com.exasol.sql.dql.*; - /** * This interface represents a visitor for SQL statement fragments. */ public interface FragmentVisitor { - public void visit(final Select select); - public void visit(final Field field); - public void visit(FromClause fromClause); - public void visit(Table table); - - public void visit(Join join); - - public void visit(LimitClause limitClause); - - public void visit(WhereClause whereClause); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/GenericFragment.java b/src/main/java/com/exasol/sql/GenericFragment.java new file mode 100644 index 00000000..2e6859c8 --- /dev/null +++ b/src/main/java/com/exasol/sql/GenericFragment.java @@ -0,0 +1,14 @@ +package com.exasol.sql; + +/** + * Common interface for all SQL statement fragments which are used in multiple types of statements, like tables and + * fields. + */ +public interface GenericFragment extends Fragment { + /** + * Accept a generic fragment visitor + * + * @param visitor visitor + */ + public void accept(final FragmentVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/StatementFactory.java b/src/main/java/com/exasol/sql/StatementFactory.java index 865ea05c..87cdfe74 100644 --- a/src/main/java/com/exasol/sql/StatementFactory.java +++ b/src/main/java/com/exasol/sql/StatementFactory.java @@ -1,5 +1,6 @@ package com.exasol.sql; +import com.exasol.sql.dml.Insert; import com.exasol.sql.dql.Select; /** @@ -32,4 +33,14 @@ private StatementFactory() { public Select select() { return new Select(); } + + /** + * Create a {@link Insert} statement + * + * @param tableName name of the table into which to insert the data + * @return a new instance of a {@link Insert} statement + */ + public Insert insertInto(final String tableName) { + return new Insert(tableName); + } } diff --git a/src/main/java/com/exasol/sql/dql/Table.java b/src/main/java/com/exasol/sql/Table.java similarity index 85% rename from src/main/java/com/exasol/sql/dql/Table.java rename to src/main/java/com/exasol/sql/Table.java index 56095e53..009476ee 100644 --- a/src/main/java/com/exasol/sql/dql/Table.java +++ b/src/main/java/com/exasol/sql/Table.java @@ -1,13 +1,11 @@ -package com.exasol.sql.dql; +package com.exasol.sql; import java.util.Optional; -import com.exasol.sql.*; - /** * This class represents a {@link Table} in an SQL Statement */ -public class Table extends AbstractFragment implements TableReference { +public class Table extends AbstractFragment implements TableReference, GenericFragment { private final String name; private final Optional as; @@ -17,7 +15,7 @@ public class Table extends AbstractFragment implements TableReference { * @param root SQL statement this table belongs to * @param name table name */ - public Table(final SqlStatement root, final String name) { + public Table(final Fragment root, final String name) { super(root); this.name = name; this.as = Optional.empty(); @@ -30,7 +28,7 @@ public Table(final SqlStatement root, final String name) { * @param name table name * @param as table alias */ - public Table(final SqlStatement root, final String name, final String as) { + public Table(final Fragment root, final String name, final String as) { super(root); this.name = name; this.as = Optional.of(as); diff --git a/src/main/java/com/exasol/sql/TableReference.java b/src/main/java/com/exasol/sql/TableReference.java new file mode 100644 index 00000000..beb8dcc4 --- /dev/null +++ b/src/main/java/com/exasol/sql/TableReference.java @@ -0,0 +1,4 @@ +package com.exasol.sql; + +public interface TableReference extends Fragment { +} diff --git a/src/main/java/com/exasol/sql/dml/Insert.java b/src/main/java/com/exasol/sql/dml/Insert.java new file mode 100644 index 00000000..2c83517e --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/Insert.java @@ -0,0 +1,56 @@ +package com.exasol.sql.dml; + +import com.exasol.sql.*; +import com.exasol.sql.dql.Select; + +/** + * This class implements an SQL {@link Select} statement + */ +public class Insert extends AbstractFragment implements SqlStatement, InsertFragment { + private final Table table; + private InsertFields fields; + + /** + * Create a new instance of an {@link Insert} statement + * + * @param tableName name of the table into which the data should be inserted + */ + public Insert(final String tableName) { + super(null); + this.table = new Table(this, tableName); + } + + /** + * Define fields into which should be inserted + * + * @param names field names + * @return this for fluent programming + */ + public synchronized Insert field(final String... names) { + if (this.fields == null) { + this.fields = new InsertFields(this); + } + this.fields.add(names); + return this; + } + + /** + * Get the name of the table into which data should be inserted + * + * @return table name + */ + public String getTableName() { + return this.table.getName(); + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + if (this.table != null) { + this.table.accept(visitor); + } + if (this.fields != null) { + this.fields.accept(visitor); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertFields.java b/src/main/java/com/exasol/sql/dml/InsertFields.java new file mode 100644 index 00000000..f583c005 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertFields.java @@ -0,0 +1,45 @@ +package com.exasol.sql.dml; + +import java.util.ArrayList; +import java.util.List; + +import com.exasol.sql.*; + +public class InsertFields extends AbstractFragment implements InsertFragment { + private final List fields = new ArrayList<>(); + + /** + * Create an new instance of {@link InsertFields} + * + * @param root + */ + public InsertFields(final SqlStatement root) { + super(root); + } + + /** + * Define fields into which should be inserted + * + * @param names field names + */ + void add(final String... names) { + for (final String name : names) { + this.fields.add(new Field(getRoot(), name)); + } + } + + @Override + public Fragment getRoot() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + for (final Field field : this.fields) { + field.accept(visitor); + } + visitor.leave(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertFragment.java b/src/main/java/com/exasol/sql/dml/InsertFragment.java new file mode 100644 index 00000000..d30ba380 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertFragment.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dml; + +import com.exasol.sql.Fragment; + +/** + * This is the common interface for all fragments of a SELECT statement. + */ +public interface InsertFragment extends Fragment { + /** + * Accept a visitor (e.g. a renderer or validator) + * + * @param visitor visitor to accept + */ + public void accept(InsertVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertVisitor.java b/src/main/java/com/exasol/sql/dml/InsertVisitor.java new file mode 100644 index 00000000..71731729 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertVisitor.java @@ -0,0 +1,11 @@ +package com.exasol.sql.dml; + +import com.exasol.sql.FragmentVisitor; + +public interface InsertVisitor extends FragmentVisitor { + public void visit(Insert insert); + + public void visit(InsertFields insertFields); + + public void leave(InsertFields insertFields); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java new file mode 100644 index 00000000..02655c53 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -0,0 +1,47 @@ +package com.exasol.sql.dml.rendering; + +import com.exasol.sql.Field; +import com.exasol.sql.Table; +import com.exasol.sql.dml.*; +import com.exasol.sql.rendering.AbstractFragmentRenderer; +import com.exasol.sql.rendering.StringRendererConfig; + +public class InsertRenderer extends AbstractFragmentRenderer implements InsertVisitor { + /** + * Create a new {@link InsertRenderer} with custom render settings. + * + * @param config render configuration settings + */ + public InsertRenderer(final StringRendererConfig config) { + super(config); + } + + @Override + public void visit(final Insert insert) { + appendKeyWord("INSERT INTO "); + setLastVisited(insert); + } + + @Override + public void visit(final Table table) { + append(table.getName()); + setLastVisited(table); + } + + @Override + public void visit(final Field field) { + appendCommaWhenNeeded(field); + append(field.getName()); + setLastVisited(field); + } + + @Override + public void visit(final InsertFields insertFields) { + append(" ("); + } + + @Override + public void leave(final InsertFields insertFields) { + append(")"); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/FieldDefinition.java b/src/main/java/com/exasol/sql/dql/FieldDefinition.java deleted file mode 100644 index be560c0a..00000000 --- a/src/main/java/com/exasol/sql/dql/FieldDefinition.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.exasol.sql.dql; - -import com.exasol.sql.Fragment; - -public interface FieldDefinition extends Fragment { - -} diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index deeaf9f8..bf952297 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -8,7 +8,7 @@ /** * This class represents the FROM clause of an SQL SELECT statement. */ -public class FromClause extends AbstractFragment { +public class FromClause extends AbstractFragment implements SelectFragment { private final List
tables = new ArrayList<>(); private final List joins = new ArrayList<>(); @@ -17,7 +17,7 @@ public class FromClause extends AbstractFragment { * * @param root root SQL statement this FROM clause belongs to */ - public FromClause(final SqlStatement root) { + public FromClause(final Fragment root) { super(root); } @@ -28,7 +28,7 @@ public FromClause(final SqlStatement root) { * @return new instance */ public FromClause table(final String name) { - this.tables.add(new Table(this.rootStatement, name)); + this.tables.add(new Table(getRoot(), name)); return this; } @@ -40,7 +40,7 @@ public FromClause table(final String name) { * @return new instance */ public FromClause tableAs(final String name, final String as) { - this.tables.add(new Table(this.rootStatement, name, as)); + this.tables.add(new Table(getRoot(), name, as)); return this; } @@ -52,7 +52,7 @@ public FromClause tableAs(final String name, final String as) { * @return parent FROM clause */ public FromClause join(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.DEFAULT, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.DEFAULT, name, specification)); return this; } @@ -64,7 +64,7 @@ public FromClause join(final String name, final String specification) { * @return parent FROM clause */ public FromClause innerJoin(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.INNER, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.INNER, name, specification)); return this; } @@ -76,7 +76,7 @@ public FromClause innerJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause leftJoin(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.LEFT, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.LEFT, name, specification)); return this; } @@ -88,7 +88,7 @@ public FromClause leftJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause rightJoin(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.RIGHT, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.RIGHT, name, specification)); return this; } @@ -100,7 +100,7 @@ public FromClause rightJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause fullJoin(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.FULL, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.FULL, name, specification)); return this; } @@ -112,7 +112,7 @@ public FromClause fullJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause leftOuterJoin(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.LEFT_OUTER, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.LEFT_OUTER, name, specification)); return this; } @@ -124,7 +124,7 @@ public FromClause leftOuterJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause rightOuterJoin(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.RIGHT_OUTER, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.RIGHT_OUTER, name, specification)); return this; } @@ -136,12 +136,12 @@ public FromClause rightOuterJoin(final String name, final String specification) * @return parent FROM clause */ public FromClause fullOuterJoin(final String name, final String specification) { - this.joins.add(new Join(this.rootStatement, JoinType.FULL_OUTER, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.FULL_OUTER, name, specification)); return this; } @Override - public void accept(final FragmentVisitor visitor) { + public void accept(final SelectVisitor visitor) { visitor.visit(this); for (final Table table : this.tables) { table.accept(visitor); diff --git a/src/main/java/com/exasol/sql/dql/Join.java b/src/main/java/com/exasol/sql/dql/Join.java index b0d60510..bfed5d67 100644 --- a/src/main/java/com/exasol/sql/dql/Join.java +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -1,11 +1,12 @@ package com.exasol.sql.dql; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.Fragment; /** * This class implements the {@link Join} part of a WHERE clause. */ -public class Join extends AbstractFragment implements Fragment { +public class Join extends AbstractFragment implements SelectFragment { private final JoinType type; private final String name; private final String specification; @@ -18,7 +19,7 @@ public class Join extends AbstractFragment implements Fragment { * @param name name of the table to be joined * @param specification join specification (e.g. ON or USING) */ - public Join(final SqlStatement root, final JoinType type, final String name, final String specification) { + public Join(final Fragment root, final JoinType type, final String name, final String specification) { super(root); this.type = type; this.name = name; @@ -53,7 +54,7 @@ public String getSpecification() { } @Override - public void accept(final FragmentVisitor visitor) { + public void accept(final SelectVisitor visitor) { visitor.visit(this); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java index e60a1cae..08be0050 100644 --- a/src/main/java/com/exasol/sql/dql/LimitClause.java +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -1,12 +1,13 @@ package com.exasol.sql.dql; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.SqlStatement; /** * This class represents the limit clause of an SQL statement. It lets you choose offset and / or count of rows to be * handed back in the result. */ -public class LimitClause extends AbstractFragment { +public class LimitClause extends AbstractFragment implements SelectFragment { private final int count; private final int offset; @@ -66,7 +67,7 @@ public boolean hasOffset() { } @Override - public void accept(final FragmentVisitor visitor) { + public void accept(final SelectVisitor visitor) { visitor.visit(this); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index ff8e2846..3f54de0e 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -9,7 +9,7 @@ /** * This class implements an SQL {@link Select} statement */ -public class Select extends AbstractFragment implements SqlStatement { +public class Select extends AbstractFragment implements SqlStatement, SelectFragment { private final List fields = new ArrayList<>(); private FromClause fromClause = null; private WhereClause whereClause = null; @@ -104,7 +104,7 @@ public synchronized Select where(final BooleanExpression expression) { } @Override - public void accept(final FragmentVisitor visitor) { + public void accept(final SelectVisitor visitor) { visitor.visit(this); for (final Field field : this.fields) { field.accept(visitor); diff --git a/src/main/java/com/exasol/sql/dql/SelectFragment.java b/src/main/java/com/exasol/sql/dql/SelectFragment.java new file mode 100644 index 00000000..005efd73 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/SelectFragment.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.Fragment; + +/** + * This is the common interface for all fragments of a SELECT statement. + */ +public interface SelectFragment extends Fragment { + /** + * Accept a visitor (e.g. a renderer or validator) + * + * @param visitor visitor to accept + */ + public void accept(SelectVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/SelectVisitor.java b/src/main/java/com/exasol/sql/dql/SelectVisitor.java new file mode 100644 index 00000000..070c4b08 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/SelectVisitor.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.FragmentVisitor; + +public interface SelectVisitor extends FragmentVisitor { + public void visit(final Select select); + + public void visit(FromClause fromClause); + + public void visit(Join join); + + public void visit(LimitClause limitClause); + + public void visit(WhereClause whereClause); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/TableReference.java b/src/main/java/com/exasol/sql/dql/TableReference.java deleted file mode 100644 index c9cd0f4e..00000000 --- a/src/main/java/com/exasol/sql/dql/TableReference.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.exasol.sql.dql; - -import com.exasol.sql.Fragment; - -public interface TableReference extends Fragment { -} diff --git a/src/main/java/com/exasol/sql/dql/WhereClause.java b/src/main/java/com/exasol/sql/dql/WhereClause.java index 18f7c9bc..80935738 100644 --- a/src/main/java/com/exasol/sql/dql/WhereClause.java +++ b/src/main/java/com/exasol/sql/dql/WhereClause.java @@ -1,18 +1,19 @@ package com.exasol.sql.dql; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.SqlStatement; import com.exasol.sql.expression.BooleanExpression; /** * This class represents the where clause of an SQL statement. It contains the filter criteria in form of a * {@link BooleanExpression}. */ -public class WhereClause extends AbstractFragment { +public class WhereClause extends AbstractFragment implements SelectFragment { private final BooleanExpression expression; /** * Create a new instance of a {@link WhereClause} - * + * * @param root SQL statement this WHERE clause belongs to * @param expression */ @@ -31,7 +32,7 @@ public BooleanExpression getExpression() { } @Override - public void accept(final FragmentVisitor visitor) { + public void accept(final SelectVisitor visitor) { visitor.visit(this); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java new file mode 100644 index 00000000..f7bf7601 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java @@ -0,0 +1,109 @@ +package com.exasol.sql.dql.rendering; + +import java.util.Optional; + +import com.exasol.sql.*; +import com.exasol.sql.dql.*; +import com.exasol.sql.rendering.AbstractFragmentRenderer; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * The {@link SelectRenderer} turns SQL statement structures in to SQL strings. + */ +public class SelectRenderer extends AbstractFragmentRenderer implements SelectVisitor { + /** + * Create a new {@link SelectRenderer} with custom render settings. + * + * @param config render configuration settings + */ + public SelectRenderer(final StringRendererConfig config) { + super(config); + } + + @Override + public void visit(final Select select) { + appendKeyWord("SELECT "); + setLastVisited(select); + } + + @Override + public void visit(final Field field) { + appendCommaWhenNeeded(field); + append(field.getName()); + setLastVisited(field); + } + + @Override + public void visit(final FromClause fromClause) { + appendKeyWord(" FROM "); + setLastVisited(fromClause); + } + + @Override + public void visit(final Table table) { + appendCommaWhenNeeded(table); + append(table.getName()); + final Optional as = table.getAs(); + if (as.isPresent()) { + appendKeyWord(" AS "); + append(as.get()); + } + setLastVisited(table); + } + + @Override + public void visit(final Join join) { + final JoinType type = join.getType(); + if (type != JoinType.DEFAULT) { + appendSpace(); + appendKeyWord(type.toString()); + } + appendKeyWord(" JOIN "); + append(join.getName()); + appendKeyWord(" ON "); + append(join.getSpecification()); + setLastVisited(join); + } + + @Override + public void visit(final WhereClause whereClause) { + appendKeyWord(" WHERE "); + appendRenderedExpression(whereClause.getExpression()); + setLastVisited(whereClause); + } + + @Override + public void visit(final LimitClause limit) { + appendKeyWord(" LIMIT "); + if (limit.hasOffset()) { + append(limit.getOffset()); + appendKeyWord(", "); + } + append(limit.getCount()); + setLastVisited(limit); + } + + /** + * Create a renderer for the given {@link Fragment} and render it. + * + * @param fragment SQL statement fragment to be rendered + * @return rendered statement + */ + public static String render(final Fragment fragment) { + return render(fragment, StringRendererConfig.createDefault()); + } + + /** + * Create a renderer for the given {@link Fragment} and render it. + * + * @param fragment SQL statement fragment to be rendered + * @param config renderer configuration + * @return rendered statement + */ + public static String render(final Fragment fragment, final StringRendererConfig config) { + assert (fragment instanceof SelectFragment); + final SelectRenderer renderer = new SelectRenderer(config); + ((SelectFragment) fragment).accept(renderer); + return renderer.render(); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java new file mode 100644 index 00000000..4b7f4afe --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java @@ -0,0 +1,56 @@ +package com.exasol.sql.rendering; + +import com.exasol.sql.Fragment; +import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; + +/** + * Abstract base class for SQL fragment renderers + */ +public abstract class AbstractFragmentRenderer implements FragmentRenderer { + private final StringBuilder builder = new StringBuilder(); + protected final StringRendererConfig config; + private Fragment lastVisited; + + public AbstractFragmentRenderer(final StringRendererConfig config) { + this.config = config; + this.lastVisited = null; + } + + @Override + public String render() { + return this.builder.toString(); + } + + protected void appendKeyWord(final String keyword) { + append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); + } + + protected StringBuilder append(final String string) { + return this.builder.append(string); + } + + protected void setLastVisited(final Fragment fragment) { + this.lastVisited = fragment; + } + + protected void appendSpace() { + append(" "); + } + + protected void appendCommaWhenNeeded(final Fragment fragment) { + if (this.lastVisited.getClass().equals(fragment.getClass())) { + append(", "); + } + } + + protected void appendRenderedExpression(final BooleanExpression expression) { + final BooleanExpressionRenderer expressionRenderer = new BooleanExpressionRenderer(); + expression.accept(expressionRenderer); + append(expressionRenderer.render()); + } + + protected void append(final int number) { + this.builder.append(number); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/FragmentRenderer.java b/src/main/java/com/exasol/sql/rendering/FragmentRenderer.java new file mode 100644 index 00000000..b2c50341 --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/FragmentRenderer.java @@ -0,0 +1,10 @@ +package com.exasol.sql.rendering; + +public interface FragmentRenderer { + /** + * Render an SQL statement to a string. + * + * @return rendered string + */ + public String render(); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java deleted file mode 100644 index ee57b8a6..00000000 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.exasol.sql.rendering; - -import java.util.Optional; - -import com.exasol.sql.Fragment; -import com.exasol.sql.FragmentVisitor; -import com.exasol.sql.dql.*; -import com.exasol.sql.expression.BooleanExpression; -import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; - -/** - * The {@link SqlStatementRenderer} turns SQL statement structures in to SQL strings. - */ -public class SqlStatementRenderer implements FragmentVisitor { - private final StringBuilder builder = new StringBuilder(); - private final StringRendererConfig config; - private Fragment lastVisited; - - /** - * Create a new {@link SqlStatementRenderer} using the default {@link StringRendererConfig}. - */ - public SqlStatementRenderer() { - this(new StringRendererConfig.Builder().build()); - } - - /** - * Create a new {@link SqlStatementRenderer} with custom render settings. - * - * @param config render configuration settings - */ - public SqlStatementRenderer(final StringRendererConfig config) { - this.config = config; - } - - /** - * Render an SQL statement to a string. - * - * @return rendered string - */ - public String render() { - return this.builder.toString(); - } - - @Override - public void visit(final Select select) { - appendKeyWord("SELECT"); - setLastVisited(select); - } - - private void appendKeyWord(final String keyword) { - append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); - } - - private StringBuilder append(final String string) { - return this.builder.append(string); - } - - @Override - public void visit(final Field field) { - appendCommaWhenNeeded(field); - appendSpace(); - append(field.getName()); - setLastVisited(field); - } - - private void setLastVisited(final Fragment fragment) { - this.lastVisited = fragment; - } - - private void appendSpace() { - append(" "); - } - - private void appendCommaWhenNeeded(final Fragment fragment) { - if (this.lastVisited.getClass().equals(fragment.getClass())) { - append(","); - } - } - - @Override - public void visit(final FromClause fromClause) { - appendKeyWord(" FROM"); - setLastVisited(fromClause); - } - - @Override - public void visit(final Table table) { - appendCommaWhenNeeded(table); - appendSpace(); - append(table.getName()); - final Optional as = table.getAs(); - if (as.isPresent()) { - appendKeyWord(" AS "); - append(as.get()); - } - setLastVisited(table); - } - - @Override - public void visit(final Join join) { - final JoinType type = join.getType(); - if (type != JoinType.DEFAULT) { - appendSpace(); - appendKeyWord(type.toString()); - } - appendKeyWord(" JOIN "); - append(join.getName()); - appendKeyWord(" ON "); - append(join.getSpecification()); - setLastVisited(join); - } - - private void appendRenderedExpression(final BooleanExpression expression) { - final BooleanExpressionRenderer expressionRenderer = new BooleanExpressionRenderer(); - expression.accept(expressionRenderer); - append(expressionRenderer.render()); - } - - @Override - public void visit(final WhereClause whereClause) { - appendKeyWord(" WHERE "); - appendRenderedExpression(whereClause.getExpression()); - setLastVisited(whereClause); - } - - @Override - public void visit(final LimitClause limit) { - appendKeyWord(" LIMIT "); - if (limit.hasOffset()) { - append(limit.getOffset()); - appendKeyWord(", "); - } - append(limit.getCount()); - setLastVisited(limit); - } - - private void append(final int number) { - this.builder.append(number); - } - - /** - * Create a renderer for the given {@link Fragment} and render it. - * - * @param fragment SQL statement fragment to be rendered - * @return rendered statement - */ - public static String render(final Fragment fragment) { - final SqlStatementRenderer renderer = new SqlStatementRenderer(); - fragment.accept(renderer); - return renderer.render(); - } -} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java index 2ab8bf7d..07d99554 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java +++ b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java @@ -3,8 +3,7 @@ import com.exasol.sql.StatementFactory; /** - * This class implements a parameter object containing the configuration options - * for the {@link StatementFactory}. + * This class implements a parameter object containing the configuration options for the {@link StatementFactory}. */ public class StringRendererConfig { private final boolean lowerCase; @@ -40,8 +39,7 @@ public StringRendererConfig build() { /** * Define whether the statement should be produced in lower case * - * @param lowerCase set to true if the statement should be produced - * in lower case + * @param lowerCase set to true if the statement should be produced in lower case * @return this instance for fluent programming */ public Builder lowerCase(final boolean lowerCase) { @@ -49,4 +47,13 @@ public Builder lowerCase(final boolean lowerCase) { return this; } } + + /** + * Create the default configuration. + * + * @return default configuration + */ + public static StringRendererConfig createDefault() { + return new Builder().build(); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java index 22549ff6..65e627c8 100644 --- a/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java +++ b/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java @@ -2,9 +2,9 @@ import org.hamcrest.Description; +import com.exasol.sql.dql.rendering.SelectRenderer; import com.exasol.sql.expression.BooleanExpression; import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; -import com.exasol.sql.rendering.SqlStatementRenderer; import com.exasol.sql.rendering.StringRendererConfig; /** @@ -55,7 +55,7 @@ public static BooleanExpressionRenderResultMatcher rendersTo(final String expect * Factory method for {@link BooleanExpressionRenderResultMatcher} * * @param config configuration settings for the - * {@link SqlStatementRenderer} + * {@link SelectRenderer} * @param expectedText text that represents the expected rendering result * @return the matcher */ diff --git a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java index b20e1388..e4a19988 100644 --- a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java +++ b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java @@ -3,23 +3,27 @@ import org.hamcrest.Description; import com.exasol.sql.Fragment; -import com.exasol.sql.rendering.SqlStatementRenderer; +import com.exasol.sql.dml.InsertFragment; +import com.exasol.sql.dml.rendering.InsertRenderer; +import com.exasol.sql.dql.SelectFragment; +import com.exasol.sql.dql.rendering.SelectRenderer; import com.exasol.sql.rendering.StringRendererConfig; /** * This class implements a matcher for the results of rendering SQL statements to text. */ public class SqlFragmentRenderResultMatcher extends AbstractRenderResultMatcher { - private final SqlStatementRenderer renderer; + + private final StringRendererConfig config; private SqlFragmentRenderResultMatcher(final String expectedText) { super(expectedText); - this.renderer = new SqlStatementRenderer(); + this.config = StringRendererConfig.createDefault(); } private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final String expectedText) { super(expectedText); - this.renderer = new SqlStatementRenderer(config); + this.config = config; } /** @@ -29,8 +33,19 @@ private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final */ @Override public boolean matchesSafely(final Fragment fragment) { - fragment.getRoot().accept(this.renderer); - this.renderedText = this.renderer.render(); + final Fragment root = fragment.getRoot(); + if (root instanceof SelectFragment) { + final SelectRenderer renderer = new SelectRenderer(this.config); + ((SelectFragment) root).accept(renderer); + this.renderedText = renderer.render(); + } else if (root instanceof InsertFragment) { + final InsertRenderer renderer = new InsertRenderer(this.config); + ((InsertFragment) root).accept(renderer); + this.renderedText = renderer.render(); + } else { + throw new UnsupportedOperationException( + "Don't know how to render fragment of type\"" + root.getClass().getName() + "\"."); + } return this.renderedText.equals(this.expectedText); } @@ -52,7 +67,7 @@ public static SqlFragmentRenderResultMatcher rendersTo(final String expectedText /** * Factory method for {@link SqlFragmentRenderResultMatcher} * - * @param config configuration settings for the {@link SqlStatementRenderer} + * @param config configuration settings for the {@link SelectRenderer} * @param expectedText text that represents the expected rendering result * @return the matcher */ diff --git a/src/test/java/com/exasol/sql/dml/TestInsert.java b/src/test/java/com/exasol/sql/dml/TestInsert.java new file mode 100644 index 00000000..2894aa20 --- /dev/null +++ b/src/test/java/com/exasol/sql/dml/TestInsert.java @@ -0,0 +1,32 @@ +package com.exasol.sql.dml; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; + +class TestInsert { + private static final String TABLE_NAME = "person"; + private Insert insert; + + @BeforeEach + void beforeEach() { + this.insert = StatementFactory.getInstance().insertInto(TABLE_NAME); + } + + // [utest->dsn~insert-statements~1] + @Test + void testInsert() { + assertThat(this.insert, instanceOf(Insert.class)); + } + + // [utest->dsn~insert-statements~1] + @Test + void testInsertTableName() { + assertThat(this.insert.getTableName(), equalTo(TABLE_NAME)); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java new file mode 100644 index 00000000..b5492f01 --- /dev/null +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java @@ -0,0 +1,32 @@ +package com.exasol.sql.dml.rendering; + +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; +import com.exasol.sql.dml.Insert; + +class TestInsertRendering { + private static final String PERSON = "person"; + private Insert insert; + + @BeforeEach + void beforeEach() { + this.insert = StatementFactory.getInstance().insertInto(PERSON); + } + + // [dsn~rendering.sql.insert~1] + @Test + void testInsert() { + assertThat(this.insert, rendersTo("INSERT INTO person")); + } + + // [dsn~rendering.sql.insert~1] + @Test + void testInsertFields() { + assertThat(this.insert.field("a", "b"), rendersTo("INSERT INTO person (a, b)")); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/TestSelect.java b/src/test/java/com/exasol/sql/dql/TestSelect.java similarity index 96% rename from src/test/java/com/exasol/dql/TestSelect.java rename to src/test/java/com/exasol/sql/dql/TestSelect.java index 37a50427..3ec9176d 100644 --- a/src/test/java/com/exasol/dql/TestSelect.java +++ b/src/test/java/com/exasol/sql/dql/TestSelect.java @@ -1,4 +1,4 @@ -package com.exasol.dql; +package com.exasol.sql.dql; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestJoinRendering.java similarity index 98% rename from src/test/java/com/exasol/dql/rendering/TestJoinRendering.java rename to src/test/java/com/exasol/sql/dql/rendering/TestJoinRendering.java index 8980d91a..714c6049 100644 --- a/src/test/java/com/exasol/dql/rendering/TestJoinRendering.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestJoinRendering.java @@ -1,4 +1,4 @@ -package com.exasol.dql.rendering; +package com.exasol.sql.dql.rendering; import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestLimitRendering.java similarity index 95% rename from src/test/java/com/exasol/dql/rendering/TestLimitRendering.java rename to src/test/java/com/exasol/sql/dql/rendering/TestLimitRendering.java index 8191d47f..258a8004 100644 --- a/src/test/java/com/exasol/dql/rendering/TestLimitRendering.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestLimitRendering.java @@ -1,4 +1,4 @@ -package com.exasol.dql.rendering; +package com.exasol.sql.dql.rendering; import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java similarity index 75% rename from src/test/java/com/exasol/dql/rendering/TestSelectRendering.java rename to src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java index 9a40405c..e640566e 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java @@ -1,7 +1,6 @@ -package com.exasol.dql.rendering; +package com.exasol.sql.dql.rendering; import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; -import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersWithConfigTo; import static org.hamcrest.MatcherAssert.assertThat; import org.junit.jupiter.api.BeforeEach; @@ -9,7 +8,6 @@ import com.exasol.sql.StatementFactory; import com.exasol.sql.dql.Select; -import com.exasol.sql.rendering.StringRendererConfig; class TestSelectRendering { private Select select; @@ -19,47 +17,43 @@ void beforeEach() { this.select = StatementFactory.getInstance().select(); } - @Test - void testEmptySelect() { - assertThat(this.select, rendersTo("SELECT")); - } - - @Test - void testEmptySelectLowerCase() { - final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); - assertThat(this.select, rendersWithConfigTo(config, "select")); - } - + // [dsn~rendering.sql.select~1] @Test void testSelectAll() { assertThat(this.select.all(), rendersTo("SELECT *")); } + // [dsn~rendering.sql.select~1] @Test void testSelectFieldNames() { assertThat(this.select.field("a", "b"), rendersTo("SELECT a, b")); } + // [dsn~rendering.sql.select~1] @Test void testSelectChainOfFieldNames() { assertThat(this.select.field("a", "b").field("c"), rendersTo("SELECT a, b, c")); } + // [dsn~rendering.sql.select~1] @Test void testSelectFromTable() { assertThat(this.select.all().from().table("persons"), rendersTo("SELECT * FROM persons")); } + // [dsn~rendering.sql.select~1] @Test void testSelectFromMultipleTable() { assertThat(this.select.all().from().table("table1").table("table2"), rendersTo("SELECT * FROM table1, table2")); } + // [dsn~rendering.sql.select~1] @Test void testSelectFromTableAs() { assertThat(this.select.all().from().tableAs("table", "t"), rendersTo("SELECT * FROM table AS t")); } + // [dsn~rendering.sql.select~1] @Test void testSelectFromMultipleTableAs() { assertThat(this.select.all().from().tableAs("table1", "t1").tableAs("table2", "t2"), diff --git a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java b/src/test/java/com/exasol/sql/dql/rendering/TestSqlStatementRenderer.java similarity index 59% rename from src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java rename to src/test/java/com/exasol/sql/dql/rendering/TestSqlStatementRenderer.java index 1aa28ef6..9469fe38 100644 --- a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSqlStatementRenderer.java @@ -1,26 +1,25 @@ -package com.exasol.dql.rendering; +package com.exasol.sql.dql.rendering; +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; import com.exasol.sql.dql.Select; -import com.exasol.sql.rendering.SqlStatementRenderer; class TestSqlStatementRenderer { private Select select; @BeforeEach void beforeEach() { - select = StatementFactory.getInstance().select(); + this.select = StatementFactory.getInstance().select(); } @Test void testCreateAndRender() { this.select.all().from().table("foo"); - assertThat(SqlStatementRenderer.render(this.select), equalTo("SELECT * FROM foo")); + assertThat(this.select, rendersTo("SELECT * FROM foo")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestWhereRendering.java similarity index 95% rename from src/test/java/com/exasol/dql/rendering/TestWhereRendering.java rename to src/test/java/com/exasol/sql/dql/rendering/TestWhereRendering.java index 5647e795..ff256237 100644 --- a/src/test/java/com/exasol/dql/rendering/TestWhereRendering.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestWhereRendering.java @@ -1,4 +1,4 @@ -package com.exasol.dql.rendering; +package com.exasol.sql.dql.rendering; import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; import static com.exasol.sql.expression.BooleanTerm.eq; From 2d3818c22a3932e810653ccd02c6433f6f770955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 9 Oct 2018 09:29:20 +0200 Subject: [PATCH 26/43] PMI-86: Moved model to more Maven-compliant location. --- .../uml}/diagrams/class/cl_fragments.plantuml | 0 .../cl_select_fragment_hierarchy.plantuml | 7 ++-- .../uml}/diagrams/class/cl_visitor.plantuml | 0 {model => src/uml}/diagrams/exasol.skin | 32 ++++++++++--------- 4 files changed, 21 insertions(+), 18 deletions(-) rename {model => src/uml}/diagrams/class/cl_fragments.plantuml (100%) rename {model => src/uml}/diagrams/class/cl_select_fragment_hierarchy.plantuml (63%) rename {model => src/uml}/diagrams/class/cl_visitor.plantuml (100%) rename {model => src/uml}/diagrams/exasol.skin (50%) diff --git a/model/diagrams/class/cl_fragments.plantuml b/src/uml/diagrams/class/cl_fragments.plantuml similarity index 100% rename from model/diagrams/class/cl_fragments.plantuml rename to src/uml/diagrams/class/cl_fragments.plantuml diff --git a/model/diagrams/class/cl_select_fragment_hierarchy.plantuml b/src/uml/diagrams/class/cl_select_fragment_hierarchy.plantuml similarity index 63% rename from model/diagrams/class/cl_select_fragment_hierarchy.plantuml rename to src/uml/diagrams/class/cl_select_fragment_hierarchy.plantuml index 6b924d3a..95b6777f 100644 --- a/model/diagrams/class/cl_select_fragment_hierarchy.plantuml +++ b/src/uml/diagrams/class/cl_select_fragment_hierarchy.plantuml @@ -1,11 +1,12 @@ @startuml -'!include ../exasol.skin +!include ../exasol.skin Select *-- "*" Field -Select *-- "0..1" From +Select *-- "0..1" FromClause Select *-- "0..1" LimitClause Select *-- "0..1" WhereClause -From *-- "*" Table +FromClause *-- "*" Table +FromClause *-- "*" Join WhereClause *-- BooleanExpression BooleanExpression *-- "0..1" BooleanExpression @enduml \ No newline at end of file diff --git a/model/diagrams/class/cl_visitor.plantuml b/src/uml/diagrams/class/cl_visitor.plantuml similarity index 100% rename from model/diagrams/class/cl_visitor.plantuml rename to src/uml/diagrams/class/cl_visitor.plantuml diff --git a/model/diagrams/exasol.skin b/src/uml/diagrams/exasol.skin similarity index 50% rename from model/diagrams/exasol.skin rename to src/uml/diagrams/exasol.skin index f3d4df42..7c79025c 100644 --- a/model/diagrams/exasol.skin +++ b/src/uml/diagrams/exasol.skin @@ -1,3 +1,4 @@ +@startuml hide empty methods hide empty attributes skinparam style strictuml @@ -5,34 +6,35 @@ skinparam style strictuml '!pragma horizontalLineBetweenDifferentPackageAllowed skinparam Arrow { - Color 093e52 - FontColor 093e52 + Color 093e52 + FontColor 093e52 } skinparam Class { BackgroundColor fffff - FontColor 093e52 - FontStyle bold - BorderColor 093e52 + FontColor 093e52 + FontStyle bold + BorderColor 093e52 BackgroundColor<> 00b09b - FontColor<> ffffff - StereotypeFontColor<> ffffff + FontColor<> ffffff + StereotypeFontColor<> ffffff } skinparam ClassAttribute { BackgroundColor fffff - FontColor 093e52 - BorderColor 093e52 + FontColor 093e52 + BorderColor 093e52 BackgroundColor<> 00b09b - FontColor<> ffffff - StereotypeFontColor<> ffffff + FontColor<> ffffff + StereotypeFontColor<> ffffff } skinparam Package { BackgroundColor fffff - FontColor 093e52 - FontStyle bold - BorderColor 093e52 + FontColor 093e52 + FontStyle bold + BorderColor 093e52 } -skinparam padding 5 \ No newline at end of file +skinparam padding 5 +@enduml \ No newline at end of file From 334361ba26ee9f04547ae5008e6d061bee843823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 9 Oct 2018 11:03:38 +0200 Subject: [PATCH 27/43] PMI-95: Support basic inserting with values. --- src/main/java/com/exasol/sql/dml/Insert.java | 30 ++++++++-- .../java/com/exasol/sql/dml/InsertValues.java | 52 ++++++++++++++++ .../com/exasol/sql/dml/InsertVisitor.java | 4 ++ .../sql/dml/rendering/InsertRenderer.java | 17 ++++++ .../com/exasol/sql/dql/ValueExpression.java | 24 -------- .../sql/dql/rendering/SelectRenderer.java | 27 +-------- .../expression/AbstractValueExpression.java | 15 +++++ .../java/com/exasol/sql/expression/Value.java | 31 ++++++++++ .../sql/expression/ValueExpression.java | 7 +++ .../expression/ValueExpressionVisitor.java | 8 +++ .../rendering/AbstractExpressionRenderer.java | 60 +++++++++++++++++++ .../rendering/BooleanExpressionRenderer.java | 44 +------------- .../rendering/ValueExpressionRenderer.java | 26 ++++++++ .../rendering/AbstractFragmentRenderer.java | 18 ++++-- .../dml/rendering/TestInsertRendering.java | 15 +++++ 15 files changed, 277 insertions(+), 101 deletions(-) create mode 100644 src/main/java/com/exasol/sql/dml/InsertValues.java delete mode 100644 src/main/java/com/exasol/sql/dql/ValueExpression.java create mode 100644 src/main/java/com/exasol/sql/expression/AbstractValueExpression.java create mode 100644 src/main/java/com/exasol/sql/expression/Value.java create mode 100644 src/main/java/com/exasol/sql/expression/ValueExpression.java create mode 100644 src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java create mode 100644 src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java create mode 100644 src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java diff --git a/src/main/java/com/exasol/sql/dml/Insert.java b/src/main/java/com/exasol/sql/dml/Insert.java index 2c83517e..8a038d9d 100644 --- a/src/main/java/com/exasol/sql/dml/Insert.java +++ b/src/main/java/com/exasol/sql/dml/Insert.java @@ -8,7 +8,8 @@ */ public class Insert extends AbstractFragment implements SqlStatement, InsertFragment { private final Table table; - private InsertFields fields; + private InsertFields insertFields; + private InsertValues insertValues; /** * Create a new instance of an {@link Insert} statement @@ -27,10 +28,10 @@ public Insert(final String tableName) { * @return this for fluent programming */ public synchronized Insert field(final String... names) { - if (this.fields == null) { - this.fields = new InsertFields(this); + if (this.insertFields == null) { + this.insertFields = new InsertFields(this); } - this.fields.add(names); + this.insertFields.add(names); return this; } @@ -49,8 +50,25 @@ public void accept(final InsertVisitor visitor) { if (this.table != null) { this.table.accept(visitor); } - if (this.fields != null) { - this.fields.accept(visitor); + if (this.insertFields != null) { + this.insertFields.accept(visitor); } + if (this.insertValues != null) { + this.insertValues.accept(visitor); + } + } + + /** + * Insert a list of concrete values + * + * @param values values to be inserted + * @return this for fluent programming + */ + public synchronized Insert values(final Object... values) { + if (this.insertValues == null) { + this.insertValues = new InsertValues(this); + } + this.insertValues.add(values); + return this; } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertValues.java b/src/main/java/com/exasol/sql/dml/InsertValues.java new file mode 100644 index 00000000..6a539163 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertValues.java @@ -0,0 +1,52 @@ +package com.exasol.sql.dml; + +import java.util.ArrayList; +import java.util.List; + +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.Fragment; +import com.exasol.sql.expression.Value; +import com.exasol.sql.expression.ValueExpression; + +/** + * Container class for values to be inserted by an INSERT statement. + */ +public class InsertValues extends AbstractFragment implements InsertFragment { + private final List values = new ArrayList<>(); + + /** + * Create a new instance of {@link InsertValues + * + * @param root root SQL statement + */ + public InsertValues(final Fragment root) { + super(root); + } + + /** + * Add one or more values + * + * @param values values + */ + public void add(final Object... values) { + for (final Object value : values) { + this.getValues().add(new Value(value)); + } + } + + /** + * Get the values + * + * @return value + */ + public List getValues() { + return this.values; + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + // sub-expression left out intentionally + visitor.leave(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertVisitor.java b/src/main/java/com/exasol/sql/dml/InsertVisitor.java index 71731729..d4ce8970 100644 --- a/src/main/java/com/exasol/sql/dml/InsertVisitor.java +++ b/src/main/java/com/exasol/sql/dml/InsertVisitor.java @@ -8,4 +8,8 @@ public interface InsertVisitor extends FragmentVisitor { public void visit(InsertFields insertFields); public void leave(InsertFields insertFields); + + public void visit(InsertValues insertValues); + + public void leave(InsertValues insertValues); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java index 02655c53..d498f292 100644 --- a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -3,6 +3,7 @@ import com.exasol.sql.Field; import com.exasol.sql.Table; import com.exasol.sql.dml.*; +import com.exasol.sql.expression.ValueExpression; import com.exasol.sql.rendering.AbstractFragmentRenderer; import com.exasol.sql.rendering.StringRendererConfig; @@ -38,10 +39,26 @@ public void visit(final Field field) { @Override public void visit(final InsertFields insertFields) { append(" ("); + setLastVisited(insertFields); } @Override public void leave(final InsertFields insertFields) { append(")"); } + + @Override + public void visit(final InsertValues insertValues) { + appendKeyWord(" VALUES "); + for (final ValueExpression expression : insertValues.getValues()) { + appendCommaWhenNeeded(insertValues); + appendRenderedValueExpression(expression); + setLastVisited(insertValues); + } + } + + @Override + public void leave(final InsertValues insertValues) { + // intentionally empty + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/ValueExpression.java b/src/main/java/com/exasol/sql/dql/ValueExpression.java deleted file mode 100644 index 2a6d59db..00000000 --- a/src/main/java/com/exasol/sql/dql/ValueExpression.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.exasol.sql.dql; - -import com.exasol.sql.*; - -/** - * Abstract base class for all types of value expressions - */ -public abstract class ValueExpression extends AbstractFragment implements Fragment { - /** - * Create a new instance of a {@link ValueExpression} - */ - public ValueExpression() { - super(null); - } - - /** - * Create a new instance of a {@link ValueExpression} - * - * @param root root SQL statement this expression belongs to - */ - public ValueExpression(final SqlStatement root) { - super(root); - } -} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java index f7bf7601..55dc760d 100644 --- a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java +++ b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java @@ -2,7 +2,8 @@ import java.util.Optional; -import com.exasol.sql.*; +import com.exasol.sql.Field; +import com.exasol.sql.Table; import com.exasol.sql.dql.*; import com.exasol.sql.rendering.AbstractFragmentRenderer; import com.exasol.sql.rendering.StringRendererConfig; @@ -82,28 +83,4 @@ public void visit(final LimitClause limit) { append(limit.getCount()); setLastVisited(limit); } - - /** - * Create a renderer for the given {@link Fragment} and render it. - * - * @param fragment SQL statement fragment to be rendered - * @return rendered statement - */ - public static String render(final Fragment fragment) { - return render(fragment, StringRendererConfig.createDefault()); - } - - /** - * Create a renderer for the given {@link Fragment} and render it. - * - * @param fragment SQL statement fragment to be rendered - * @param config renderer configuration - * @return rendered statement - */ - public static String render(final Fragment fragment, final StringRendererConfig config) { - assert (fragment instanceof SelectFragment); - final SelectRenderer renderer = new SelectRenderer(config); - ((SelectFragment) fragment).accept(renderer); - return renderer.render(); - } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/AbstractValueExpression.java b/src/main/java/com/exasol/sql/expression/AbstractValueExpression.java new file mode 100644 index 00000000..fcf1f26f --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/AbstractValueExpression.java @@ -0,0 +1,15 @@ +package com.exasol.sql.expression; + +import com.exasol.util.AbstractBottomUpTreeNode; + +/** + * Abstract base class for all types of value expressions + */ +public abstract class AbstractValueExpression extends AbstractBottomUpTreeNode implements ValueExpression { + /** + * Create a new instance of a {@link AbstractValueExpression} + */ + public AbstractValueExpression() { + super(); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Value.java b/src/main/java/com/exasol/sql/expression/Value.java new file mode 100644 index 00000000..c98e39f1 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Value.java @@ -0,0 +1,31 @@ +package com.exasol.sql.expression; + +/** + * This class represents a concrete value link a number or a text. + */ +public class Value extends AbstractValueExpression { + private final Object value; + + /** + * Create a new instance of a {@link Value} + * + * @param value contained value + */ + public Value(final Object value) { + this.value = value; + } + + /** + * Get the value + * + * @return value + */ + public Object get() { + return this.value; + } + + @Override + public void accept(final ValueExpressionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/ValueExpression.java b/src/main/java/com/exasol/sql/expression/ValueExpression.java new file mode 100644 index 00000000..e705c907 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/ValueExpression.java @@ -0,0 +1,7 @@ +package com.exasol.sql.expression; + +import com.exasol.util.TreeNode; + +public interface ValueExpression extends TreeNode { + void accept(ValueExpressionVisitor visitor); +} diff --git a/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java new file mode 100644 index 00000000..5009eeda --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java @@ -0,0 +1,8 @@ +package com.exasol.sql.expression; + +/** + * Visitor interface for a {@link BooleanTerm} + */ +public interface ValueExpressionVisitor { + void visit(Value value); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java new file mode 100644 index 00000000..ab36958f --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java @@ -0,0 +1,60 @@ +package com.exasol.sql.expression.rendering; + +import java.util.Stack; + +import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * Common base class for expression renderers + */ +public class AbstractExpressionRenderer { + protected final StringRendererConfig config; + protected final StringBuilder builder = new StringBuilder(); + protected final Stack connectorStack = new Stack<>(); + + public AbstractExpressionRenderer(final StringRendererConfig config) { + this.config = config; + } + + protected void appendKeyword(final String keyword) { + this.builder.append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); + } + + protected void connect(final BooleanExpression expression) { + if (expression.isChild() && !expression.isFirstSibling()) { + appendConnector(); + } + } + + private void appendConnector() { + if (!this.connectorStack.isEmpty()) { + appendKeyword(this.connectorStack.peek()); + } + } + + protected void appendLiteral(final String string) { + this.builder.append(string); + } + + protected void startParenthesis() { + this.builder.append("("); + } + + protected void endParenthesis(final BooleanExpression expression) { + this.builder.append(")"); + } + + /** + * Render expression to a string + * + * @return rendered string + */ + public String render() { + return this.builder.toString(); + } + + protected void append(final String string) { + this.builder.append(string); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java index 14657429..1588a707 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -1,25 +1,15 @@ package com.exasol.sql.expression.rendering; -import java.util.Stack; - import com.exasol.sql.expression.*; import com.exasol.sql.rendering.StringRendererConfig; -public class BooleanExpressionRenderer implements BooleanExpressionVisitor { - private final StringRendererConfig config; - private final StringBuilder builder = new StringBuilder(); - private final Stack connectorStack = new Stack<>(); - +public class BooleanExpressionRenderer extends AbstractExpressionRenderer implements BooleanExpressionVisitor { public BooleanExpressionRenderer(final StringRendererConfig config) { - this.config = config; + super(config); } public BooleanExpressionRenderer() { - this.config = new StringRendererConfig.Builder().build(); - } - - private void appendKeyword(final String keyword) { - this.builder.append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); + this(new StringRendererConfig.Builder().build()); } @Override @@ -74,39 +64,11 @@ public void visit(final Literal literal) { appendLiteral(literal.toString()); } - private void connect(final BooleanExpression expression) { - if (expression.isChild() && !expression.isFirstSibling()) { - appendConnector(); - } - } - @Override public void leave(final Literal literal) { // intentionally empty } - private void appendConnector() { - if (!this.connectorStack.isEmpty()) { - appendKeyword(this.connectorStack.peek()); - } - } - - private void appendLiteral(final String string) { - this.builder.append(string); - } - - private void startParenthesis() { - this.builder.append("("); - } - - private void endParenthesis(final BooleanExpression expression) { - this.builder.append(")"); - } - - public String render() { - return this.builder.toString(); - } - @Override public void visit(final Comparison comparison) { connect(comparison); diff --git a/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java new file mode 100644 index 00000000..d6511737 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java @@ -0,0 +1,26 @@ +package com.exasol.sql.expression.rendering; + +import com.exasol.sql.expression.Value; +import com.exasol.sql.expression.ValueExpressionVisitor; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * Renderer for common value expressions + */ +public class ValueExpressionRenderer extends AbstractExpressionRenderer implements ValueExpressionVisitor { + public ValueExpressionRenderer(final StringRendererConfig config) { + super(config); + } + + @Override + public void visit(final Value value) { + final Object object = value.get(); + if (object instanceof String) { + append("'"); + append((String) object); + append("'"); + } else { + this.builder.append(value.get().toString()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java index 4b7f4afe..415e770d 100644 --- a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java @@ -2,7 +2,9 @@ import com.exasol.sql.Fragment; import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.expression.ValueExpression; import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; +import com.exasol.sql.expression.rendering.ValueExpressionRenderer; /** * Abstract base class for SQL fragment renderers @@ -17,11 +19,6 @@ public AbstractFragmentRenderer(final StringRendererConfig config) { this.lastVisited = null; } - @Override - public String render() { - return this.builder.toString(); - } - protected void appendKeyWord(final String keyword) { append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); } @@ -53,4 +50,15 @@ protected void appendRenderedExpression(final BooleanExpression expression) { protected void append(final int number) { this.builder.append(number); } + + protected void appendRenderedValueExpression(final ValueExpression expression) { + final ValueExpressionRenderer renderer = new ValueExpressionRenderer(this.config); + expression.accept(renderer); + append(renderer.render()); + } + + @Override + public String render() { + return this.builder.toString(); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java index b5492f01..53f7ba65 100644 --- a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java @@ -1,6 +1,7 @@ package com.exasol.sql.dml.rendering; import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersWithConfigTo; import static org.hamcrest.MatcherAssert.assertThat; import org.junit.jupiter.api.BeforeEach; @@ -8,6 +9,7 @@ import com.exasol.sql.StatementFactory; import com.exasol.sql.dml.Insert; +import com.exasol.sql.rendering.StringRendererConfig; class TestInsertRendering { private static final String PERSON = "person"; @@ -24,9 +26,22 @@ void testInsert() { assertThat(this.insert, rendersTo("INSERT INTO person")); } + // [dsn~rendering.sql.insert~1] + @Test + void testInsertRendersToWithConfig() { + assertThat(this.insert, + rendersWithConfigTo(new StringRendererConfig.Builder().lowerCase(true).build(), "insert into person")); + } + // [dsn~rendering.sql.insert~1] @Test void testInsertFields() { assertThat(this.insert.field("a", "b"), rendersTo("INSERT INTO person (a, b)")); } + + // [dsn~rendering.sql.insert~1] + @Test + void testInsertValues() { + assertThat(this.insert.values(1, "a"), rendersTo("INSERT INTO person VALUES 1, 'a'")); + } } \ No newline at end of file From 5b4eedeb16dbe74b1e3510635b198914b98f1d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 9 Oct 2018 12:35:56 +0200 Subject: [PATCH 28/43] PMI-95: Added factory methods for renderers. --- .../sql/dml/rendering/InsertRenderer.java | 19 +++++++++++++ .../sql/dql/rendering/SelectRenderer.java | 19 +++++++++++++ .../sql/dml/rendering/TestInsertRenderer.java | 27 +++++++++++++++++++ .../sql/dql/rendering/TestSelectRenderer.java | 27 +++++++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java create mode 100644 src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java diff --git a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java index d498f292..6a8c8db9 100644 --- a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -61,4 +61,23 @@ public void visit(final InsertValues insertValues) { public void leave(final InsertValues insertValues) { // intentionally empty } + + /** + * Create an {@link InsertRenderer} using the default renderer configuration + * + * @return insert renderer + */ + public static InsertRenderer create() { + return create(StringRendererConfig.createDefault()); + } + + /** + * Create an {@link InsertRenderer} + * + * @param config renderer configuration + * @return insert renderer + */ + public static InsertRenderer create(final StringRendererConfig config) { + return new InsertRenderer(config); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java index 55dc760d..cf83d9fa 100644 --- a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java +++ b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java @@ -83,4 +83,23 @@ public void visit(final LimitClause limit) { append(limit.getCount()); setLastVisited(limit); } + + /** + * Create an {@link SelectRenderer} using the default renderer configuration + * + * @return select renderer + */ + public static SelectRenderer create() { + return create(StringRendererConfig.createDefault()); + } + + /** + * Create an {@link SelectRenderer} + * + * @param config renderer configuration + * @return select renderer + */ + public static SelectRenderer create(final StringRendererConfig config) { + return new SelectRenderer(config); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java new file mode 100644 index 00000000..ec691730 --- /dev/null +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java @@ -0,0 +1,27 @@ +package com.exasol.sql.dml.rendering; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; +import com.exasol.sql.dml.Insert; +import com.exasol.sql.rendering.StringRendererConfig; + +class TestInsertRenderer { + @Test + void testCreateWithDefaultConfig() { + assertThat(InsertRenderer.create(), instanceOf(InsertRenderer.class)); + } + + @Test + void testCreateWithConfig() { + final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + final InsertRenderer renderer = InsertRenderer.create(config); + final Insert insert = StatementFactory.getInstance().insertInto("city"); + insert.accept(renderer); + assertThat(renderer.render(), startsWith("insert")); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java new file mode 100644 index 00000000..5954bac6 --- /dev/null +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java @@ -0,0 +1,27 @@ +package com.exasol.sql.dql.rendering; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.startsWith; + +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; +import com.exasol.sql.dql.Select; +import com.exasol.sql.rendering.StringRendererConfig; + +class TestSelectRenderer { + @Test + void testCreateWithDefaultConfig() { + assertThat(SelectRenderer.create(), instanceOf(SelectRenderer.class)); + } + + @Test + void testCreateWithConfig() { + final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + final SelectRenderer renderer = SelectRenderer.create(config); + final Select select = StatementFactory.getInstance().select(); + select.accept(renderer); + assertThat(renderer.render(), startsWith("select")); + } +} \ No newline at end of file From d05eccf376655f3d6e1ea1a8bc64d9e4b78ebaf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 10 Oct 2018 09:36:19 +0200 Subject: [PATCH 29/43] PMI-95: Fixed specification trace and added missing unit tests. --- doc/design.md | 34 ++++++++--- doc/system_requirements.md | 20 +++++- .../com/exasol/sql/UnnamedPlaceholder.java | 10 +++ src/main/java/com/exasol/sql/dml/Insert.java | 61 ++++++++++++++----- .../java/com/exasol/sql/dml/InsertValues.java | 11 ++-- .../sql/dml/rendering/InsertRenderer.java | 4 ++ src/main/java/com/exasol/sql/dql/Select.java | 4 ++ .../sql/dql/rendering/SelectRenderer.java | 1 + .../exasol/sql/expression/BooleanTerm.java | 3 +- .../com/exasol/sql/expression/Comparison.java | 7 ++- .../expression/ValueExpressionVisitor.java | 4 ++ .../rendering/ValueExpressionRenderer.java | 6 ++ .../rendering/AbstractFragmentRenderer.java | 1 + .../dml/rendering/TestInsertRendering.java | 31 ++++++++-- .../java/com/exasol/sql/dql/TestSelect.java | 1 - .../dql/rendering/TestSelectRendering.java | 31 +++++++--- .../sql/expression/TestBooleanTerm.java | 4 +- .../expression/TestComparisonOperator.java | 2 + 18 files changed, 189 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/exasol/sql/UnnamedPlaceholder.java diff --git a/doc/design.md b/doc/design.md index bf237a2a..4068a95b 100644 --- a/doc/design.md +++ b/doc/design.md @@ -7,17 +7,35 @@ The Data Query Language (DQL) building block is responsible for managing `SELECT` statements. +## Solution Strategy + +### Fluent Programming + +###### Statement Construction With Fluent Programming +`dsn~statement-construction-with-fluent-programming~1` + +All statement builders use the "fluent programming" model, where the return type of each builder step determines the possible next structural elements that can be added. + +Comment: + +This is a design principle that cuts across the whole project. Therefore locating it in a single test or implementation part makes no sense. + +Covers: + +* `req~statement-structure-limited-at-compile-time~1` + ## Runtime View ### Building Select Statements #### Accessing the Clauses That Make Up a SELECT Statement -`dsn~select-statement.accessing-clauses~1` +`dsn~select-statement.out-of-order-clauses~1` -The DQL statement component allows getting the following clauses, provided that they already exist: +`SELECT` commands allow attaching the following clauses in any order: * `FROM` clause * `WHERE` clause +* `LIMIT` clause Covers: @@ -29,6 +47,11 @@ Tags: Select Statement Builder ### Building Boolean Expressions +#### Forwarded Requirements + +* `dsn --> impl, utest: req~boolean-operators~1` +* `dsn --> impl, utest: req~comparison-operations~1` + #### Constructing Boolean Comparison Operations From Operator Strings `dsn~boolean-operation.comparison.constructing-from-strings~1` @@ -57,11 +80,6 @@ Covers: Needs: impl, utest -#### Forwarded Requirements - -* `dsn --> impl, utest : req~comparison-operations~1` -* `dsn --> impl, utest : req~boolean-operators~1` - ### Building INSERT Statements #### Forwarded Requirements @@ -73,6 +91,6 @@ Needs: impl, utest #### Forwarded Requirements -* `dsn --> req~rendering.sql.configurable-case~1` +* `dsn --> impl, utest: req~rendering.sql.configurable-case~1` * `dsn --> impl, utest: req~rendering.sql.select~1` * `dsn --> impl, utest: req~rendering.sql.insert~1` diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 9e7a0562..b8d9ef20 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -99,7 +99,22 @@ This is necessary since complex statements are usually build as a result of mult Covers: -* [feat~statment-definition~1](#statement-definition) +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +#### Statement Structure Limited at Compile-time +`req~statement-structure-limited-at-compile-time~1` + +ESB lets users create only valid statement structures at compile-time. + +Rationale: + +If users can't get illegal structures to compile, they don't need to spend time debugging them later. + +Covers: + +* [feat~compile-time-error-checking~1](#compile-time-error-checking) Needs: dsn @@ -187,7 +202,8 @@ Covers: Needs: dsn -* Upper case / lower case +#### TODO + * One line / pretty #### SELECT Statement Rendering diff --git a/src/main/java/com/exasol/sql/UnnamedPlaceholder.java b/src/main/java/com/exasol/sql/UnnamedPlaceholder.java new file mode 100644 index 00000000..415429d9 --- /dev/null +++ b/src/main/java/com/exasol/sql/UnnamedPlaceholder.java @@ -0,0 +1,10 @@ +package com.exasol.sql; + +import com.exasol.sql.expression.*; + +public class UnnamedPlaceholder extends AbstractValueExpression implements ValueExpression { + @Override + public void accept(final ValueExpressionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/Insert.java b/src/main/java/com/exasol/sql/dml/Insert.java index 8a038d9d..49100441 100644 --- a/src/main/java/com/exasol/sql/dml/Insert.java +++ b/src/main/java/com/exasol/sql/dml/Insert.java @@ -6,6 +6,7 @@ /** * This class implements an SQL {@link Select} statement */ +// [impl->dsn~insert-statements~1] public class Insert extends AbstractFragment implements SqlStatement, InsertFragment { private final Table table; private InsertFields insertFields; @@ -44,26 +45,13 @@ public String getTableName() { return this.table.getName(); } - @Override - public void accept(final InsertVisitor visitor) { - visitor.visit(this); - if (this.table != null) { - this.table.accept(visitor); - } - if (this.insertFields != null) { - this.insertFields.accept(visitor); - } - if (this.insertValues != null) { - this.insertValues.accept(visitor); - } - } - /** * Insert a list of concrete values * * @param values values to be inserted * @return this for fluent programming */ + // [impl->dsn~values-as-insert-source~1] public synchronized Insert values(final Object... values) { if (this.insertValues == null) { this.insertValues = new InsertValues(this); @@ -71,4 +59,49 @@ public synchronized Insert values(final Object... values) { this.insertValues.add(values); return this; } + + /** + * Add an unnamed value placeholder to the value list (this is useful for prepared statements) + * + * @return this for fluent programming + */ + // [impl->dsn~values-as-insert-source~1] + public synchronized Insert valuePlaceholder() { + if (this.insertValues == null) { + this.insertValues = new InsertValues(this); + } + this.insertValues.addPlaceholder(); + return this; + } + + /** + * Add a given number unnamed value placeholder to the value list (this is useful for prepared statements) + * + * @param amount number of placeholders to be added + * @return this for fluent programming + */ + // [impl->dsn~values-as-insert-source~1] + public synchronized Insert valuePlaceholders(final int amount) { + if (this.insertValues == null) { + this.insertValues = new InsertValues(this); + } + for (int i = 0; i < amount; ++i) { + this.insertValues.addPlaceholder(); + } + return this; + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + if (this.table != null) { + this.table.accept(visitor); + } + if (this.insertFields != null) { + this.insertFields.accept(visitor); + } + if (this.insertValues != null) { + this.insertValues.accept(visitor); + } + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertValues.java b/src/main/java/com/exasol/sql/dml/InsertValues.java index 6a539163..aa5e4456 100644 --- a/src/main/java/com/exasol/sql/dml/InsertValues.java +++ b/src/main/java/com/exasol/sql/dml/InsertValues.java @@ -3,8 +3,7 @@ import java.util.ArrayList; import java.util.List; -import com.exasol.sql.AbstractFragment; -import com.exasol.sql.Fragment; +import com.exasol.sql.*; import com.exasol.sql.expression.Value; import com.exasol.sql.expression.ValueExpression; @@ -25,7 +24,7 @@ public InsertValues(final Fragment root) { /** * Add one or more values - * + * * @param values values */ public void add(final Object... values) { @@ -36,7 +35,7 @@ public void add(final Object... values) { /** * Get the values - * + * * @return value */ public List getValues() { @@ -49,4 +48,8 @@ public void accept(final InsertVisitor visitor) { // sub-expression left out intentionally visitor.leave(this); } + + public void addPlaceholder() { + this.values.add(new UnnamedPlaceholder()); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java index 6a8c8db9..1d44aacb 100644 --- a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -7,6 +7,10 @@ import com.exasol.sql.rendering.AbstractFragmentRenderer; import com.exasol.sql.rendering.StringRendererConfig; +/** + * The {@link InsertRenderer} turns SQL statement structures in to SQL strings. + */ +// [impl->dsn~rendering.sql.insert~1] public class InsertRenderer extends AbstractFragmentRenderer implements InsertVisitor { /** * Create a new {@link InsertRenderer} with custom render settings. diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 3f54de0e..5cf6b9cb 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -50,6 +50,7 @@ public Select field(final String... names) { * * @return from clause */ + // [impl->dsn~select-statement.out-of-order-clauses~1] public synchronized FromClause from() { if (this.fromClause == null) { this.fromClause = new FromClause(this); @@ -64,6 +65,7 @@ public synchronized FromClause from() { * @return new instance * @throws IllegalStateException if a limit clause already exists */ + // [impl->dsn~select-statement.out-of-order-clauses~1] public synchronized Select limit(final int count) { if (this.limitClause != null) { throw new IllegalStateException( @@ -81,6 +83,7 @@ public synchronized Select limit(final int count) { * @return thisdsn~select-statement.out-of-order-clauses~1] public synchronized Select limit(final int offset, final int count) { if (this.limitClause != null) { throw new IllegalStateException( @@ -96,6 +99,7 @@ public synchronized Select limit(final int offset, final int count) { * @param expression boolean expression that defines the filter criteria * @return new instance */ + // [impl->dsn~select-statement.out-of-order-clauses~1] public synchronized Select where(final BooleanExpression expression) { if (this.whereClause == null) { this.whereClause = new WhereClause(this, expression); diff --git a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java index cf83d9fa..5073f7bb 100644 --- a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java +++ b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java @@ -11,6 +11,7 @@ /** * The {@link SelectRenderer} turns SQL statement structures in to SQL strings. */ +// [impl->dsn~rendering.sql.select~1] public class SelectRenderer extends AbstractFragmentRenderer implements SelectVisitor { /** * Create a new {@link SelectRenderer} with custom render settings. diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 0ae285e4..a66e81e3 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -1,5 +1,6 @@ package com.exasol.sql.expression; +// [impl->dsn~boolean-operators~1] public abstract class BooleanTerm extends AbstractBooleanExpression { private BooleanTerm() { super(); @@ -50,7 +51,7 @@ public static BooleanExpression compare(final String left, final String operator return new Comparison(ComparisonOperator.ofSymbol(operatorSymbol), Literal.of(left), Literal.of(right)); } - // [dsn~boolean-operation.comparison.constructing-from-enum~1] + // [impl->dsn~boolean-operation.comparison.constructing-from-enum~1] public static BooleanExpression compare(final String left, final ComparisonOperator operator, final String right) { return new Comparison(operator, Literal.of(left), Literal.of(right)); } diff --git a/src/main/java/com/exasol/sql/expression/Comparison.java b/src/main/java/com/exasol/sql/expression/Comparison.java index 0a6381af..0fdf2eb3 100644 --- a/src/main/java/com/exasol/sql/expression/Comparison.java +++ b/src/main/java/com/exasol/sql/expression/Comparison.java @@ -6,6 +6,7 @@ public class Comparison extends AbstractBooleanExpression { private final Literal leftOperand; private final Literal rightOperand; + // [impl->dsn~boolean-operation.comparison.constructing-from-enum~1] public Comparison(final ComparisonOperator equal, final Literal leftOperand, final Literal rightOperand) { this.operator = equal; this.leftOperand = leftOperand; @@ -24,7 +25,7 @@ public void dismissConcrete(final BooleanExpressionVisitor visitor) { /** * Get the left-hand side operator of the comparison - * + * * @return left operator */ public AbstractBooleanExpression getLeftOperand() { @@ -33,7 +34,7 @@ public AbstractBooleanExpression getLeftOperand() { /** * Get the right-hand side operator of the comparison - * + * * @return right operator */ public AbstractBooleanExpression getRightOperand() { @@ -42,7 +43,7 @@ public AbstractBooleanExpression getRightOperand() { /** * Get the comparison operator - * + * * @return comparison operator */ public ComparisonOperator getOperator() { diff --git a/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java index 5009eeda..f780eb51 100644 --- a/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java +++ b/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java @@ -1,8 +1,12 @@ package com.exasol.sql.expression; +import com.exasol.sql.UnnamedPlaceholder; + /** * Visitor interface for a {@link BooleanTerm} */ public interface ValueExpressionVisitor { void visit(Value value); + + void visit(UnnamedPlaceholder unnamedPlaceholder); } \ No newline at end of file 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 d6511737..53eac8d7 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java @@ -1,5 +1,6 @@ package com.exasol.sql.expression.rendering; +import com.exasol.sql.UnnamedPlaceholder; import com.exasol.sql.expression.Value; import com.exasol.sql.expression.ValueExpressionVisitor; import com.exasol.sql.rendering.StringRendererConfig; @@ -23,4 +24,9 @@ public void visit(final Value value) { this.builder.append(value.get().toString()); } } + + @Override + public void visit(final UnnamedPlaceholder unnamedPlaceholder) { + append("?"); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java index 415e770d..08abe2a0 100644 --- a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java @@ -19,6 +19,7 @@ public AbstractFragmentRenderer(final StringRendererConfig config) { this.lastVisited = null; } + // [impl->dsn~rendering.sql.configurable-case~1] protected void appendKeyWord(final String keyword) { append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); } diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java index 53f7ba65..40e96e6e 100644 --- a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java @@ -20,28 +20,51 @@ void beforeEach() { this.insert = StatementFactory.getInstance().insertInto(PERSON); } - // [dsn~rendering.sql.insert~1] + // [utest->dsn~rendering.sql.insert~1] @Test void testInsert() { assertThat(this.insert, rendersTo("INSERT INTO person")); } - // [dsn~rendering.sql.insert~1] + // [utest->dsn~rendering.sql.configurable-case~1] @Test void testInsertRendersToWithConfig() { assertThat(this.insert, rendersWithConfigTo(new StringRendererConfig.Builder().lowerCase(true).build(), "insert into person")); } - // [dsn~rendering.sql.insert~1] + // [utest->dsn~rendering.sql.insert~1] @Test void testInsertFields() { assertThat(this.insert.field("a", "b"), rendersTo("INSERT INTO person (a, b)")); } - // [dsn~rendering.sql.insert~1] + // [utest->dsn~rendering.sql.insert~1] + // [utest->dsn~values-as-insert-source~1] @Test void testInsertValues() { assertThat(this.insert.values(1, "a"), rendersTo("INSERT INTO person VALUES 1, 'a'")); } + + // [utest->dsn~rendering.sql.insert~1] + // [utest->dsn~values-as-insert-source~1] + @Test + void testInsertValuePlaceholder() { + assertThat(this.insert.valuePlaceholder(), rendersTo("INSERT INTO person VALUES ?")); + } + + // [utest->dsn~rendering.sql.insert~1] + // [utest->dsn~values-as-insert-source~1] + @Test + void testInsertValuePlaceholders() { + assertThat(this.insert.valuePlaceholders(3), rendersTo("INSERT INTO person VALUES ?, ?, ?")); + } + + // [utest->dsn~rendering.sql.insert~1] + // [utest->dsn~values-as-insert-source~1] + @Test + void testInsertMixedValuesAndPlaceholders() { + assertThat(this.insert.values(1).valuePlaceholders(3).values("b", 4), + rendersTo("INSERT INTO person VALUES 1, ?, ?, ?, 'b', 4")); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dql/TestSelect.java b/src/test/java/com/exasol/sql/dql/TestSelect.java index 3ec9176d..c1917502 100644 --- a/src/test/java/com/exasol/sql/dql/TestSelect.java +++ b/src/test/java/com/exasol/sql/dql/TestSelect.java @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Test; import com.exasol.sql.StatementFactory; -import com.exasol.sql.dql.Select; class TestSelect { private Select select; diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java index e640566e..f9b6420b 100644 --- a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java @@ -1,6 +1,7 @@ package com.exasol.sql.dql.rendering; import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersWithConfigTo; import static org.hamcrest.MatcherAssert.assertThat; import org.junit.jupiter.api.BeforeEach; @@ -8,6 +9,8 @@ import com.exasol.sql.StatementFactory; import com.exasol.sql.dql.Select; +import com.exasol.sql.expression.BooleanTerm; +import com.exasol.sql.rendering.StringRendererConfig; class TestSelectRendering { private Select select; @@ -17,46 +20,60 @@ void beforeEach() { this.select = StatementFactory.getInstance().select(); } - // [dsn~rendering.sql.select~1] + // [utest->dsn~rendering.sql.select~1] @Test void testSelectAll() { assertThat(this.select.all(), rendersTo("SELECT *")); } - // [dsn~rendering.sql.select~1] + // [utest->dsn~rendering.sql.configurable-case~1] + @Test + void testSelectAllLowerCase() { + assertThat(this.select.all(), + rendersWithConfigTo(new StringRendererConfig.Builder().lowerCase(true).build(), "select *")); + } + + // [utest->dsn~rendering.sql.select~1] @Test void testSelectFieldNames() { assertThat(this.select.field("a", "b"), rendersTo("SELECT a, b")); } - // [dsn~rendering.sql.select~1] + // [utest->dsn~rendering.sql.select~1] @Test void testSelectChainOfFieldNames() { assertThat(this.select.field("a", "b").field("c"), rendersTo("SELECT a, b, c")); } - // [dsn~rendering.sql.select~1] + // [utest->dsn~rendering.sql.select~1] @Test void testSelectFromTable() { assertThat(this.select.all().from().table("persons"), rendersTo("SELECT * FROM persons")); } - // [dsn~rendering.sql.select~1] + // [utest->dsn~rendering.sql.select~1] @Test void testSelectFromMultipleTable() { assertThat(this.select.all().from().table("table1").table("table2"), rendersTo("SELECT * FROM table1, table2")); } - // [dsn~rendering.sql.select~1] + // [utest->dsn~rendering.sql.select~1] @Test void testSelectFromTableAs() { assertThat(this.select.all().from().tableAs("table", "t"), rendersTo("SELECT * FROM table AS t")); } - // [dsn~rendering.sql.select~1] + // [utest->dsn~rendering.sql.select~1] @Test void testSelectFromMultipleTableAs() { assertThat(this.select.all().from().tableAs("table1", "t1").tableAs("table2", "t2"), rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); } + + // [utest->dsn~select-statement.out-of-order-clauses~1] + @Test + void testAddClausesInRandomOrder() { + assertThat(this.select.limit(1).all().where(BooleanTerm.not("foo")).from().join("A", "A.aa = B.bb").table("B"), + rendersTo("SELECT * FROM B JOIN A ON A.aa = B.bb WHERE NOT(foo) LIMIT 1")); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java index 9c7e77d3..2813da2f 100644 --- a/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java +++ b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java @@ -76,13 +76,13 @@ void testOperationFromNullOperatorThrowsException() { assertThrows(NullPointerException.class, () -> BooleanTerm.operation(null, not("a"), not("b"))); } - // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] + // [utest->dsn~boolean-operation.comparison.constructing-from-strings~1] @Test void testOperationFromComparisonOperatorString() { assertThat(BooleanTerm.compare("a", "<>", "b"), instanceOf(Comparison.class)); } - // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] + // [utest->dsn~boolean-operation.comparison.constructing-from-enum~1] @Test void testOperationFromComparisonOperatorEnum() { assertThat(BooleanTerm.compare("a", ComparisonOperator.NOT_EQUAL, "b"), instanceOf(Comparison.class)); diff --git a/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java index 819b28d3..e4c501af 100644 --- a/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java +++ b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java @@ -12,11 +12,13 @@ void testToString() { assertThat(ComparisonOperator.EQUAL.toString(), equalTo("=")); } + // [utest->dsn~boolean-operation.comparison.constructing-from-strings~1] @Test void testOfSymbol() { assertThat(ComparisonOperator.ofSymbol("<>"), equalTo(ComparisonOperator.NOT_EQUAL)); } + // [utest->dsn~boolean-operation.comparison.constructing-from-strings~1] @Test void testOfUnknownSymbolThrowsException() { assertThrows(IllegalArgumentException.class, () -> ComparisonOperator.ofSymbol("ยง")); From 86b618f078977d29cc534b96cf7fb51af73dd00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 10 Oct 2018 10:41:31 +0200 Subject: [PATCH 30/43] PMI-95: Added auto-quoting for identifiers. --- doc/design.md | 19 ++++++ doc/system_requirements.md | 19 ++++++ .../sql/dml/rendering/InsertRenderer.java | 4 +- .../sql/dql/rendering/SelectRenderer.java | 6 +- .../rendering/AbstractExpressionRenderer.java | 2 +- .../rendering/BooleanExpressionRenderer.java | 2 +- .../rendering/AbstractFragmentRenderer.java | 36 ++++++++++- .../sql/rendering/StringRendererConfig.java | 59 +++++++++++++++---- .../sql/dml/rendering/TestInsertRenderer.java | 2 +- .../dml/rendering/TestInsertRendering.java | 2 +- .../sql/dql/rendering/TestSelectRenderer.java | 2 +- .../dql/rendering/TestSelectRendering.java | 10 +++- .../TestBooleanExpressionRenderer.java | 2 +- 13 files changed, 140 insertions(+), 25 deletions(-) diff --git a/doc/design.md b/doc/design.md index 4068a95b..03f001aa 100644 --- a/doc/design.md +++ b/doc/design.md @@ -94,3 +94,22 @@ Needs: impl, utest * `dsn --> impl, utest: req~rendering.sql.configurable-case~1` * `dsn --> impl, utest: req~rendering.sql.select~1` * `dsn --> impl, utest: req~rendering.sql.insert~1` + +#### Renderer add Double Quotes for Schema, Table and Column Identifiers +`dsn~rendering.add-double-quotes-for-schema-table-and-column-identifiers~1` + +The renderer adds double quotes the following identifiers if configured: + +* Schema identifiers +* Table identifiers +* Column identifiers (except the asterisks) + +Comment: + +Examples are `"my_schema"."my_table"."my_field"`, `"MY_TABLE"."MyField"` and `"MyTable".*` + +Covers: + +* `req~rendering.sql.confiugrable-identifier-quoting~1` + +Needs: impl, utest \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md index b8d9ef20..109c6efd 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -202,6 +202,25 @@ Covers: Needs: dsn +###### Configurable Identifier Quoting +`req~rendering.sql.confiugrable-identifier-quoting~1` + +ESB allows users to choose whether the following identifiers should be quoted in the rendered query: + +* Schema identifiers +* Table identifiers +* Column identifiers + +Rationale: + +The Exasol database for example requires identifiers to be enclosed in double quotes in order to enable case sensitivity. + +Covers: + +* [feat~sql-string-rendering~1](#sql-string-rendering) + +Needs: dsn + #### TODO * One line / pretty diff --git a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java index 1d44aacb..f3e89425 100644 --- a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -29,14 +29,14 @@ public void visit(final Insert insert) { @Override public void visit(final Table table) { - append(table.getName()); + appendAutoQuoted(table.getName()); setLastVisited(table); } @Override public void visit(final Field field) { appendCommaWhenNeeded(field); - append(field.getName()); + appendAutoQuoted(field.getName()); setLastVisited(field); } diff --git a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java index 5073f7bb..93e2dead 100644 --- a/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java +++ b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java @@ -31,7 +31,7 @@ public void visit(final Select select) { @Override public void visit(final Field field) { appendCommaWhenNeeded(field); - append(field.getName()); + appendAutoQuoted(field.getName()); setLastVisited(field); } @@ -44,7 +44,7 @@ public void visit(final FromClause fromClause) { @Override public void visit(final Table table) { appendCommaWhenNeeded(table); - append(table.getName()); + appendAutoQuoted(table.getName()); final Optional as = table.getAs(); if (as.isPresent()) { appendKeyWord(" AS "); @@ -61,7 +61,7 @@ public void visit(final Join join) { appendKeyWord(type.toString()); } appendKeyWord(" JOIN "); - append(join.getName()); + appendAutoQuoted(join.getName()); appendKeyWord(" ON "); append(join.getSpecification()); setLastVisited(join); diff --git a/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java index ab36958f..47af626e 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java @@ -18,7 +18,7 @@ public AbstractExpressionRenderer(final StringRendererConfig config) { } protected void appendKeyword(final String keyword) { - this.builder.append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); + this.builder.append(this.config.useLowerCase() ? keyword.toLowerCase() : keyword); } protected void connect(final BooleanExpression expression) { diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java index 1588a707..bb112496 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -9,7 +9,7 @@ public BooleanExpressionRenderer(final StringRendererConfig config) { } public BooleanExpressionRenderer() { - this(new StringRendererConfig.Builder().build()); + this(StringRendererConfig.builder().build()); } @Override diff --git a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java index 08abe2a0..ef7c8f02 100644 --- a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java +++ b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java @@ -21,7 +21,7 @@ public AbstractFragmentRenderer(final StringRendererConfig config) { // [impl->dsn~rendering.sql.configurable-case~1] protected void appendKeyWord(final String keyword) { - append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); + append(this.config.useLowerCase() ? keyword.toLowerCase() : keyword); } protected StringBuilder append(final String string) { @@ -58,6 +58,40 @@ protected void appendRenderedValueExpression(final ValueExpression expression) { append(renderer.render()); } + // [impl->dsn~rendering.add-double-quotes-for-schema-table-and-column-identifiers~1] + protected void appendAutoQuoted(final String identifier) { + if (this.config.useQuotes()) { + appendQuoted(identifier); + } else { + append(identifier); + } + } + + private void appendQuoted(final String identifier) { + boolean first = true; + for (final String part : identifier.split("\\.")) { + if (!first) { + append("."); + } + quoteIdentiferPart(part); + first = false; + } + } + + private void quoteIdentiferPart(final String part) { + if ("*".equals(part)) { + append("*"); + } else { + if (!part.startsWith("\"")) { + append("\""); + } + append(part); + if (!part.endsWith("\"")) { + append("\""); + } + } + } + @Override public String render() { return this.builder.toString(); diff --git a/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java index 07d99554..cbc79551 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java +++ b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java @@ -7,9 +7,11 @@ */ public class StringRendererConfig { private final boolean lowerCase; + private final boolean quote; - private StringRendererConfig(final boolean lowerCase) { - this.lowerCase = lowerCase; + private StringRendererConfig(final Builder builder) { + this.lowerCase = builder.lowerCase; + this.quote = builder.quote; } /** @@ -17,15 +19,46 @@ private StringRendererConfig(final boolean lowerCase) { * * @return true if statements are produced in lower case */ - public boolean produceLowerCase() { + public boolean useLowerCase() { return this.lowerCase; } + /** + * Get whether identifiers should be enclosed in double quotation marks. + * + * @return true if should be enclosed in quotes + */ + public boolean useQuotes() { + return this.quote; + } + + /** + * Create the default configuration. + * + * @return default configuration + */ + public static StringRendererConfig createDefault() { + return builder().build(); + } + + /** + * Get a builder for {@link StringRendererConfig} + * + * @return builder + */ + public static Builder builder() { + return new Builder(); + } + /** * Builder for {@link StringRendererConfig} */ public static class Builder { private boolean lowerCase = false; + private boolean quote = false; + + private Builder() { + } /** * Create a new instance of a {@link StringRendererConfig} @@ -33,7 +66,7 @@ public static class Builder { * @return new instance */ public StringRendererConfig build() { - return new StringRendererConfig(this.lowerCase); + return new StringRendererConfig(this); } /** @@ -46,14 +79,16 @@ public Builder lowerCase(final boolean lowerCase) { this.lowerCase = lowerCase; return this; } - } - /** - * Create the default configuration. - * - * @return default configuration - */ - public static StringRendererConfig createDefault() { - return new Builder().build(); + /** + * Define whether schema, table and field identifiers should be enclosed in double quotation marks. + * + * @param quote set to true if identifiers should be enclosed in quotes + * @return this instance for fluent programming + */ + public Builder quoteIdentifiers(final boolean quote) { + this.quote = quote; + return this; + } } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java index ec691730..0d2b462a 100644 --- a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java @@ -18,7 +18,7 @@ void testCreateWithDefaultConfig() { @Test void testCreateWithConfig() { - final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + final StringRendererConfig config = StringRendererConfig.builder().lowerCase(true).build(); final InsertRenderer renderer = InsertRenderer.create(config); final Insert insert = StatementFactory.getInstance().insertInto("city"); insert.accept(renderer); diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java index 40e96e6e..16a07966 100644 --- a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java @@ -30,7 +30,7 @@ void testInsert() { @Test void testInsertRendersToWithConfig() { assertThat(this.insert, - rendersWithConfigTo(new StringRendererConfig.Builder().lowerCase(true).build(), "insert into person")); + rendersWithConfigTo(StringRendererConfig.builder().lowerCase(true).build(), "insert into person")); } // [utest->dsn~rendering.sql.insert~1] diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java index 5954bac6..f71fbbd9 100644 --- a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java @@ -18,7 +18,7 @@ void testCreateWithDefaultConfig() { @Test void testCreateWithConfig() { - final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + final StringRendererConfig config = StringRendererConfig.builder().lowerCase(true).build(); final SelectRenderer renderer = SelectRenderer.create(config); final Select select = StatementFactory.getInstance().select(); select.accept(renderer); diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java index f9b6420b..5d7c1d07 100644 --- a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java @@ -30,7 +30,7 @@ void testSelectAll() { @Test void testSelectAllLowerCase() { assertThat(this.select.all(), - rendersWithConfigTo(new StringRendererConfig.Builder().lowerCase(true).build(), "select *")); + rendersWithConfigTo(StringRendererConfig.builder().lowerCase(true).build(), "select *")); } // [utest->dsn~rendering.sql.select~1] @@ -76,4 +76,12 @@ void testAddClausesInRandomOrder() { assertThat(this.select.limit(1).all().where(BooleanTerm.not("foo")).from().join("A", "A.aa = B.bb").table("B"), rendersTo("SELECT * FROM B JOIN A ON A.aa = B.bb WHERE NOT(foo) LIMIT 1")); } + + // [utest->dsn~rendering.add-double-quotes-for-schema-table-and-column-identifiers~1] + @Test + void testSelectWithQuotedIdentifiers() { + final StringRendererConfig config = StringRendererConfig.builder().quoteIdentifiers(true).build(); + assertThat(this.select.field("fieldA", "tableA.fieldB").from().table("schemaA.tableA"), + rendersWithConfigTo(config, "SELECT \"fieldA\", \"tableA\".\"fieldB\" FROM \"schemaA\".\"tableA\"")); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java index 5e8129a2..6faaa2b1 100644 --- a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -95,7 +95,7 @@ void testAndWhitNestedOr() { @Test void testAndWhitNestedOrInLowercase() { final BooleanExpression expression = and(or(not("a"), "b"), or("c", "d")); - final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + final StringRendererConfig config = StringRendererConfig.builder().lowerCase(true).build(); assertThat(expression, rendersWithConfigTo(config, "(not(a) or b) and (c or d)")); } From d8b2d74e8a73b02c76995e613b5ce6f9cd91965f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Wed, 10 Oct 2018 14:35:37 +0200 Subject: [PATCH 31/43] PMI-95: Fixed insert value list rendering. Added missing unit tests. --- .../com/exasol/sql/dml/rendering/InsertRenderer.java | 4 ++-- .../sql/dml/rendering/TestInsertRendering.java | 8 ++++---- .../sql/dql/rendering/TestSelectRendering.java | 12 ++++++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java index f3e89425..97c2905d 100644 --- a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -53,7 +53,7 @@ public void leave(final InsertFields insertFields) { @Override public void visit(final InsertValues insertValues) { - appendKeyWord(" VALUES "); + appendKeyWord(" VALUES ("); for (final ValueExpression expression : insertValues.getValues()) { appendCommaWhenNeeded(insertValues); appendRenderedValueExpression(expression); @@ -63,7 +63,7 @@ public void visit(final InsertValues insertValues) { @Override public void leave(final InsertValues insertValues) { - // intentionally empty + append(")"); } /** diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java index 16a07966..387da8f2 100644 --- a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java @@ -43,21 +43,21 @@ void testInsertFields() { // [utest->dsn~values-as-insert-source~1] @Test void testInsertValues() { - assertThat(this.insert.values(1, "a"), rendersTo("INSERT INTO person VALUES 1, 'a'")); + assertThat(this.insert.values(1, "a"), rendersTo("INSERT INTO person VALUES (1, 'a')")); } // [utest->dsn~rendering.sql.insert~1] // [utest->dsn~values-as-insert-source~1] @Test void testInsertValuePlaceholder() { - assertThat(this.insert.valuePlaceholder(), rendersTo("INSERT INTO person VALUES ?")); + assertThat(this.insert.valuePlaceholder(), rendersTo("INSERT INTO person VALUES (?)")); } // [utest->dsn~rendering.sql.insert~1] // [utest->dsn~values-as-insert-source~1] @Test void testInsertValuePlaceholders() { - assertThat(this.insert.valuePlaceholders(3), rendersTo("INSERT INTO person VALUES ?, ?, ?")); + assertThat(this.insert.valuePlaceholders(3), rendersTo("INSERT INTO person VALUES (?, ?, ?)")); } // [utest->dsn~rendering.sql.insert~1] @@ -65,6 +65,6 @@ void testInsertValuePlaceholders() { @Test void testInsertMixedValuesAndPlaceholders() { assertThat(this.insert.values(1).valuePlaceholders(3).values("b", 4), - rendersTo("INSERT INTO person VALUES 1, ?, ?, ?, 'b', 4")); + rendersTo("INSERT INTO person VALUES (1, ?, ?, ?, 'b', 4)")); } } \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java index 5d7c1d07..e19c76df 100644 --- a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java @@ -81,7 +81,15 @@ void testAddClausesInRandomOrder() { @Test void testSelectWithQuotedIdentifiers() { final StringRendererConfig config = StringRendererConfig.builder().quoteIdentifiers(true).build(); - assertThat(this.select.field("fieldA", "tableA.fieldB").from().table("schemaA.tableA"), - rendersWithConfigTo(config, "SELECT \"fieldA\", \"tableA\".\"fieldB\" FROM \"schemaA\".\"tableA\"")); + assertThat(this.select.field("fieldA", "tableA.fieldB", "tableB.*").from().table("schemaA.tableA"), + rendersWithConfigTo(config, + "SELECT \"fieldA\", \"tableA\".\"fieldB\", \"tableB\".* FROM \"schemaA\".\"tableA\"")); + } + + @Test + void testSelectWithQuotedIdentifiersDoesNotAddExtraQuotes() { + final StringRendererConfig config = StringRendererConfig.builder().quoteIdentifiers(true).build(); + assertThat(this.select.field("\"fieldA\"", "\"tableA\".fieldB"), + rendersWithConfigTo(config, "SELECT \"fieldA\", \"tableA\".\"fieldB\"")); } } \ No newline at end of file From 3b0291947c74904a4fc8ad1f46f4d07d7a31e880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Mon, 15 Oct 2018 10:59:34 +0200 Subject: [PATCH 32/43] PMI-95: Fixed findings from @bobkodex. --- doc/design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/design.md b/doc/design.md index 03f001aa..aa444da3 100644 --- a/doc/design.md +++ b/doc/design.md @@ -98,7 +98,7 @@ Needs: impl, utest #### Renderer add Double Quotes for Schema, Table and Column Identifiers `dsn~rendering.add-double-quotes-for-schema-table-and-column-identifiers~1` -The renderer adds double quotes the following identifiers if configured: +The renderer sets the following identifiers in double quotes if configured: * Schema identifiers * Table identifiers From 42eb923aebcdf6aaa1bbc0d540aeecf913c08358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 09:17:45 +0200 Subject: [PATCH 33/43] PMI-97: Convert and parse INTERVALS. --- .classpath | 45 ++++++--- doc/design.md | 34 +++++++ doc/system_requirements.md | 39 +++++--- pom.xml | 6 ++ .../interval/IntervalDayToSecond.java | 99 +++++++++++++++++++ .../datatype/interval/TestInterval.java | 47 +++++++++ 6 files changed, 241 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java create mode 100644 src/test/java/com/exasol/datatype/interval/TestInterval.java diff --git a/.classpath b/.classpath index c3b4b716..ebfacabc 100644 --- a/.classpath +++ b/.classpath @@ -1,15 +1,34 @@ - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/design.md b/doc/design.md index aa444da3..580a4e20 100644 --- a/doc/design.md +++ b/doc/design.md @@ -112,4 +112,38 @@ Covers: * `req~rendering.sql.confiugrable-identifier-quoting~1` +Needs: impl, utest + +### Exasol Dialect Specific + +#### Converting from 64 bit Integers to INTERVAL DAY TO SECOND +`dsn~exasol.converting-int-to-interval-day-to-second~1` + +The data converter converts integers to `INTERVAL DAY TO SECOND`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL DAY TO SECOND From Strings +`dsn~exasol.parsing-interval-day-to-second-from-strings~1` + +The data converter can parse `INTERVAL DAY TO SECOND` from strings in the following format: + + inverval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] + + hours = "2" "0" - "3" / [ "0" / "1" ] DIGIT + + minutes = "5" DIGIT / [ "0" - "4" ] DIGIT + + seconds = "5" DIGIT / [ "0" - "4" ] DIGIT + + milliseconds = 1*3DIGIT + +Covers: + +* `req~integer-interval-conversion~1` + Needs: impl, utest \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 109c6efd..824be8d3 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -84,6 +84,17 @@ Making sure at compile time that illegal constructs do not compile make the resu Needs: req +### Data Conversion +`feat~data-conversion~1` + +ESB converts between values of compatible data types. + +Rationale: + +Different databases and related tools use different ways to store and process similar data types. A collection of well-tested converters saves the API users time and trouble. + +Needs: req + ## Functional Requirements ### Statement Structure @@ -202,7 +213,7 @@ Covers: Needs: dsn -###### Configurable Identifier Quoting +#### Configurable Identifier Quoting `req~rendering.sql.confiugrable-identifier-quoting~1` ESB allows users to choose whether the following identifiers should be quoted in the rendered query: @@ -221,10 +232,6 @@ Covers: Needs: dsn -#### TODO - -* One line / pretty - #### SELECT Statement Rendering `req~rendering.sql.select~1` @@ -247,19 +254,19 @@ Covers: Needs: dsn -### TODO +### Exasol Dialect Specific Requirements + +###### Integer - Interval Conversion +`req~integer-interval-conversion~1` ---- +ESB converts values of type `INTERVAL` to integer and vice-versa. -SELECT -* Fields -* Asterisk ("*") +Rationale: + +Neighboring systems of an Exasol database often do to have equivalent data types, so conversion to a primitive data type is required. -FROM +Covers: -( INNER / ( LEFT / RIGHT / FULL ) OUTER ) JOIN -* ON +* [feat~data-conversion~1](#data-conversion) -LIMIT -* offset -* count +Needs: dsn \ No newline at end of file diff --git a/pom.xml b/pom.xml index efa0541c..d891ef7e 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,12 @@ ${junit.platform.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + org.hamcrest hamcrest-all diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java new file mode 100644 index 00000000..d77f5eea --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -0,0 +1,99 @@ +package com.exasol.datatype.interval; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class IntervalDayToSecond { + private static final long MILLIS_PER_SECOND = 1000L; + private static final long SECONDS_PER_MINUTE = 60L; + private static final long MINUTES_PER_HOUR = 60L; + private static final long HOURS_PER_DAY = 24L; + private static final long MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND; + private static final long MILLIS_PER_HOUR = MINUTES_PER_HOUR * MILLIS_PER_MINUTE; + private static final long MILLIS_PER_DAY = HOURS_PER_DAY * MILLIS_PER_HOUR; + private final long value; + private static final int DAYS_MATCHING_GROUP = 1; + private static final int HOURS_MATCHING_GROUP = 2; + private static final int MINUTES_MATCHING_GROUP = 3; + private static final int SECONDS_MATCHING_GROUP = 4; + private static final int MILLIS_MATCHING_GROUP = 5; + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(?:(\\d{1,9})\\s+)?" // days + + "(\\d{1,2})" // hours + + ":(\\d{1,2})" // minutes + + "(?::(\\d{1,2})" // seconds + + "(?:\\.(\\d{1,3}))?)?" // milliseconds + ); + + private IntervalDayToSecond(final long value) { + this.value = value; + } + + private IntervalDayToSecond(final String text) { + final Matcher matcher = INTERVAL_PATTERN.matcher(text); + if (matcher.matches()) { + this.value = MILLIS_PER_DAY * parseMatchingGroupToLong(matcher, DAYS_MATCHING_GROUP) // + + MILLIS_PER_HOUR * parseMatchingGroupToLong(matcher, HOURS_MATCHING_GROUP) // + + MILLIS_PER_MINUTE * parseMatchingGroupToLong(matcher, MINUTES_MATCHING_GROUP) // + + MILLIS_PER_SECOND * parseMatchingGroupToLong(matcher, SECONDS_MATCHING_GROUP) // + + parseMatchingGroupToLong(matcher, MILLIS_MATCHING_GROUP); + } else { + throw new IllegalArgumentException( + "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\""); + } + } + + private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) { + return (matcher.group(groupNumber) == null) ? 0 : Long.parseLong(matcher.group(groupNumber)); + } + + @Override + public String toString() { + return hasDays() // + ? String.format("%d %d:%02d:%02d.%03d", getDays(), getHours(), getMinutes(), getSeconds(), getMillis()) // + : String.format("%d:%02d:%02d.%03d", getHours(), getMinutes(), getSeconds(), getMillis()); + } + + private boolean hasDays() { + return this.value >= MILLIS_PER_DAY; + } + + private long getDays() { + return this.value / MILLIS_PER_DAY; + } + + private long getHours() { + return this.value / MILLIS_PER_HOUR % HOURS_PER_DAY; + } + + private long getMinutes() { + return this.value / MILLIS_PER_MINUTE % MINUTES_PER_HOUR; + } + + private long getSeconds() { + return this.value / MILLIS_PER_SECOND % SECONDS_PER_MINUTE; + } + + private long getMillis() { + return this.value % MILLIS_PER_SECOND; + } + + /** + * Create an {@link IntervalDayToSecond} from a number of milliseconds + * + * @param value milliseconds + * @return interval + */ + public static IntervalDayToSecond ofMillis(final long value) { + return new IntervalDayToSecond(value); + } + + /** + * Parse an {@link IntervalDayToSecond} from a string + * + * @param text + * @return + */ + public static IntervalDayToSecond parse(final String text) { + return new IntervalDayToSecond(text); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/datatype/interval/TestInterval.java b/src/test/java/com/exasol/datatype/interval/TestInterval.java new file mode 100644 index 00000000..915f907e --- /dev/null +++ b/src/test/java/com/exasol/datatype/interval/TestInterval.java @@ -0,0 +1,47 @@ +package com.exasol.datatype.interval; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TestInterval { + // [utest->req~integer-interval-conversion~1] + @ParameterizedTest + @CsvSource({ // + 0L + ", '0:00:00.000'", // + 999L + ", '0:00:00.999'", // + 59L * 1000 + ", '0:00:59.000'", // + 59L * 60 * 1000 + ", '0:59:00.000'", // + 23L * 60 * 60 * 1000 + ", '23:00:00.000'", // + 999999999L * 24 * 60 * 60 * 1000 + ", '999999999 0:00:00.000'", // + 1L * 24 * 60 * 60 * 1000 + 1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1 * 1000 + 1 + ", '1 1:01:01.001'" // + }) + void testMillisecondsToIntervalDayToSecond(final long value, final String expected) { + assertThat(IntervalDayToSecond.ofMillis(value).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + @ParameterizedTest + @CsvSource({ "'0:0', '0:00:00.000'", // + "'1:2:3', '1:02:03.000'", // + "'11:22:33.444', '11:22:33.444'", // + "'1 22:33:44.555', '1 22:33:44.555'", // + "'999999999 22:33:44', '999999999 22:33:44.000'" // + }) + void testParse(final String text, final String expected) { + assertThat(IntervalDayToSecond.parse(text).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + @ParameterizedTest + @ValueSource(strings = { "0", ":0", "1.0", "123:45", "12:234", "12:34:567", "12:34:56:7890", // + "1000000000 0:0" // + }) + void testParseIllegalInputThrowsException(final String text) { + assertThrows(IllegalArgumentException.class, () -> IntervalDayToSecond.parse(text)); + } +} \ No newline at end of file From fafa12f5e380ba7ba56ea747af1e3b5a134f4d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 12:29:09 +0200 Subject: [PATCH 34/43] Added missing documentation. --- .../com/exasol/datatype/interval/IntervalDayToSecond.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index d77f5eea..4907a250 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -3,6 +3,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * This class implements the Exasol-proprietary data type INTERVAL DAY(x) TO SECONDS(y). It supports + * conversions to and from strings and from milliseconds. + */ public class IntervalDayToSecond { private static final long MILLIS_PER_SECOND = 1000L; private static final long SECONDS_PER_MINUTE = 60L; @@ -11,7 +15,6 @@ public class IntervalDayToSecond { private static final long MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND; private static final long MILLIS_PER_HOUR = MINUTES_PER_HOUR * MILLIS_PER_MINUTE; private static final long MILLIS_PER_DAY = HOURS_PER_DAY * MILLIS_PER_HOUR; - private final long value; private static final int DAYS_MATCHING_GROUP = 1; private static final int HOURS_MATCHING_GROUP = 2; private static final int MINUTES_MATCHING_GROUP = 3; @@ -23,6 +26,7 @@ public class IntervalDayToSecond { + "(?::(\\d{1,2})" // seconds + "(?:\\.(\\d{1,3}))?)?" // milliseconds ); + private final long value; private IntervalDayToSecond(final long value) { this.value = value; From aaf4bf6ba7a2055e647ea093f3391b4512c1e0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 13:00:25 +0200 Subject: [PATCH 35/43] PMI-97: Improved API documentation. --- .../interval/IntervalDayToSecond.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 4907a250..0e682801 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -6,6 +6,21 @@ /** * This class implements the Exasol-proprietary data type INTERVAL DAY(x) TO SECONDS(y). It supports * conversions to and from strings and from milliseconds. + * + *

+ * In Exasol this data type represents a time difference consisting of the following components: + *

+ *
    + *
  • days
  • + *
  • hours
  • + *
  • minutes
  • + *
  • seconds
  • + *
  • milliseconds (or fraction of seconds)
  • + *
+ * + * Since milliseconds are the highest resolution, each interval can also be expresses as a total number of milliseconds. + * This is also the recommended way to represent the interval values in other systems which do not natively support this + * data type. */ public class IntervalDayToSecond { private static final long MILLIS_PER_SECOND = 1000L; @@ -84,8 +99,8 @@ private long getMillis() { /** * Create an {@link IntervalDayToSecond} from a number of milliseconds * - * @param value milliseconds - * @return interval + * @param value total length of the interval in milliseconds + * @return interval with milliseconds resolution */ public static IntervalDayToSecond ofMillis(final long value) { return new IntervalDayToSecond(value); @@ -94,8 +109,24 @@ public static IntervalDayToSecond ofMillis(final long value) { /** * Parse an {@link IntervalDayToSecond} from a string * - * @param text - * @return + *

+ * The accepted format is: + *

+ *

+ * [dddddddd ]hh:mm[:ss[.SSS]] + *

+ * Where + *

+ *
    + *
  • d: day, 1-9 digits, optional
  • + *
  • h: hours, 1-2 digits, mandatory
  • + *
  • m: minutes, 1-2 digits, mandatory
  • + *
  • s: seconds, 1-2 digits, optional
  • + *
  • S: milliseconds, 1-3 digits, optional
  • + *
+ * + * @param text string representing an interval + * @return interval with milliseconds resolution */ public static IntervalDayToSecond parse(final String text) { return new IntervalDayToSecond(text); From c06e3b49c5fd642f6f26b0cec4aa25d8541ef9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 13:09:16 +0200 Subject: [PATCH 36/43] PMI-97: Improved API documentation. --- .../interval/IntervalDayToSecond.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 0e682801..492a3333 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -113,17 +113,22 @@ public static IntervalDayToSecond ofMillis(final long value) { * The accepted format is: *

*

- * [dddddddd ]hh:mm[:ss[.SSS]] + * [dddddddd ]hh:mm[:ss[.SSS]] *

* Where *

- *
    - *
  • d: day, 1-9 digits, optional
  • - *
  • h: hours, 1-2 digits, mandatory
  • - *
  • m: minutes, 1-2 digits, mandatory
  • - *
  • s: seconds, 1-2 digits, optional
  • - *
  • S: milliseconds, 1-3 digits, optional
  • - *
+ *
+ *
d
+ *
day, 1-9 digits, optional
+ *
h
+ *
hours, 1-2 digits, mandatory
+ *
m
+ *
minutes, 1-2 digits, mandatory
+ *
s
+ *
seconds, 1-2 digits, optional
+ *
S
+ *
milliseconds, 1-3 digits, optional
+ *
* * @param text string representing an interval * @return interval with milliseconds resolution From 19c0face7f295714f8010da7270e19c9802f0f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 14:05:44 +0200 Subject: [PATCH 37/43] PMI-97: Fixed requirement trace. --- .../java/com/exasol/datatype/interval/IntervalDayToSecond.java | 2 ++ src/test/java/com/exasol/datatype/interval/TestInterval.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 492a3333..905e55e3 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -102,6 +102,7 @@ private long getMillis() { * @param value total length of the interval in milliseconds * @return interval with milliseconds resolution */ + // [impl->dsn~exasol.converting-int-to-interval-day-to-second~1] public static IntervalDayToSecond ofMillis(final long value) { return new IntervalDayToSecond(value); } @@ -133,6 +134,7 @@ public static IntervalDayToSecond ofMillis(final long value) { * @param text string representing an interval * @return interval with milliseconds resolution */ + // [impl->dsn~exasol.parsing-interval-day-to-second-from-strings~1] public static IntervalDayToSecond parse(final String text) { return new IntervalDayToSecond(text); } diff --git a/src/test/java/com/exasol/datatype/interval/TestInterval.java b/src/test/java/com/exasol/datatype/interval/TestInterval.java index 915f907e..5b4f04ea 100644 --- a/src/test/java/com/exasol/datatype/interval/TestInterval.java +++ b/src/test/java/com/exasol/datatype/interval/TestInterval.java @@ -9,7 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource; class TestInterval { - // [utest->req~integer-interval-conversion~1] + // [utest->dsn~exasol.converting-int-to-interval-day-to-second~1] @ParameterizedTest @CsvSource({ // 0L + ", '0:00:00.000'", // From 9eef6c27162de399ee52a17b1ea31e953b0db66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 14:42:36 +0200 Subject: [PATCH 38/43] PMI-97: Added support for INTERVAL YEAR TO MONTH. Fixed documentation --- doc/design.md | 36 ++++++- .../interval/IntervalDayToSecond.java | 2 +- .../interval/IntervalYearToMonth.java | 95 +++++++++++++++++++ ...rval.java => TestIntervalDayToSecond.java} | 4 +- .../interval/TestIntervalYearToMonth.java | 42 ++++++++ 5 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java rename src/test/java/com/exasol/datatype/interval/{TestInterval.java => TestIntervalDayToSecond.java} (94%) create mode 100644 src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java diff --git a/doc/design.md b/doc/design.md index 580a4e20..4e4368f3 100644 --- a/doc/design.md +++ b/doc/design.md @@ -11,7 +11,7 @@ The Data Query Language (DQL) building block is responsible for managing `SELECT ### Fluent Programming -###### Statement Construction With Fluent Programming +#### Statement Construction With Fluent Programming `dsn~statement-construction-with-fluent-programming~1` All statement builders use the "fluent programming" model, where the return type of each builder step determines the possible next structural elements that can be added. @@ -134,11 +134,11 @@ The data converter can parse `INTERVAL DAY TO SECOND` from strings in the follow inverval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] - hours = "2" "0" - "3" / [ "0" / "1" ] DIGIT + hours = ( "2" "0" - "3" ) / ( [ "0" / "1" ] DIGIT ) - minutes = "5" DIGIT / [ "0" - "4" ] DIGIT + minutes = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) - seconds = "5" DIGIT / [ "0" - "4" ] DIGIT + seconds = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) milliseconds = 1*3DIGIT @@ -146,4 +146,32 @@ Covers: * `req~integer-interval-conversion~1` +Needs: impl, utest + +#### Converting from 64 bit Integers to INTERVAL YEAR TO MONTH +`dsn~exasol.converting-int-to-interval-year-to-month~1` + +The data converter converts integers to `INTERVAL YEAR TO MONTH`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL YEAR TO MONTH From Strings +`dsn~exasol.parsing-interval-year-to-month-from-strings~1` + +The data converter can parse `INTERVAL YEAR TO MONTH` from strings in the following format: + + inverval-y2m = days "-" months + + days = 1*9DIGIT + + months = ( "1" "0" - "2" ) / DIGIT + +Covers: + +* `req~integer-interval-conversion~1` + Needs: impl, utest \ No newline at end of file diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 905e55e3..bb55524b 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -18,7 +18,7 @@ *
  • milliseconds (or fraction of seconds)
  • * * - * Since milliseconds are the highest resolution, each interval can also be expresses as a total number of milliseconds. + * Since milliseconds are the highest resolution, each interval can also be expressed as a total number of milliseconds. * This is also the recommended way to represent the interval values in other systems which do not natively support this * data type. */ diff --git a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java new file mode 100644 index 00000000..14e1dbc3 --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java @@ -0,0 +1,95 @@ +package com.exasol.datatype.interval; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class implements the Exasol-proprietary data type INTERVAL YEAR(x) TO MONTH(y). It supports + * conversions to and from strings and from a number of months. + * + *

    + * In Exasol this data type represents a time difference consisting of the following components: + *

    + *
      + *
    • years
    • + *
    • months
    • + *
    + * + * Since months are the highest resolution, each interval can also be expressed as a total number of months. This is + * also the recommended way to represent the interval values in other systems which do not natively support this data + * type. + */ +public class IntervalYearToMonth { + private static final long MONTHS_PER_YEAR = 12L; + private static final int YEARS_MATCHING_GROUP = 1; + private static final int MONTHS_MATCHING_GROUP = 2; + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(\\d{1,9})-(\\d{1,2})"); + private final long value; + + private IntervalYearToMonth(final long value) { + this.value = value; + } + + private IntervalYearToMonth(final String text) { + final Matcher matcher = INTERVAL_PATTERN.matcher(text); + if (matcher.matches()) { + this.value = MONTHS_PER_YEAR * parseMatchingGroupToLong(matcher, YEARS_MATCHING_GROUP) // + + parseMatchingGroupToLong(matcher, MONTHS_MATCHING_GROUP); + } else { + throw new IllegalArgumentException( + "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\""); + } + } + + private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) { + return Long.parseLong(matcher.group(groupNumber)); + } + + @Override + public String toString() { + return String.format("%d-%02d", getYears(), getMonths()); + } + + private long getYears() { + return this.value / MONTHS_PER_YEAR; + } + + private long getMonths() { + return this.value % MONTHS_PER_YEAR; + } + + /** + * Create an {@link IntervalDayToSecond} from a number of months + * + * @param value total length of the interval in months + * @return interval with months resolution + */ + public static IntervalYearToMonth ofMonths(final long value) { + return new IntervalYearToMonth(value); + } + + /** + * Parse an {@link IntervalDayToSecond} from a string + * + *

    + * The accepted format is: + *

    + *

    + * YYYYYYYYY:MM + *

    + * Where + *

    + *
    + *
    Y
    + *
    years, 1-9 digits, mandatory
    + *
    M
    + *
    months, 1-2 digits, mandatory
    + *
    + * + * @param text string representing an interval + * @return interval with months resolution + */ + public static IntervalYearToMonth parse(final String text) { + return new IntervalYearToMonth(text); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/datatype/interval/TestInterval.java b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java similarity index 94% rename from src/test/java/com/exasol/datatype/interval/TestInterval.java rename to src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java index 5b4f04ea..285b5461 100644 --- a/src/test/java/com/exasol/datatype/interval/TestInterval.java +++ b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java @@ -8,7 +8,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; -class TestInterval { +class TestIntervalDayToSecond { // [utest->dsn~exasol.converting-int-to-interval-day-to-second~1] @ParameterizedTest @CsvSource({ // @@ -20,7 +20,7 @@ class TestInterval { 999999999L * 24 * 60 * 60 * 1000 + ", '999999999 0:00:00.000'", // 1L * 24 * 60 * 60 * 1000 + 1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1 * 1000 + 1 + ", '1 1:01:01.001'" // }) - void testMillisecondsToIntervalDayToSecond(final long value, final String expected) { + void testofMillis(final long value, final String expected) { assertThat(IntervalDayToSecond.ofMillis(value).toString(), equalTo(expected)); } diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java new file mode 100644 index 00000000..03cff591 --- /dev/null +++ b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java @@ -0,0 +1,42 @@ +package com.exasol.datatype.interval; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class TestIntervalYearToMonth { + // [utest->dsn~exasol.converting-int-to-interval-year-to-month~1] + @ParameterizedTest + @CsvSource({ // + 0L + ", '0-00'", // + 11L + ", '0-11'", // + 999999999L * 12 + ", '999999999-00'", // + 999999999L * 12 + 11 + ", '999999999-11'", // + 1L * 12 + 1 + ", '1-01'" // + }) + void testOfMonths(final long value, final String expected) { + assertThat(IntervalYearToMonth.ofMonths(value).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1] + @ParameterizedTest + @CsvSource({ "'0-0', '0-00'", // + "'1-2', '1-02'", // + "'22-11', '22-11'", // + "'999999999-11', '999999999-11'" // + }) + void testParse(final String text, final String expected) { + assertThat(IntervalYearToMonth.parse(text).toString(), equalTo(expected)); + } + + // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + @ParameterizedTest + @ValueSource(strings = { "0", "-0", "0-", "0-123", "1000000000-0" }) + void testParseIllegalInputThrowsException(final String text) { + assertThrows(IllegalArgumentException.class, () -> IntervalYearToMonth.parse(text)); + } +} \ No newline at end of file From 804a4cf5555eb7778919a6f2807fe3ba22d933db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 14:56:45 +0200 Subject: [PATCH 39/43] PMI-97: Fixed requirement trace. --- .../java/com/exasol/datatype/interval/IntervalYearToMonth.java | 2 ++ .../com/exasol/datatype/interval/TestIntervalYearToMonth.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java index 14e1dbc3..613a615a 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java @@ -64,6 +64,7 @@ private long getMonths() { * @param value total length of the interval in months * @return interval with months resolution */ + // [impl->dsn~exasol.converting-int-to-interval-year-to-month~1] public static IntervalYearToMonth ofMonths(final long value) { return new IntervalYearToMonth(value); } @@ -89,6 +90,7 @@ public static IntervalYearToMonth ofMonths(final long value) { * @param text string representing an interval * @return interval with months resolution */ + // [impl->dsn~exasol.parsing-interval-year-to-month-from-strings~1] public static IntervalYearToMonth parse(final String text) { return new IntervalYearToMonth(text); } diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java index 03cff591..8f25d81e 100644 --- a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java +++ b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java @@ -33,7 +33,7 @@ void testParse(final String text, final String expected) { assertThat(IntervalYearToMonth.parse(text).toString(), equalTo(expected)); } - // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1] @ParameterizedTest @ValueSource(strings = { "0", "-0", "0-", "0-123", "1000000000-0" }) void testParseIllegalInputThrowsException(final String text) { From 091fa79a75bee57795e2a27b2ac90ef95345d343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 15:04:00 +0200 Subject: [PATCH 40/43] PMI-97: Worked in review findings of @bobkodex. --- doc/design.md | 8 ++++++-- doc/system_requirements.md | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/design.md b/doc/design.md index 4e4368f3..1b8837ef 100644 --- a/doc/design.md +++ b/doc/design.md @@ -132,7 +132,7 @@ Needs: impl, utest The data converter can parse `INTERVAL DAY TO SECOND` from strings in the following format: - inverval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] + interval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] hours = ( "2" "0" - "3" ) / ( [ "0" / "1" ] DIGIT ) @@ -142,6 +142,8 @@ The data converter can parse `INTERVAL DAY TO SECOND` from strings in the follow milliseconds = 1*3DIGIT +Examples are `12:30`, `12:30.081` or `100 12:30:00.081`. + Covers: * `req~integer-interval-conversion~1` @@ -164,12 +166,14 @@ Needs: impl, utest The data converter can parse `INTERVAL YEAR TO MONTH` from strings in the following format: - inverval-y2m = days "-" months + interval-y2m = days "-" months days = 1*9DIGIT months = ( "1" "0" - "2" ) / DIGIT +Examples are `0-1` and `100-11`. + Covers: * `req~integer-interval-conversion~1` diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 824be8d3..f6287272 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -263,7 +263,7 @@ ESB converts values of type `INTERVAL` to integer and vice-versa. Rationale: -Neighboring systems of an Exasol database often do to have equivalent data types, so conversion to a primitive data type is required. +Neighboring systems of an Exasol database often do not have equivalent data types, so conversion to a primitive data type is required. Covers: From 9668d8806b071e244d3b726b6d6f43641ad0380b Mon Sep 17 00:00:00 2001 From: Sebastian B Date: Wed, 17 Oct 2018 09:34:36 +0200 Subject: [PATCH 41/43] Fixed error in example. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3fa252e1..93f2004e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Goals: ```java import com.exasol.sql.StatementFactory; import com.exasol.sql.SqlStatement; -import com.exasol.sql.rendering.SqlStatementRenderer; +import com.exasol.sql.rendering.SelectRenderer; SqlStatement statement = StatementFactory.getInstance() .select().field("firstname", "lastname") @@ -71,4 +71,4 @@ The milestones listed below are a rough outline and might be subject to change d * Data Manipulation Language (DML) statements * Data Definition Language (DDL) statements * Support for Standard SQL -* Support for other dialects (help welcome!) \ No newline at end of file +* Support for other dialects (help welcome!) From da665622ef2b806334f2299c3bb94040b8e92f46 Mon Sep 17 00:00:00 2001 From: Sebastian B Date: Wed, 17 Oct 2018 10:19:23 +0200 Subject: [PATCH 42/43] Removed Oracle JDK because automatic download now fails in Travis CI --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 861ccb97..f0632a28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,10 @@ language: java jdk: - openjdk8 - - oraclejdk10 - openjdk10 before_script: - version=$(grep -oP '(?<=^ )[^<]*' pom.xml) script: - - mvn clean install \ No newline at end of file + - mvn clean install From de1a121a423e5fc3922d247e695697c620bbf5e8 Mon Sep 17 00:00:00 2001 From: Sebastian B Date: Wed, 17 Oct 2018 10:30:09 +0200 Subject: [PATCH 43/43] Updated OpenJDK 10 to 11 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f0632a28..a8dd7a62 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: java jdk: - openjdk8 - - openjdk10 + - openjdk11 before_script: - version=$(grep -oP '(?<=^ )[^<]*' pom.xml)