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/datatype/interval/TestIntervalDayToSecond.java b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java
new file mode 100644
index 00000000..285b5461
--- /dev/null
+++ b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java
@@ -0,0 +1,47 @@
+package com.exasol.datatype.interval;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestIntervalDayToSecond {
+ // [utest->dsn~exasol.converting-int-to-interval-day-to-second~1]
+ @ParameterizedTest
+ @CsvSource({ //
+ 0L + ", '0:00:00.000'", //
+ 999L + ", '0:00:00.999'", //
+ 59L * 1000 + ", '0:00:59.000'", //
+ 59L * 60 * 1000 + ", '0:59:00.000'", //
+ 23L * 60 * 60 * 1000 + ", '23:00:00.000'", //
+ 999999999L * 24 * 60 * 60 * 1000 + ", '999999999 0:00:00.000'", //
+ 1L * 24 * 60 * 60 * 1000 + 1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1 * 1000 + 1 + ", '1 1:01:01.001'" //
+ })
+ void testofMillis(final long value, final String expected) {
+ assertThat(IntervalDayToSecond.ofMillis(value).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1]
+ @ParameterizedTest
+ @CsvSource({ "'0:0', '0:00:00.000'", //
+ "'1:2:3', '1:02:03.000'", //
+ "'11:22:33.444', '11:22:33.444'", //
+ "'1 22:33:44.555', '1 22:33:44.555'", //
+ "'999999999 22:33:44', '999999999 22:33:44.000'" //
+ })
+ void testParse(final String text, final String expected) {
+ assertThat(IntervalDayToSecond.parse(text).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1]
+ @ParameterizedTest
+ @ValueSource(strings = { "0", ":0", "1.0", "123:45", "12:234", "12:34:567", "12:34:56:7890", //
+ "1000000000 0:0" //
+ })
+ void testParseIllegalInputThrowsException(final String text) {
+ assertThrows(IllegalArgumentException.class, () -> IntervalDayToSecond.parse(text));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java
new file mode 100644
index 00000000..8f25d81e
--- /dev/null
+++ b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java
@@ -0,0 +1,42 @@
+package com.exasol.datatype.interval;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestIntervalYearToMonth {
+ // [utest->dsn~exasol.converting-int-to-interval-year-to-month~1]
+ @ParameterizedTest
+ @CsvSource({ //
+ 0L + ", '0-00'", //
+ 11L + ", '0-11'", //
+ 999999999L * 12 + ", '999999999-00'", //
+ 999999999L * 12 + 11 + ", '999999999-11'", //
+ 1L * 12 + 1 + ", '1-01'" //
+ })
+ void testOfMonths(final long value, final String expected) {
+ assertThat(IntervalYearToMonth.ofMonths(value).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1]
+ @ParameterizedTest
+ @CsvSource({ "'0-0', '0-00'", //
+ "'1-2', '1-02'", //
+ "'22-11', '22-11'", //
+ "'999999999-11', '999999999-11'" //
+ })
+ void testParse(final String text, final String expected) {
+ assertThat(IntervalYearToMonth.parse(text).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1]
+ @ParameterizedTest
+ @ValueSource(strings = { "0", "-0", "0-", "0-123", "1000000000-0" })
+ void testParseIllegalInputThrowsException(final String text) {
+ assertThrows(IllegalArgumentException.class, () -> IntervalYearToMonth.parse(text));
+ }
+}
\ No newline at end of file
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..65e627c8
--- /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.dql.rendering.SelectRenderer;
+import com.exasol.sql.expression.BooleanExpression;
+import com.exasol.sql.expression.rendering.BooleanExpressionRenderer;
+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 SelectRenderer}
+ * @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..e4a19988
--- /dev/null
+++ b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java
@@ -0,0 +1,78 @@
+package com.exasol.hamcrest;
+
+import org.hamcrest.Description;
+
+import com.exasol.sql.Fragment;
+import com.exasol.sql.dml.InsertFragment;
+import com.exasol.sql.dml.rendering.InsertRenderer;
+import com.exasol.sql.dql.SelectFragment;
+import com.exasol.sql.dql.rendering.SelectRenderer;
+import com.exasol.sql.rendering.StringRendererConfig;
+
+/**
+ * This class implements a matcher for the results of rendering SQL statements to text.
+ */
+public class SqlFragmentRenderResultMatcher extends AbstractRenderResultMatcher {
+
+ private final StringRendererConfig config;
+
+ private SqlFragmentRenderResultMatcher(final String expectedText) {
+ super(expectedText);
+ this.config = StringRendererConfig.createDefault();
+ }
+
+ private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final String expectedText) {
+ super(expectedText);
+ this.config = 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) {
+ final Fragment root = fragment.getRoot();
+ if (root instanceof SelectFragment) {
+ final SelectRenderer renderer = new SelectRenderer(this.config);
+ ((SelectFragment) root).accept(renderer);
+ this.renderedText = renderer.render();
+ } else if (root instanceof InsertFragment) {
+ final InsertRenderer renderer = new InsertRenderer(this.config);
+ ((InsertFragment) root).accept(renderer);
+ this.renderedText = renderer.render();
+ } else {
+ throw new UnsupportedOperationException(
+ "Don't know how to render fragment of type\"" + root.getClass().getName() + "\".");
+ }
+ return this.renderedText.equals(this.expectedText);
+ }
+
+ @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 SelectRenderer}
+ * @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/dml/TestInsert.java b/src/test/java/com/exasol/sql/dml/TestInsert.java
new file mode 100644
index 00000000..2894aa20
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dml/TestInsert.java
@@ -0,0 +1,32 @@
+package com.exasol.sql.dml;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.instanceOf;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+
+class TestInsert {
+ private static final String TABLE_NAME = "person";
+ private Insert insert;
+
+ @BeforeEach
+ void beforeEach() {
+ this.insert = StatementFactory.getInstance().insertInto(TABLE_NAME);
+ }
+
+ // [utest->dsn~insert-statements~1]
+ @Test
+ void testInsert() {
+ assertThat(this.insert, instanceOf(Insert.class));
+ }
+
+ // [utest->dsn~insert-statements~1]
+ @Test
+ void testInsertTableName() {
+ assertThat(this.insert.getTableName(), equalTo(TABLE_NAME));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java
new file mode 100644
index 00000000..0d2b462a
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRenderer.java
@@ -0,0 +1,27 @@
+package com.exasol.sql.dml.rendering;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.startsWith;
+
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dml.Insert;
+import com.exasol.sql.rendering.StringRendererConfig;
+
+class TestInsertRenderer {
+ @Test
+ void testCreateWithDefaultConfig() {
+ assertThat(InsertRenderer.create(), instanceOf(InsertRenderer.class));
+ }
+
+ @Test
+ void testCreateWithConfig() {
+ final StringRendererConfig config = StringRendererConfig.builder().lowerCase(true).build();
+ final InsertRenderer renderer = InsertRenderer.create(config);
+ final Insert insert = StatementFactory.getInstance().insertInto("city");
+ insert.accept(renderer);
+ assertThat(renderer.render(), startsWith("insert"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java
new file mode 100644
index 00000000..387da8f2
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java
@@ -0,0 +1,70 @@
+package com.exasol.sql.dml.rendering;
+
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo;
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersWithConfigTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dml.Insert;
+import com.exasol.sql.rendering.StringRendererConfig;
+
+class TestInsertRendering {
+ private static final String PERSON = "person";
+ private Insert insert;
+
+ @BeforeEach
+ void beforeEach() {
+ this.insert = StatementFactory.getInstance().insertInto(PERSON);
+ }
+
+ // [utest->dsn~rendering.sql.insert~1]
+ @Test
+ void testInsert() {
+ assertThat(this.insert, rendersTo("INSERT INTO person"));
+ }
+
+ // [utest->dsn~rendering.sql.configurable-case~1]
+ @Test
+ void testInsertRendersToWithConfig() {
+ assertThat(this.insert,
+ rendersWithConfigTo(StringRendererConfig.builder().lowerCase(true).build(), "insert into person"));
+ }
+
+ // [utest->dsn~rendering.sql.insert~1]
+ @Test
+ void testInsertFields() {
+ assertThat(this.insert.field("a", "b"), rendersTo("INSERT INTO person (a, b)"));
+ }
+
+ // [utest->dsn~rendering.sql.insert~1]
+ // [utest->dsn~values-as-insert-source~1]
+ @Test
+ void testInsertValues() {
+ assertThat(this.insert.values(1, "a"), rendersTo("INSERT INTO person VALUES (1, 'a')"));
+ }
+
+ // [utest->dsn~rendering.sql.insert~1]
+ // [utest->dsn~values-as-insert-source~1]
+ @Test
+ void testInsertValuePlaceholder() {
+ assertThat(this.insert.valuePlaceholder(), rendersTo("INSERT INTO person VALUES (?)"));
+ }
+
+ // [utest->dsn~rendering.sql.insert~1]
+ // [utest->dsn~values-as-insert-source~1]
+ @Test
+ void testInsertValuePlaceholders() {
+ assertThat(this.insert.valuePlaceholders(3), rendersTo("INSERT INTO person VALUES (?, ?, ?)"));
+ }
+
+ // [utest->dsn~rendering.sql.insert~1]
+ // [utest->dsn~values-as-insert-source~1]
+ @Test
+ void testInsertMixedValuesAndPlaceholders() {
+ assertThat(this.insert.values(1).valuePlaceholders(3).values("b", 4),
+ rendersTo("INSERT INTO person VALUES (1, ?, ?, ?, 'b', 4)"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dql/TestSelect.java b/src/test/java/com/exasol/sql/dql/TestSelect.java
new file mode 100644
index 00000000..c1917502
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dql/TestSelect.java
@@ -0,0 +1,29 @@
+package com.exasol.sql.dql;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+
+class TestSelect {
+ private Select select;
+
+ @BeforeEach
+ void beforeEach() {
+ this.select = StatementFactory.getInstance().select();
+ }
+
+ @Test
+ void testLimitTwiceThrowsException() {
+ this.select.limit(1);
+ assertThrows(IllegalStateException.class, () -> this.select.limit(2));
+ }
+
+ @Test
+ void testLimitWithOffsetTwiceThrowsException() {
+ this.select.limit(1, 2);
+ assertThrows(IllegalStateException.class, () -> this.select.limit(2, 3));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestJoinRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestJoinRendering.java
new file mode 100644
index 00000000..714c6049
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dql/rendering/TestJoinRendering.java
@@ -0,0 +1,70 @@
+package com.exasol.sql.dql.rendering;
+
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dql.FromClause;
+import com.exasol.sql.dql.Select;
+
+class TestJoinRendering {
+ private Select select;
+ private FromClause leftTable;
+
+ @BeforeEach()
+ void beforeEach() {
+ this.select = StatementFactory.getInstance().select();
+ this.leftTable = this.select.all().from().table("left_table");
+ }
+
+ @Test
+ void testJoin() {
+ assertThat(this.leftTable.join("right_table", "left_table.foo_id = right_table.foo_id"),
+ rendersTo("SELECT * FROM left_table JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+
+ @Test
+ void testInnerJoin() {
+ assertThat(this.leftTable.innerJoin("right_table", "left_table.foo_id = right_table.foo_id"),
+ rendersTo("SELECT * FROM left_table INNER JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+
+ @Test
+ void testLeftJoin() {
+ assertThat(this.leftTable.leftJoin("right_table", "left_table.foo_id = right_table.foo_id"),
+ rendersTo("SELECT * FROM left_table LEFT JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+
+ @Test
+ void testRightJoin() {
+ assertThat(this.leftTable.rightJoin("right_table", "left_table.foo_id = right_table.foo_id"),
+ rendersTo("SELECT * FROM left_table RIGHT JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+
+ @Test
+ void testFullJoin() {
+ assertThat(this.leftTable.fullJoin("right_table", "left_table.foo_id = right_table.foo_id"),
+ rendersTo("SELECT * FROM left_table FULL JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+
+ @Test
+ void testLeftOuterJoin() {
+ assertThat(this.leftTable.leftOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo(
+ "SELECT * FROM left_table LEFT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+
+ @Test
+ void testRightOuterJoin() {
+ assertThat(this.leftTable.rightOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo(
+ "SELECT * FROM left_table RIGHT OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+
+ @Test
+ void testFullOuterJoin() {
+ assertThat(this.leftTable.fullOuterJoin("right_table", "left_table.foo_id = right_table.foo_id"), rendersTo(
+ "SELECT * FROM left_table FULL OUTER JOIN right_table ON left_table.foo_id = right_table.foo_id"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestLimitRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestLimitRendering.java
new file mode 100644
index 00000000..258a8004
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dql/rendering/TestLimitRendering.java
@@ -0,0 +1,30 @@
+package com.exasol.sql.dql.rendering;
+
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dql.Select;
+
+class TestLimitRendering {
+ private Select select;
+
+ @BeforeEach
+ void beforeEach() {
+ this.select = StatementFactory.getInstance().select();
+ this.select.all().from().table("t");
+ }
+
+ @Test
+ void testLimitCountAfterFrom() {
+ assertThat(this.select.limit(1), rendersTo("SELECT * FROM t LIMIT 1"));
+ }
+
+ @Test
+ void testLimitOffsetCountAfterFrom() {
+ assertThat(this.select.limit(2, 3), rendersTo("SELECT * FROM t LIMIT 2, 3"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java
new file mode 100644
index 00000000..f71fbbd9
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRenderer.java
@@ -0,0 +1,27 @@
+package com.exasol.sql.dql.rendering;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.startsWith;
+
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dql.Select;
+import com.exasol.sql.rendering.StringRendererConfig;
+
+class TestSelectRenderer {
+ @Test
+ void testCreateWithDefaultConfig() {
+ assertThat(SelectRenderer.create(), instanceOf(SelectRenderer.class));
+ }
+
+ @Test
+ void testCreateWithConfig() {
+ final StringRendererConfig config = StringRendererConfig.builder().lowerCase(true).build();
+ final SelectRenderer renderer = SelectRenderer.create(config);
+ final Select select = StatementFactory.getInstance().select();
+ select.accept(renderer);
+ assertThat(renderer.render(), startsWith("select"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java
new file mode 100644
index 00000000..e19c76df
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java
@@ -0,0 +1,95 @@
+package com.exasol.sql.dql.rendering;
+
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo;
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersWithConfigTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dql.Select;
+import com.exasol.sql.expression.BooleanTerm;
+import com.exasol.sql.rendering.StringRendererConfig;
+
+class TestSelectRendering {
+ private Select select;
+
+ @BeforeEach
+ void beforeEach() {
+ this.select = StatementFactory.getInstance().select();
+ }
+
+ // [utest->dsn~rendering.sql.select~1]
+ @Test
+ void testSelectAll() {
+ assertThat(this.select.all(), rendersTo("SELECT *"));
+ }
+
+ // [utest->dsn~rendering.sql.configurable-case~1]
+ @Test
+ void testSelectAllLowerCase() {
+ assertThat(this.select.all(),
+ rendersWithConfigTo(StringRendererConfig.builder().lowerCase(true).build(), "select *"));
+ }
+
+ // [utest->dsn~rendering.sql.select~1]
+ @Test
+ void testSelectFieldNames() {
+ assertThat(this.select.field("a", "b"), rendersTo("SELECT a, b"));
+ }
+
+ // [utest->dsn~rendering.sql.select~1]
+ @Test
+ void testSelectChainOfFieldNames() {
+ assertThat(this.select.field("a", "b").field("c"), rendersTo("SELECT a, b, c"));
+ }
+
+ // [utest->dsn~rendering.sql.select~1]
+ @Test
+ void testSelectFromTable() {
+ assertThat(this.select.all().from().table("persons"), rendersTo("SELECT * FROM persons"));
+ }
+
+ // [utest->dsn~rendering.sql.select~1]
+ @Test
+ void testSelectFromMultipleTable() {
+ assertThat(this.select.all().from().table("table1").table("table2"), rendersTo("SELECT * FROM table1, table2"));
+ }
+
+ // [utest->dsn~rendering.sql.select~1]
+ @Test
+ void testSelectFromTableAs() {
+ assertThat(this.select.all().from().tableAs("table", "t"), rendersTo("SELECT * FROM table AS t"));
+ }
+
+ // [utest->dsn~rendering.sql.select~1]
+ @Test
+ void testSelectFromMultipleTableAs() {
+ assertThat(this.select.all().from().tableAs("table1", "t1").tableAs("table2", "t2"),
+ rendersTo("SELECT * FROM table1 AS t1, table2 AS t2"));
+ }
+
+ // [utest->dsn~select-statement.out-of-order-clauses~1]
+ @Test
+ void testAddClausesInRandomOrder() {
+ assertThat(this.select.limit(1).all().where(BooleanTerm.not("foo")).from().join("A", "A.aa = B.bb").table("B"),
+ rendersTo("SELECT * FROM B JOIN A ON A.aa = B.bb WHERE NOT(foo) LIMIT 1"));
+ }
+
+ // [utest->dsn~rendering.add-double-quotes-for-schema-table-and-column-identifiers~1]
+ @Test
+ void testSelectWithQuotedIdentifiers() {
+ final StringRendererConfig config = StringRendererConfig.builder().quoteIdentifiers(true).build();
+ assertThat(this.select.field("fieldA", "tableA.fieldB", "tableB.*").from().table("schemaA.tableA"),
+ rendersWithConfigTo(config,
+ "SELECT \"fieldA\", \"tableA\".\"fieldB\", \"tableB\".* FROM \"schemaA\".\"tableA\""));
+ }
+
+ @Test
+ void testSelectWithQuotedIdentifiersDoesNotAddExtraQuotes() {
+ final StringRendererConfig config = StringRendererConfig.builder().quoteIdentifiers(true).build();
+ assertThat(this.select.field("\"fieldA\"", "\"tableA\".fieldB"),
+ rendersWithConfigTo(config, "SELECT \"fieldA\", \"tableA\".\"fieldB\""));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestSqlStatementRenderer.java b/src/test/java/com/exasol/sql/dql/rendering/TestSqlStatementRenderer.java
new file mode 100644
index 00000000..9469fe38
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dql/rendering/TestSqlStatementRenderer.java
@@ -0,0 +1,25 @@
+package com.exasol.sql.dql.rendering;
+
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dql.Select;
+
+class TestSqlStatementRenderer {
+ private Select select;
+
+ @BeforeEach
+ void beforeEach() {
+ this.select = StatementFactory.getInstance().select();
+ }
+
+ @Test
+ void testCreateAndRender() {
+ this.select.all().from().table("foo");
+ assertThat(this.select, rendersTo("SELECT * FROM foo"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/dql/rendering/TestWhereRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestWhereRendering.java
new file mode 100644
index 00000000..ff256237
--- /dev/null
+++ b/src/test/java/com/exasol/sql/dql/rendering/TestWhereRendering.java
@@ -0,0 +1,27 @@
+package com.exasol.sql.dql.rendering;
+
+import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo;
+import static com.exasol.sql.expression.BooleanTerm.eq;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.StatementFactory;
+import com.exasol.sql.dql.Select;
+
+class TestWhereRendering {
+ private Select select;
+
+ @BeforeEach
+ void beforeEach() {
+ this.select = StatementFactory.getInstance().select();
+ this.select.all().from().table("person");
+ }
+
+ @Test
+ void testWhere() {
+ assertThat(this.select.where(eq("firstname", "Jane")),
+ rendersTo("SELECT * FROM person WHERE firstname = Jane"));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java
new file mode 100644
index 00000000..2813da2f
--- /dev/null
+++ b/src/test/java/com/exasol/sql/expression/TestBooleanTerm.java
@@ -0,0 +1,90 @@
+package com.exasol.sql.expression;
+
+import static com.exasol.sql.expression.BooleanTerm.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class TestBooleanTerm {
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationYieldsAnd() {
+ final BooleanExpression term = BooleanTerm.operation("and", not("a"), not("b"));
+ assertThat(term, instanceOf(And.class));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromUpperCaseYieldsAnd() {
+ assertThat(BooleanTerm.operation("AND", not("a"), not("b")), instanceOf(And.class));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationYieldsOr() {
+ assertThat(BooleanTerm.operation("or", not("a"), not("b")), instanceOf(Or.class));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromUpperCaseYieldsOr() {
+ assertThat(BooleanTerm.operation("OR", not("a"), not("b")), instanceOf(Or.class));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationYieldsNot() {
+ assertThat(BooleanTerm.operation("not", not("a")), instanceOf(Not.class));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromUpperCaseYieldsNot() {
+ assertThat(BooleanTerm.operation("NOT", not("a")), instanceOf(Not.class));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromUnknownOperatorThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("illegal", not("a")));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromNotWithMoreOrLessThanOneOperandThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("not", not("a"), not("b")));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromUpperCaseUnknownOperatorThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("ILLEGAL", not("a")));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromUpperCaseNotWithMoreOrLessThanOneOperandThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> BooleanTerm.operation("NOT", not("a"), not("b")));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOperationFromNullOperatorThrowsException() {
+ assertThrows(NullPointerException.class, () -> BooleanTerm.operation(null, not("a"), not("b")));
+ }
+
+ // [utest->dsn~boolean-operation.comparison.constructing-from-strings~1]
+ @Test
+ void testOperationFromComparisonOperatorString() {
+ assertThat(BooleanTerm.compare("a", "<>", "b"), instanceOf(Comparison.class));
+ }
+
+ // [utest->dsn~boolean-operation.comparison.constructing-from-enum~1]
+ @Test
+ void testOperationFromComparisonOperatorEnum() {
+ assertThat(BooleanTerm.compare("a", ComparisonOperator.NOT_EQUAL, "b"), instanceOf(Comparison.class));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java
new file mode 100644
index 00000000..e4c501af
--- /dev/null
+++ b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java
@@ -0,0 +1,26 @@
+package com.exasol.sql.expression;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+class TestComparisonOperator {
+ @Test
+ void testToString() {
+ assertThat(ComparisonOperator.EQUAL.toString(), equalTo("="));
+ }
+
+ // [utest->dsn~boolean-operation.comparison.constructing-from-strings~1]
+ @Test
+ void testOfSymbol() {
+ assertThat(ComparisonOperator.ofSymbol("<>"), equalTo(ComparisonOperator.NOT_EQUAL));
+ }
+
+ // [utest->dsn~boolean-operation.comparison.constructing-from-strings~1]
+ @Test
+ void testOfUnknownSymbolThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> ComparisonOperator.ofSymbol("§"));
+ }
+}
\ 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..6faaa2b1
--- /dev/null
+++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java
@@ -0,0 +1,121 @@
+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 static org.junit.jupiter.api.Assertions.assertAll;
+
+import org.junit.jupiter.api.Test;
+
+import com.exasol.sql.expression.BooleanExpression;
+import com.exasol.sql.expression.ComparisonOperator;
+import com.exasol.sql.rendering.StringRendererConfig;
+
+class TestBooleanExpressionRenderer {
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testUnaryNotWithLiteral() {
+ final BooleanExpression expression = not("a");
+ assertThat(expression, rendersTo("NOT(a)"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testUnaryNotWithExpression() {
+ final BooleanExpression expression = not(not("a"));
+ assertThat(expression, rendersTo("NOT(NOT(a))"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testAndWithLiterals() {
+ final BooleanExpression expression = and("a", "b", "c");
+ assertThat(expression, rendersTo("a AND b AND c"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testAndNestedComparisons() {
+ final BooleanExpression expression = and(compare("a", ComparisonOperator.EQUAL, "b"),
+ compare("c", ComparisonOperator.NOT_EQUAL, "d"));
+ assertThat(expression, rendersTo("(a = b) AND (c <> d)"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testAndWithLeftLiteralAndRightExpression() {
+ final BooleanExpression expression = and("a", not("b"));
+ assertThat(expression, rendersTo("a AND NOT(b)"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testAndWithLeftExpressionAndRightLiteral() {
+ final BooleanExpression expression = and(not("a"), "b");
+ assertThat(expression, rendersTo("NOT(a) AND b"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOrWithLiterals() {
+ final BooleanExpression expression = or("a", "b", "c");
+ assertThat(expression, rendersTo("a OR b OR c"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testoRWithLeftLiteralAndRightExpression() {
+ final BooleanExpression expression = or("a", not("b"));
+ assertThat(expression, rendersTo("a OR NOT(b)"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOrWithLeftExpressionAndRightLiteral() {
+ final BooleanExpression expression = or(not("a"), "b");
+ assertThat(expression, rendersTo("NOT(a) OR b"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testOrWhitNestedAnd() {
+ final BooleanExpression expression = or(and(not("a"), "b"), and("c", "d"));
+ assertThat(expression, rendersTo("(NOT(a) AND b) OR (c AND d)"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testAndWhitNestedOr() {
+ final BooleanExpression expression = and(or(not("a"), "b"), or("c", "d"));
+ assertThat(expression, rendersTo("(NOT(a) OR b) AND (c OR d)"));
+ }
+
+ // [utest->dsn~boolean-operators~1]
+ @Test
+ void testAndWhitNestedOrInLowercase() {
+ final BooleanExpression expression = and(or(not("a"), "b"), or("c", "d"));
+ final StringRendererConfig config = StringRendererConfig.builder().lowerCase(true).build();
+ assertThat(expression, rendersWithConfigTo(config, "(not(a) or b) and (c or d)"));
+ }
+
+ // [utest->dsn~comparison-operations~1]
+ @Test
+ void testComparisonFromSymbol() {
+ final BooleanExpression expression = compare("a", ">=", "b");
+ assertThat(expression, rendersTo("a >= b"));
+ }
+
+ // [utest->dsn~comparison-operations~1]
+ @Test
+ void testComparisonOperators() {
+ assertAll( //
+ () -> assertThat("equal", eq("a", "b"), rendersTo("a = b")), //
+ () -> assertThat("not equal", ne("a", "b"), rendersTo("a <> b")), //
+ () -> assertThat("not equal", lt("a", "b"), rendersTo("a < b")), //
+ () -> assertThat("not equal", gt("a", "b"), rendersTo("a > b")), //
+ () -> assertThat("not equal", le("a", "b"), rendersTo("a <= b")), //
+ () -> assertThat("not equal", ge("a", "b"), rendersTo("a >= b")) //
+ );
+ }
+}
\ No newline at end of file
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/src/uml/diagrams/class/cl_fragments.plantuml b/src/uml/diagrams/class/cl_fragments.plantuml
new file mode 100644
index 00000000..1f6fef13
--- /dev/null
+++ b/src/uml/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/src/uml/diagrams/class/cl_select_fragment_hierarchy.plantuml b/src/uml/diagrams/class/cl_select_fragment_hierarchy.plantuml
new file mode 100644
index 00000000..95b6777f
--- /dev/null
+++ b/src/uml/diagrams/class/cl_select_fragment_hierarchy.plantuml
@@ -0,0 +1,12 @@
+@startuml
+!include ../exasol.skin
+
+Select *-- "*" Field
+Select *-- "0..1" FromClause
+Select *-- "0..1" LimitClause
+Select *-- "0..1" WhereClause
+FromClause *-- "*" Table
+FromClause *-- "*" Join
+WhereClause *-- BooleanExpression
+BooleanExpression *-- "0..1" BooleanExpression
+@enduml
\ No newline at end of file
diff --git a/src/uml/diagrams/class/cl_visitor.plantuml b/src/uml/diagrams/class/cl_visitor.plantuml
new file mode 100644
index 00000000..6850a479
--- /dev/null
+++ b/src/uml/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/src/uml/diagrams/exasol.skin b/src/uml/diagrams/exasol.skin
new file mode 100644
index 00000000..7c79025c
--- /dev/null
+++ b/src/uml/diagrams/exasol.skin
@@ -0,0 +1,40 @@
+@startuml
+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
+@enduml
\ 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