+ * 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 Optionaltrue 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 Listnull 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
+ * 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 Listtrue 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