diff --git a/.classpath b/.classpath new file mode 100644 index 00000000..c3b4b716 --- /dev/null +++ b/.classpath @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file 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/.travis.yml b/.travis.yml new file mode 100644 index 00000000..861ccb97 --- /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 diff --git a/README.md b/README.md index 3ae35b3a..760004fa 100644 --- a/README.md +++ b/README.md @@ -1 +1,75 @@ -# sql-statement-builder \ No newline at end of file +# 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 +import com.exasol.sql.StatementFactory; +import com.exasol.sql.SqlStatement; +import com.exasol.sql.rendering.SqlStatementRenderer; + +SqlStatement statement = StatementFactory.getInstance() + .select() + .field("firstname", "lastname") + .from("person"); + +String statementText = SqlStatementRenderer.render(statement); +``` + +## 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 | + +### 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 diff --git a/doc/system_requirements.md b/doc/system_requirements.md new file mode 100644 index 00000000..b95ee9db --- /dev/null +++ b/doc/system_requirements.md @@ -0,0 +1,19 @@ +* Upper case / lower case +* One line / pretty + +SQL features: + +--- + +SELECT +* Fields +* Asterisk ("*") + +FROM + +( INNER / ( LEFT / RIGHT / FULL ) OUTER ) JOIN +* ON + +LIMIT +* offset +* count 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 @@ + + + + + + + + + + + + + + + + + + + + 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/class/cl_fragments.plantuml b/model/diagrams/class/cl_fragments.plantuml new file mode 100644 index 00000000..0dbc4c44 --- /dev/null +++ b/model/diagrams/class/cl_fragments.plantuml @@ -0,0 +1,19 @@ +@startuml +!include ../exasol.skin + +together { + interface Fragment <> + interface FieldDefinition <> + interface TableReference <> +} + +FieldDefinition -u-|> Fragment +Field .u.|> FieldDefinition +Select .u.|> Fragment +TableReference -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/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 new file mode 100644 index 00000000..aaf51dc4 --- /dev/null +++ b/model/diagrams/exasol.skin @@ -0,0 +1,38 @@ +hide empty methods +hide empty attributes +skinparam style strictuml +skinparam classAttributeIconSize 0 +!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 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/pom.xml b/pom.xml new file mode 100644 index 00000000..efa0541c --- /dev/null +++ b/pom.xml @@ -0,0 +1,59 @@ + + 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.junit.platform + junit-platform-launcher + ${junit.platform.version} + test + + + org.hamcrest + hamcrest-all + 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/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java new file mode 100644 index 00000000..64c47657 --- /dev/null +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -0,0 +1,26 @@ +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. + */ +public abstract class AbstractFragment extends AbstractTreeNode implements Fragment { + protected AbstractFragment() { + super(); + } + + @Override + public void accept(final FragmentVisitor visitor) { + acceptConcrete(visitor); + for (final TreeNode child : this.getChildren()) { + ((Fragment) 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..2269bf73 --- /dev/null +++ b/src/main/java/com/exasol/sql/Fragment.java @@ -0,0 +1,7 @@ +package com.exasol.sql; + +import com.exasol.util.TreeNode; + +public interface Fragment extends TreeNode { + public void accept(FragmentVisitor visitor); +} 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..33fad8b1 --- /dev/null +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -0,0 +1,22 @@ +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(BooleanValueExpression booleanValueExpression); + + public void visit(LimitClause limitClause); +} \ 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/StatementFactory.java b/src/main/java/com/exasol/sql/StatementFactory.java new file mode 100644 index 00000000..865ea05c --- /dev/null +++ b/src/main/java/com/exasol/sql/StatementFactory.java @@ -0,0 +1,35 @@ +package com.exasol.sql; + +import com.exasol.sql.dql.Select; + +/** + * 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(); + } +} 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..e95d16eb --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java @@ -0,0 +1,36 @@ +package com.exasol.sql.dql; + +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 expression nested boolean expression + */ + public BooleanValueExpression(final BooleanExpression expression) { + super(); + 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/Field.java b/src/main/java/com/exasol/sql/dql/Field.java new file mode 100644 index 00000000..89770a2a --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Field.java @@ -0,0 +1,26 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; + +public class Field extends AbstractFragment implements FieldDefinition { + private final String name; + + protected Field(final String name) { + super(); + this.name = name; + } + + public String getName() { + return this.name; + } + + public static Field all() { + return new Field("*"); + } + + @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/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java new file mode 100644 index 00000000..38b8254d --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -0,0 +1,165 @@ +package com.exasol.sql.dql; + +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 { + /** + * 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(name)); + return this; + } + + /** + * 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; + } + + /** + * 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(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(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(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(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(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(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(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(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(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(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 new file mode 100644 index 00000000..7eaadf2f --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -0,0 +1,58 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.*; + +/** + * This class implements the {@link Join} part of a WHERE clause. + */ +public class Join extends AbstractFragment implements Fragment { + private final JoinType type; + private final String name; + private final String specification; + + /** + * Create a new {@link Join} instance + * + * @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(); + 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; + } + + /** + * 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/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/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java new file mode 100644 index 00000000..741d15b9 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -0,0 +1,69 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; + +/** + * 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 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); + } + + /** + * Create a new instance of a {@link LimitClause} + * + * @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(); + 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; + } +} \ 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 new file mode 100644 index 00000000..2f384302 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -0,0 +1,81 @@ +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 { + /** + * Create a new instance of a {@link Select} + */ + public Select() { + super(); + } + + /** + * Add a wildcard field for all involved fields. + * + * @return this instance for fluent programming + */ + public Select all() { + addChild(Field.all()); + 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(name)); + } + 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(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 + * + * @param name table reference name + * @return the FROM clause + */ + public FromClause from(final String name) { + final FromClause from = new FromClause().from(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 = new FromClause().fromTableAs(name, as); + 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..486142f3 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Table.java @@ -0,0 +1,60 @@ +package com.exasol.sql.dql; + +import java.util.Optional; + +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.FragmentVisitor; + +/** + * This class represents a {@link Table} in an SQL Statement + */ +public class Table extends AbstractFragment implements TableReference { + private final String name; + private final Optional as; + + /** + * Create a new {@link Table} + * + * @param name table name + */ + public Table(final String name) { + super(); + this.name = name; + this.as = Optional.empty(); + } + + /** + * 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); + } + + /** + * Get the name of the table + * + * @return table name + */ + 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); + } +} \ 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 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/dql/ValueExpression.java b/src/main/java/com/exasol/sql/dql/ValueExpression.java new file mode 100644 index 00000000..3ec57fd5 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/ValueExpression.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.AbstractFragment; + +/** + * Abstract base class for all types of value expressions + */ +public abstract class ValueExpression extends AbstractFragment { + /** + * Create a new instance of a {@link ValueExpression} + */ + public ValueExpression() { + super(); + } +} \ No newline at end of file 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..03040d2c --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java @@ -0,0 +1,46 @@ +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); + } + dismissConcrete(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); + + /** + * 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 new file mode 100644 index 00000000..7e1ddd5a --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/And.java @@ -0,0 +1,34 @@ +package com.exasol.sql.expression; + +/** + * 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(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/BooleanExpression.java b/src/main/java/com/exasol/sql/expression/BooleanExpression.java new file mode 100644 index 00000000..2bf25f9b --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanExpression.java @@ -0,0 +1,15 @@ +package com.exasol.sql.expression; + +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/BooleanExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java new file mode 100644 index 00000000..d2b55684 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java @@ -0,0 +1,22 @@ +package com.exasol.sql.expression; + +/** + * Visitor interface for a {@link BooleanTerm} + */ +public interface BooleanExpressionVisitor { + void visit(Not not); + + void visit(Literal literal); + + void visit(And and); + + void leave(Not not); + + void leave(Literal literal); + + 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 new file mode 100644 index 00000000..9fad266f --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -0,0 +1,47 @@ +package com.exasol.sql.expression; + +public abstract class BooleanTerm extends AbstractBooleanExpression { + private BooleanTerm() { + super(); + } + + public static BooleanExpression not(final String string) { + return new Not(string); + } + + public static BooleanExpression not(final BooleanExpression expression) { + return new Not(expression); + } + + public static BooleanExpression and(final String... strings) { + return new And(strings); + } + + public static BooleanExpression and(final BooleanExpression expression, final String string) { + return new And(expression, Literal.of(string)); + } + + public static BooleanExpression and(final String literal, final BooleanExpression expression) { + return new And(Literal.of(literal), expression); + } + + public static BooleanExpression and(final BooleanExpression... 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); + } +} \ 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..879212a5 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Literal.java @@ -0,0 +1,48 @@ +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); + } + + @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 new file mode 100644 index 00000000..f67bddde --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Not.java @@ -0,0 +1,34 @@ +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); + } + + @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 new file mode 100644 index 00000000..c3e5b55e --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -0,0 +1,109 @@ +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 BooleanExpressionRenderer(final StringRendererConfig config) { + this.config = config; + } + + public BooleanExpressionRenderer() { + this.config = new StringRendererConfig.Builder().build(); + } + + private void appendKeyword(final String keyword) { + this.builder.append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); + } + + @Override + public void visit(final Not 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 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 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(); + } +} \ 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 new file mode 100644 index 00000000..71dbf2e1 --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java @@ -0,0 +1,137 @@ +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.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; + + /** + * 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"); + } + + 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()); + } + + private void appendSpace() { + append(" "); + } + + private void appendCommaWhenNeeded(final Fragment fragment) { + if (!fragment.isFirstSibling()) { + append(","); + } + } + + @Override + public void visit(final FromClause fromClause) { + appendKeyWord(" FROM"); + } + + @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()); + } + } + + @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()); + } + + @Override + public void visit(final BooleanValueExpression value) { + final BooleanExpressionRenderer subRenderer = new BooleanExpressionRenderer(); + value.getExpression().accept(subRenderer); + appendSpace(); + append(subRenderer.render()); + } + + @Override + public void visit(final LimitClause limit) { + appendKeyWord(" LIMIT "); + if (limit.hasOffset()) { + append(limit.getOffset()); + appendKeyWord(", "); + } + append(limit.getCount()); + } + + 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 new file mode 100644 index 00000000..2ab8bf7d --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java @@ -0,0 +1,52 @@ +package com.exasol.sql.rendering; + +import com.exasol.sql.StatementFactory; + +/** + * 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 + * @return this instance for fluent programming + */ + 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/util/AbstractBottomUpTreeNode.java b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java new file mode 100644 index 00000000..a71be4f9 --- /dev/null +++ b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java @@ -0,0 +1,105 @@ +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}. + * + * @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 { + throw new IllegalStateException( + "Tried to link node \"" + child.toString() + "\" in bottom-up tree to parent \"" + this.toString() + + "\" which already has a parent \"" + 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() + "\""); + } + } + + @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 isRoot() { + return (this == getRoot()); + } + + @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..02227d82 --- /dev/null +++ b/src/main/java/com/exasol/util/AbstractTreeNode.java @@ -0,0 +1,82 @@ +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 TreeNode root; + private 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.root = this; + this.parent = null; + } + + /** + * Link to a parent node + * + * @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 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(); + } + } + + @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); + ((AbstractTreeNode) child).setParent(this); + } + + @Override + public List getChildren() { + return this.children; + } + + @Override + 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); + } + + @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..3ac7d172 --- /dev/null +++ b/src/main/java/com/exasol/util/TreeNode.java @@ -0,0 +1,72 @@ +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. + *

+ * Important: this also automatically creates a link in the + * opposite direction. All implementations must adhere to this rule. + * + * @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 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(); + + /** + * 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(); +} \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestJoin.java b/src/test/java/com/exasol/dql/rendering/TestJoin.java new file mode 100644 index 00000000..fed3eb20 --- /dev/null +++ b/src/test/java/com/exasol/dql/rendering/TestJoin.java @@ -0,0 +1,77 @@ +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 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")); + } + + @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 testRightJoin() { + 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").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 + 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 testRightOuterJoin() { + 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/rendering/TestLimit.java b/src/test/java/com/exasol/dql/rendering/TestLimit.java new file mode 100644 index 00000000..02d6d7dc --- /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 2, 3")); + } +} \ 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 new file mode 100644 index 00000000..472d980b --- /dev/null +++ b/src/test/java/com/exasol/dql/rendering/TestSelect.java @@ -0,0 +1,74 @@ +package com.exasol.dql.rendering; + +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.Test; + +import com.exasol.sql.StatementFactory; +import com.exasol.sql.expression.BooleanTerm; +import com.exasol.sql.rendering.StringRendererConfig; + +class TestSelect { + @Test + void testGetParentReturnsNull() { + assertThat(StatementFactory.getInstance().select().getParent(), nullValue()); + } + + @Test + void testEmptySelect() { + assertThat(StatementFactory.getInstance().select(), rendersTo("SELECT")); + } + + @Test + void testEmptySelectLowerCase() { + final StringRendererConfig config = new StringRendererConfig.Builder().lowerCase(true).build(); + assertThat(StatementFactory.getInstance().select(), rendersWithConfigTo(config, "select")); + } + + @Test + void testSelectAll() { + assertThat(StatementFactory.getInstance().select().all(), rendersTo("SELECT *")); + } + + @Test + void testSelectFieldNames() { + 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() { + 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")); + } + + @Test + void testSelectEmbeddedBooleanExpression() { + assertThat(StatementFactory.getInstance().select().value(BooleanTerm.not("a")), rendersTo("SELECT NOT(a)")); + } +} \ 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 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/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/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/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java new file mode 100644 index 00000000..5d58da54 --- /dev/null +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -0,0 +1,80 @@ +package com.exasol.sql.expression.rendering; + +import static com.exasol.hamcrest.BooleanExpressionRenderResultMatcher.rendersTo; +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("a"); + assertThat(expression, rendersTo("NOT(a)")); + } + + @Test + void testUnaryNotWithExpression() { + final BooleanExpression expression = not(not("a")); + assertThat(expression, rendersTo("NOT(NOT(a))")); + } + + @Test + 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 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..9ef5afb7 --- /dev/null +++ b/src/test/java/com/exasol/util/DummyTreeNode.java @@ -0,0 +1,7 @@ +package com.exasol.util; + +public class DummyTreeNode extends AbstractTreeNode { + public DummyTreeNode() { + super(); + } +} 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..31c20636 --- /dev/null +++ b/src/test/java/com/exasol/util/TestAbstractTreeNode.java @@ -0,0 +1,110 @@ +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.*; + +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.addChild(child); + assertFalse(child.isRoot()); + } + + @Test + void testIsChildOnChild() { + final TreeNode child = new DummyTreeNode(); + this.node.addChild(child); + assertTrue(child.isChild()); + } + + @Test + void testIsFirstSiblingOnChild() { + final TreeNode child = new DummyTreeNode(); + this.node.addChild(child); + assertTrue(child.isFirstSibling()); + } + + @Test + void testIsFirstSiblingOnFirstChild() { + final TreeNode child = new DummyTreeNode(); + final TreeNode otherChild = new DummyTreeNode(); + this.node.addChild(child); + this.node.addChild(otherChild); + assertTrue(child.isFirstSibling()); + } + + @Test + void testIsFirstSiblingOnSecondChild() { + final TreeNode child = new DummyTreeNode(); + final TreeNode otherChild = new DummyTreeNode(); + this.node.addChild(child); + this.node.addChild(otherChild); + assertFalse(otherChild.isFirstSibling()); + } + + @Test + void testGetChildren() { + 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)); + } + + @Test + void testGetChild() { + 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)); + } + + @Test + void testGetParent() { + final TreeNode child = new DummyTreeNode(); + 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 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