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