diff --git a/README.md b/README.md index 760004fa..3fa252e1 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,8 @@ import com.exasol.sql.SqlStatement; import com.exasol.sql.rendering.SqlStatementRenderer; SqlStatement statement = StatementFactory.getInstance() - .select() - .field("firstname", "lastname") - .from("person"); + .select().field("firstname", "lastname") + .from().table("person"); String statementText = SqlStatementRenderer.render(statement); ``` diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 00000000..bf237a2a --- /dev/null +++ b/doc/design.md @@ -0,0 +1,78 @@ +# Software Architectural Design -- Exasol SQL Statement Builder + +## Building Block View + +### Select Statement +`dsn~dql-statement~1` + +The Data Query Language (DQL) building block is responsible for managing `SELECT` statements. + +## Runtime View + +### Building Select Statements + +#### Accessing the Clauses That Make Up a SELECT Statement +`dsn~select-statement.accessing-clauses~1` + +The DQL statement component allows getting the following clauses, provided that they already exist: + +* `FROM` clause +* `WHERE` clause + +Covers: + +* `req~statement-structure.step-wise~1` + +Needs: impl, utest + +Tags: Select Statement Builder + +### Building Boolean Expressions + +#### Constructing Boolean Comparison Operations From Operator Strings +`dsn~boolean-operation.comparison.constructing-from-strings~1` + +The Boolean Expression builder allows creating expression objects from a string representing the comparison operator (options listed below) and a list of operands. + +* `>` +* `<` +* `=` +* `>=` +* `<=` +* `<>` + +Covers: + +* `req~boolean-operators~1` + +Needs: impl, utest + +#### Constructing Boolean Comparison Operations From Operator Enumeration +`dsn~boolean-operation.comparison.constructing-from-enum~1` + +The Boolean Expression builder allows creating expression objects from a enumeration of comparison operators. +Covers: + +* `req~boolean-operators~1` + +Needs: impl, utest + +#### Forwarded Requirements + +* `dsn --> impl, utest : req~comparison-operations~1` +* `dsn --> impl, utest : req~boolean-operators~1` + +### Building INSERT Statements + +#### Forwarded Requirements + +* `dsn --> impl, utest: req~insert-statements~1` +* `dsn --> impl, utest: req~values-as-insert-source~1` + +### Rendering Statements + +#### Forwarded Requirements + +* `dsn --> req~rendering.sql.configurable-case~1` +* `dsn --> impl, utest: req~rendering.sql.select~1` +* `dsn --> impl, utest: req~rendering.sql.insert~1` diff --git a/doc/system_requirements.md b/doc/system_requirements.md index b95ee9db..9e7a0562 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -1,7 +1,218 @@ +# System Requirement Specification -- Exasol SQL Statement Builder + +## Introduction + +The Exasol SQL Statement Builder (ESB) is a [Java](https://java.com) library that allows you to define SQL statements in a [internal Domain-specific language (DSL)](https://en.wikipedia.org/wiki/Domain-specific_language#Usage_patterns). This means it uses standard Java language features to create a DSL. + +The project uses agile software development process, so this document contains only the portion of requirement necessary for the current iteration. + +### Goals + +The goals of the ESB are: + +* Abstraction from the SQL text representation +* Compile-time error detection (where possible) +* Extensibility (for different SQL dialects, validators and renderers) +* User friendly API + +### Terms and Abbreviations + +* ESB: Exasol SQL Statement Builder +* Renderer: extension that turns the abstract representation into a different one (most notably an SQL string) +* Validator: extension that validates a statements structure and content + +### Notation + +#### Augmented Backus–Naur Form (ABNF) + +This document uses Augmented Backus–Naur Form (ABNF) for syntax definitions. + +#### ABNF Terminals + +This subsection list the ABNF terminals used in this document. Terminals are ABNF rules that cannot be split further down, like string literals for example. + +##### General Terminals + + COMMA = "," + + L-BRACKET = "(" + + R-BRACKET = ")" + +##### Operator Terminals + + EQUAL-OPERATOR = "=" + + NOT-EQUAL-OPERATOR = "<>" + + LESS-THAN-OPERATOR = "<" + + LESS-THAN-OR-EQUAL-OPERATOR = "<=" + + GREATER-THAN-OPERATOR = ">" + + GREATER-THAN-OR-EQUAL-OPERATOR = ">=" + +## Features + +### Statement Definition +`feat~statement-definition~1` + +The ESB allows users to define SQL statements in abstract form. + +Needs: req + +### SQL String Rendering +`feat~sql-string-rendering~1` + +The ESB renders abstract SQL statements into strings. + +Rationale: + +The SQL strings are necessary input for executing queries (e.g. with JDBC). + +Needs: req + +### Compile-time Error Checking +`feat~compile-time-error-checking~1` + +ESB reports detectable errors at compile-time. + +Rationale: + +Making sure at compile time that illegal constructs do not compile make the resulting code safer and reduces debugging efforts. + +Needs: req + +## Functional Requirements + +### Statement Structure + +#### Building the Statement Structure Step-wise +`req~statement-structure.step-wise~1` + +ESB lets users build the statement structure step-by-step. + +Rationale: + +This is necessary since complex statements are usually build as a result of multi-layered decision trees and parts of the statements are constructed in different places. + +Covers: + +* [feat~statment-definition~1](#statement-definition) + +Needs: dsn + +### General SQL Construction + +#### Comparison Operations +`req~comparison-operations~1` + +ESB supports the following comparison operations: + + operation = left-operand operator right-operand + + left-operand = field-reference / literal + + operator = equal-operator / not-equal-operator / greater-operator / less-than-operator / + greater-or-equal-operator / less-than-or-equal-operator + + right-operand = field-reference / literal + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +#### Boolean Operators +`req~boolean-operators~1` + +ESB supports the following boolean operators: `AND`, `OR` and `NOT` + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +### Data Manipulation Language + +#### INSERT Statements +`req~insert-statements~1` + +ESB supports the following insert statement: + + insert-statement = "INSERT INTO" table-reference [insert-columns] + insert-source + + table-reference = table [AS table-alias] + + insert-columns = L-BRACKET column *( COMMA column ) R-BRACKET + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +#### Values as INSERT SOURCE +`req~values-as-insert-source~1` + +ESB supports a list of explicit values as INSERT source: + + insert-source =/ "VALUES" L-BRACKET ( value-expression / + "DEFAULT" ) R-BRACKET + +Covers: + +* [feat~statement-definition~1](#statement-definition) + +Needs: dsn + +### SQL String Rendering + +#### Configurable Case Rendering +`req~rendering.sql.configurable-case~1` + +Users can choose whether the keywords in the SQL string should be rendered in upper case and lower case. + +Rationale: + +While keyword case is mostly an esthetic point, different users still have different preferences. + +Covers: + +* [feat~sql-string-rendering~1](#sql-string-rendering) + +Needs: dsn + * Upper case / lower case * One line / pretty -SQL features: +#### SELECT Statement Rendering +`req~rendering.sql.select~1` + +ESB renders abstract `SELECT` statements into SQL query strings. + +Covers: + +* [feat~sql-string-rendering~1](#sql-string-rendering) + +Needs: dsn + +#### INSERT Statement Rendering +`req~rendering.sql.insert~1` + +ESB renders abstract `INSERT` statements into SQL data manipulation language strings. + +Covers: + +* [feat~sql-string-rendering~1](#sql-string-rendering) + +Needs: dsn + +### TODO --- diff --git a/model/diagrams/exasol.skin b/model/diagrams/exasol.skin deleted file mode 100644 index aaf51dc4..00000000 --- a/model/diagrams/exasol.skin +++ /dev/null @@ -1,38 +0,0 @@ -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/src/main/java/com/exasol/sql/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java index 64c47657..374af2d6 100644 --- a/src/main/java/com/exasol/sql/AbstractFragment.java +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -1,26 +1,22 @@ package com.exasol.sql; -import com.exasol.util.AbstractTreeNode; -import com.exasol.util.TreeNode; - /** - * This class provides an abstract base for SQL statement fragments. It also - * keeps track of the relationships to other fragments. - * - * @param the type of the concrete class implementing the missing parts. + * Common base class for SQL statement fragments */ -public abstract class AbstractFragment extends AbstractTreeNode implements Fragment { - protected AbstractFragment() { - super(); +public abstract class AbstractFragment implements Fragment { + private final Fragment root; + + /** + * Create an instance of an SQL fragment + * + * @param root root SQL statement this fragment belongs to. + */ + public AbstractFragment(final Fragment root) { + this.root = root; } @Override - public void accept(final FragmentVisitor visitor) { - acceptConcrete(visitor); - for (final TreeNode child : this.getChildren()) { - ((Fragment) child).accept(visitor); - } + public Fragment getRoot() { + return (this.root == null) ? this : this.root; } - - protected abstract void acceptConcrete(final FragmentVisitor visitor); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/Field.java b/src/main/java/com/exasol/sql/Field.java new file mode 100644 index 00000000..939ec369 --- /dev/null +++ b/src/main/java/com/exasol/sql/Field.java @@ -0,0 +1,33 @@ +package com.exasol.sql; + +/** + * This class represents a table field in an SQL statement. + */ +public class Field extends AbstractFragment implements GenericFragment { + private final String name; + + /** + * Create a new instance of a {@link Field} + * + * @param root root SQL statement + * @param name field name + */ + public Field(final Fragment root, final String name) { + super(root); + this.name = name; + } + + /** + * Get the field name + * + * @return field name + */ + public String getName() { + return this.name; + } + + @Override + public void accept(final FragmentVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/Fragment.java b/src/main/java/com/exasol/sql/Fragment.java index 2269bf73..deaa007c 100644 --- a/src/main/java/com/exasol/sql/Fragment.java +++ b/src/main/java/com/exasol/sql/Fragment.java @@ -1,7 +1,14 @@ package com.exasol.sql; -import com.exasol.util.TreeNode; - -public interface Fragment extends TreeNode { - public void accept(FragmentVisitor visitor); +/** + * This is the common interface for all fragments of SQL statements. Fragments can be clauses like the WHERE clause of + * an SELECT statement but also lower level concepts like boolean expressions. + */ +public interface Fragment { + /** + * Get the root statement of this SQL fragment + * + * @return the root fragment + */ + public Fragment getRoot(); } diff --git a/src/main/java/com/exasol/sql/FragmentVisitor.java b/src/main/java/com/exasol/sql/FragmentVisitor.java index 33fad8b1..e0816758 100644 --- a/src/main/java/com/exasol/sql/FragmentVisitor.java +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -1,22 +1,10 @@ package com.exasol.sql; -import com.exasol.sql.dql.*; - /** * This interface represents a visitor for SQL statement fragments. */ public interface FragmentVisitor { - public void visit(final Select select); - public void visit(final Field field); - public void visit(FromClause fromClause); - public void visit(Table table); - - public void visit(Join join); - - public void visit(BooleanValueExpression booleanValueExpression); - - public void visit(LimitClause limitClause); } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/GenericFragment.java b/src/main/java/com/exasol/sql/GenericFragment.java new file mode 100644 index 00000000..2e6859c8 --- /dev/null +++ b/src/main/java/com/exasol/sql/GenericFragment.java @@ -0,0 +1,14 @@ +package com.exasol.sql; + +/** + * Common interface for all SQL statement fragments which are used in multiple types of statements, like tables and + * fields. + */ +public interface GenericFragment extends Fragment { + /** + * Accept a generic fragment visitor + * + * @param visitor visitor + */ + public void accept(final FragmentVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/StatementFactory.java b/src/main/java/com/exasol/sql/StatementFactory.java index 865ea05c..87cdfe74 100644 --- a/src/main/java/com/exasol/sql/StatementFactory.java +++ b/src/main/java/com/exasol/sql/StatementFactory.java @@ -1,5 +1,6 @@ package com.exasol.sql; +import com.exasol.sql.dml.Insert; import com.exasol.sql.dql.Select; /** @@ -32,4 +33,14 @@ private StatementFactory() { public Select select() { return new Select(); } + + /** + * Create a {@link Insert} statement + * + * @param tableName name of the table into which to insert the data + * @return a new instance of a {@link Insert} statement + */ + public Insert insertInto(final String tableName) { + return new Insert(tableName); + } } diff --git a/src/main/java/com/exasol/sql/dql/Table.java b/src/main/java/com/exasol/sql/Table.java similarity index 67% rename from src/main/java/com/exasol/sql/dql/Table.java rename to src/main/java/com/exasol/sql/Table.java index 486142f3..009476ee 100644 --- a/src/main/java/com/exasol/sql/dql/Table.java +++ b/src/main/java/com/exasol/sql/Table.java @@ -1,24 +1,22 @@ -package com.exasol.sql.dql; +package com.exasol.sql; 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 { +public class Table extends AbstractFragment implements TableReference, GenericFragment { private final String name; private final Optional as; /** - * Create a new {@link Table} + * Create a new {@link Table} with a name and an alias * + * @param root SQL statement this table belongs to * @param name table name */ - public Table(final String name) { - super(); + public Table(final Fragment root, final String name) { + super(root); this.name = name; this.as = Optional.empty(); } @@ -26,11 +24,12 @@ public Table(final String name) { /** * Create a new {@link Table} with a name and an alias * + * @param root SQL statement this table belongs to * @param name table name * @param as table alias */ - public Table(final String name, final String as) { - super(); + public Table(final Fragment root, final String name, final String as) { + super(root); this.name = name; this.as = Optional.of(as); } @@ -54,7 +53,7 @@ public Optional getAs() { } @Override - protected void acceptConcrete(final FragmentVisitor visitor) { + public void accept(final FragmentVisitor visitor) { visitor.visit(this); } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/TableReference.java b/src/main/java/com/exasol/sql/TableReference.java new file mode 100644 index 00000000..beb8dcc4 --- /dev/null +++ b/src/main/java/com/exasol/sql/TableReference.java @@ -0,0 +1,4 @@ +package com.exasol.sql; + +public interface TableReference extends Fragment { +} diff --git a/src/main/java/com/exasol/sql/dml/Insert.java b/src/main/java/com/exasol/sql/dml/Insert.java new file mode 100644 index 00000000..2c83517e --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/Insert.java @@ -0,0 +1,56 @@ +package com.exasol.sql.dml; + +import com.exasol.sql.*; +import com.exasol.sql.dql.Select; + +/** + * This class implements an SQL {@link Select} statement + */ +public class Insert extends AbstractFragment implements SqlStatement, InsertFragment { + private final Table table; + private InsertFields fields; + + /** + * Create a new instance of an {@link Insert} statement + * + * @param tableName name of the table into which the data should be inserted + */ + public Insert(final String tableName) { + super(null); + this.table = new Table(this, tableName); + } + + /** + * Define fields into which should be inserted + * + * @param names field names + * @return this for fluent programming + */ + public synchronized Insert field(final String... names) { + if (this.fields == null) { + this.fields = new InsertFields(this); + } + this.fields.add(names); + return this; + } + + /** + * Get the name of the table into which data should be inserted + * + * @return table name + */ + public String getTableName() { + return this.table.getName(); + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + if (this.table != null) { + this.table.accept(visitor); + } + if (this.fields != null) { + this.fields.accept(visitor); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertFields.java b/src/main/java/com/exasol/sql/dml/InsertFields.java new file mode 100644 index 00000000..f583c005 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertFields.java @@ -0,0 +1,45 @@ +package com.exasol.sql.dml; + +import java.util.ArrayList; +import java.util.List; + +import com.exasol.sql.*; + +public class InsertFields extends AbstractFragment implements InsertFragment { + private final List fields = new ArrayList<>(); + + /** + * Create an new instance of {@link InsertFields} + * + * @param root + */ + public InsertFields(final SqlStatement root) { + super(root); + } + + /** + * Define fields into which should be inserted + * + * @param names field names + */ + void add(final String... names) { + for (final String name : names) { + this.fields.add(new Field(getRoot(), name)); + } + } + + @Override + public Fragment getRoot() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + for (final Field field : this.fields) { + field.accept(visitor); + } + visitor.leave(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertFragment.java b/src/main/java/com/exasol/sql/dml/InsertFragment.java new file mode 100644 index 00000000..d30ba380 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertFragment.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dml; + +import com.exasol.sql.Fragment; + +/** + * This is the common interface for all fragments of a SELECT statement. + */ +public interface InsertFragment extends Fragment { + /** + * Accept a visitor (e.g. a renderer or validator) + * + * @param visitor visitor to accept + */ + public void accept(InsertVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/InsertVisitor.java b/src/main/java/com/exasol/sql/dml/InsertVisitor.java new file mode 100644 index 00000000..71731729 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertVisitor.java @@ -0,0 +1,11 @@ +package com.exasol.sql.dml; + +import com.exasol.sql.FragmentVisitor; + +public interface InsertVisitor extends FragmentVisitor { + public void visit(Insert insert); + + public void visit(InsertFields insertFields); + + public void leave(InsertFields insertFields); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java new file mode 100644 index 00000000..02655c53 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -0,0 +1,47 @@ +package com.exasol.sql.dml.rendering; + +import com.exasol.sql.Field; +import com.exasol.sql.Table; +import com.exasol.sql.dml.*; +import com.exasol.sql.rendering.AbstractFragmentRenderer; +import com.exasol.sql.rendering.StringRendererConfig; + +public class InsertRenderer extends AbstractFragmentRenderer implements InsertVisitor { + /** + * Create a new {@link InsertRenderer} with custom render settings. + * + * @param config render configuration settings + */ + public InsertRenderer(final StringRendererConfig config) { + super(config); + } + + @Override + public void visit(final Insert insert) { + appendKeyWord("INSERT INTO "); + setLastVisited(insert); + } + + @Override + public void visit(final Table table) { + append(table.getName()); + setLastVisited(table); + } + + @Override + public void visit(final Field field) { + appendCommaWhenNeeded(field); + append(field.getName()); + setLastVisited(field); + } + + @Override + public void visit(final InsertFields insertFields) { + append(" ("); + } + + @Override + public void leave(final InsertFields insertFields) { + append(")"); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java b/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java deleted file mode 100644 index e95d16eb..00000000 --- a/src/main/java/com/exasol/sql/dql/BooleanValueExpression.java +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index 89770a2a..00000000 --- a/src/main/java/com/exasol/sql/dql/Field.java +++ /dev/null @@ -1,26 +0,0 @@ -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 deleted file mode 100644 index be560c0a..00000000 --- a/src/main/java/com/exasol/sql/dql/FieldDefinition.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.exasol.sql.dql; - -import com.exasol.sql.Fragment; - -public interface FieldDefinition extends Fragment { - -} diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java index 38b8254d..bf952297 100644 --- a/src/main/java/com/exasol/sql/dql/FromClause.java +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -1,17 +1,24 @@ package com.exasol.sql.dql; -import com.exasol.sql.AbstractFragment; -import com.exasol.sql.FragmentVisitor; +import java.util.ArrayList; +import java.util.List; + +import com.exasol.sql.*; /** * This class represents the FROM clause of an SQL SELECT statement. */ -public class FromClause extends AbstractFragment { +public class FromClause extends AbstractFragment implements SelectFragment { + private final List tables = new ArrayList<>(); + private final List joins = new ArrayList<>(); + /** * Create a new instance of a {@link FromClause} + * + * @param root root SQL statement this FROM clause belongs to */ - public FromClause() { - super(); + public FromClause(final Fragment root) { + super(root); } /** @@ -20,8 +27,8 @@ public FromClause() { * @param name table name * @return new instance */ - public FromClause from(final String name) { - addChild(new Table(name)); + public FromClause table(final String name) { + this.tables.add(new Table(getRoot(), name)); return this; } @@ -32,8 +39,8 @@ public FromClause from(final String name) { * @param as table alias * @return new instance */ - public FromClause fromTableAs(final String name, final String as) { - addChild(new Table(name, as)); + public FromClause tableAs(final String name, final String as) { + this.tables.add(new Table(getRoot(), name, as)); return this; } @@ -45,7 +52,7 @@ public FromClause fromTableAs(final String name, final String as) { * @return parent FROM clause */ public FromClause join(final String name, final String specification) { - addChild(new Join(JoinType.DEFAULT, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.DEFAULT, name, specification)); return this; } @@ -57,7 +64,7 @@ public FromClause join(final String name, final String specification) { * @return parent FROM clause */ public FromClause innerJoin(final String name, final String specification) { - addChild(new Join(JoinType.INNER, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.INNER, name, specification)); return this; } @@ -69,7 +76,7 @@ public FromClause innerJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause leftJoin(final String name, final String specification) { - addChild(new Join(JoinType.LEFT, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.LEFT, name, specification)); return this; } @@ -81,7 +88,7 @@ public FromClause leftJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause rightJoin(final String name, final String specification) { - addChild(new Join(JoinType.RIGHT, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.RIGHT, name, specification)); return this; } @@ -93,7 +100,7 @@ public FromClause rightJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause fullJoin(final String name, final String specification) { - addChild(new Join(JoinType.FULL, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.FULL, name, specification)); return this; } @@ -105,7 +112,7 @@ public FromClause fullJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause leftOuterJoin(final String name, final String specification) { - addChild(new Join(JoinType.LEFT_OUTER, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.LEFT_OUTER, name, specification)); return this; } @@ -117,7 +124,7 @@ public FromClause leftOuterJoin(final String name, final String specification) { * @return parent FROM clause */ public FromClause rightOuterJoin(final String name, final String specification) { - addChild(new Join(JoinType.RIGHT_OUTER, name, specification)); + this.joins.add(new Join(getRoot(), JoinType.RIGHT_OUTER, name, specification)); return this; } @@ -129,37 +136,18 @@ public FromClause rightOuterJoin(final String name, final String specification) * @return parent FROM clause */ public FromClause fullOuterJoin(final String name, final String specification) { - addChild(new Join(JoinType.FULL_OUTER, name, specification)); + this.joins.add(new Join(getRoot(), 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) { + public void accept(final SelectVisitor visitor) { visitor.visit(this); + for (final Table table : this.tables) { + table.accept(visitor); + } + for (final Join join : this.joins) { + join.accept(visitor); + } } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Join.java b/src/main/java/com/exasol/sql/dql/Join.java index 7eaadf2f..bfed5d67 100644 --- a/src/main/java/com/exasol/sql/dql/Join.java +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -1,11 +1,12 @@ package com.exasol.sql.dql; -import com.exasol.sql.*; +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.Fragment; /** * This class implements the {@link Join} part of a WHERE clause. */ -public class Join extends AbstractFragment implements Fragment { +public class Join extends AbstractFragment implements SelectFragment { private final JoinType type; private final String name; private final String specification; @@ -13,12 +14,13 @@ public class Join extends AbstractFragment implements Fragment { /** * Create a new {@link Join} instance * + * @param root SQL statement this JOIN belongs to * @param type type of join (e.g. INNER, LEFT or RIGHT OUTER) * @param name name of the table to be joined * @param specification join specification (e.g. ON or USING) */ - public Join(final JoinType type, final String name, final String specification) { - super(); + public Join(final Fragment root, final JoinType type, final String name, final String specification) { + super(root); this.type = type; this.name = name; this.specification = specification; @@ -52,7 +54,7 @@ public String getSpecification() { } @Override - protected void acceptConcrete(final FragmentVisitor visitor) { + public void accept(final SelectVisitor visitor) { visitor.visit(this); } -} +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java index 741d15b9..08be0050 100644 --- a/src/main/java/com/exasol/sql/dql/LimitClause.java +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -1,45 +1,44 @@ package com.exasol.sql.dql; import com.exasol.sql.AbstractFragment; -import com.exasol.sql.FragmentVisitor; +import com.exasol.sql.SqlStatement; /** - * This class represents the limit clause of an SQL statement. It lets you - * choose offset and / or count of rows to be handed back in the result. + * This class represents the limit clause of an SQL statement. It lets you choose offset and / or count of rows to be + * handed back in the result. */ -public class LimitClause extends AbstractFragment { +public class LimitClause extends AbstractFragment implements SelectFragment { private final int count; private final int offset; /** * Create a new instance of a {@link LimitClause} * + * @param root SQL statement this LIMIT clause belongs to + * * @param offset index of the first row to be included in the query result * * @param count maximum number of rows to be included in the query result */ - public LimitClause(final int count) { - this(0, count); + public LimitClause(final SqlStatement root, final int count) { + this(root, 0, count); } /** * Create a new instance of a {@link LimitClause} * + * @param root SQL statement this LIMIT clause belongs to + * * @param offset index of the first row to be included in the query result * * @param count maximum number of rows to be included in the query result */ - public LimitClause(final int offset, final int count) { - super(); + public LimitClause(final SqlStatement root, final int offset, final int count) { + super(root); this.offset = offset; this.count = count; } - @Override - protected void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); - } - /** * Get the offset row for the limit * @@ -66,4 +65,9 @@ public int getCount() { public boolean hasOffset() { return this.offset > 0; } + + @Override + public void accept(final SelectVisitor visitor) { + visitor.visit(this); + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/Select.java b/src/main/java/com/exasol/sql/dql/Select.java index 2f384302..3f54de0e 100644 --- a/src/main/java/com/exasol/sql/dql/Select.java +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -1,17 +1,25 @@ package com.exasol.sql.dql; +import java.util.ArrayList; +import java.util.List; + import com.exasol.sql.*; import com.exasol.sql.expression.BooleanExpression; /** * This class implements an SQL {@link Select} statement */ -public class Select extends AbstractFragment implements SqlStatement { +public class Select extends AbstractFragment implements SqlStatement, SelectFragment { + private final List fields = new ArrayList<>(); + private FromClause fromClause = null; + private WhereClause whereClause = null; + private LimitClause limitClause = null; + /** * Create a new instance of a {@link Select} */ public Select() { - super(); + super(null); } /** @@ -20,7 +28,7 @@ public Select() { * @return this instance for fluent programming */ public Select all() { - addChild(Field.all()); + this.fields.add(new Field(this, "*")); return this; } @@ -32,50 +40,83 @@ public Select all() { */ public Select field(final String... names) { for (final String name : names) { - addChild(new Field(name)); + this.fields.add(new Field(this, name)); } return this; } /** - * Add a boolean value expression + * Get the {@link FromClause} of this select statement * - * @param expression boolean value expression - * @return this instance for fluent programming + * @return from clause */ - public Select value(final BooleanExpression expression) { - addChild(new BooleanValueExpression(expression)); - return this; + public synchronized FromClause from() { + if (this.fromClause == null) { + this.fromClause = new FromClause(this); + } + return this.fromClause; } - @Override - public void acceptConcrete(final FragmentVisitor visitor) { - visitor.visit(this); + /** + * Create a new full outer {@link LimitClause} + * + * @param count maximum number of rows to be included in query result + * @return new instance + * @throws IllegalStateException if a limit clause already exists + */ + public synchronized Select limit(final int count) { + if (this.limitClause != null) { + throw new IllegalStateException( + "Tried to create a LIMIT clause in a SELECT statement that already had one."); + } + this.limitClause = new LimitClause(this, count); + return this; } /** - * Add a {@link FromClause} to the statement with a table identified by its name + * Create a new full outer {@link LimitClause} * - * @param name table reference name - * @return the FROM clause + * @param offset index of the first row in the query result + * @param count maximum number of rows to be included in query result + * @return this as = table.getAs(); + if (as.isPresent()) { + appendKeyWord(" AS "); + append(as.get()); + } + setLastVisited(table); + } + + @Override + public void visit(final Join join) { + final JoinType type = join.getType(); + if (type != JoinType.DEFAULT) { + appendSpace(); + appendKeyWord(type.toString()); + } + appendKeyWord(" JOIN "); + append(join.getName()); + appendKeyWord(" ON "); + append(join.getSpecification()); + setLastVisited(join); + } + + @Override + public void visit(final WhereClause whereClause) { + appendKeyWord(" WHERE "); + appendRenderedExpression(whereClause.getExpression()); + setLastVisited(whereClause); + } + + @Override + public void visit(final LimitClause limit) { + appendKeyWord(" LIMIT "); + if (limit.hasOffset()) { + append(limit.getOffset()); + appendKeyWord(", "); + } + append(limit.getCount()); + setLastVisited(limit); + } + + /** + * Create a renderer for the given {@link Fragment} and render it. + * + * @param fragment SQL statement fragment to be rendered + * @return rendered statement + */ + public static String render(final Fragment fragment) { + return render(fragment, StringRendererConfig.createDefault()); + } + + /** + * Create a renderer for the given {@link Fragment} and render it. + * + * @param fragment SQL statement fragment to be rendered + * @param config renderer configuration + * @return rendered statement + */ + public static String render(final Fragment fragment, final StringRendererConfig config) { + assert (fragment instanceof SelectFragment); + final SelectRenderer renderer = new SelectRenderer(config); + ((SelectFragment) fragment).accept(renderer); + return renderer.render(); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java index d2b55684..279574a2 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java @@ -4,19 +4,23 @@ * Visitor interface for a {@link BooleanTerm} */ public interface BooleanExpressionVisitor { - void visit(Not not); + public void visit(Not not); - void visit(Literal literal); + public void visit(Literal literal); - void visit(And and); + public void visit(And and); - void leave(Not not); + public void leave(Not not); - void leave(Literal literal); + public void leave(Literal literal); - void leave(And and); + public void leave(And and); - void visit(Or or); + public void visit(Or or); - void leave(Or or); -} + public void leave(Or or); + + public void visit(Comparison comparison); + + public void leave(Comparison comparison); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanTerm.java b/src/main/java/com/exasol/sql/expression/BooleanTerm.java index 9fad266f..0ae285e4 100644 --- a/src/main/java/com/exasol/sql/expression/BooleanTerm.java +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -44,4 +44,72 @@ public static BooleanExpression or(final String literal, final BooleanExpression public static BooleanExpression or(final BooleanExpression... expressions) { return new Or(expressions); } + + // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] + public static BooleanExpression compare(final String left, final String operatorSymbol, final String right) { + return new Comparison(ComparisonOperator.ofSymbol(operatorSymbol), Literal.of(left), Literal.of(right)); + } + + // [dsn~boolean-operation.comparison.constructing-from-enum~1] + public static BooleanExpression compare(final String left, final ComparisonOperator operator, final String right) { + return new Comparison(operator, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression eq(final String left, final String right) { + return new Comparison(ComparisonOperator.EQUAL, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression ne(final String left, final String right) { + return new Comparison(ComparisonOperator.NOT_EQUAL, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression lt(final String left, final String right) { + return new Comparison(ComparisonOperator.LESS_THAN, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression gt(final String left, final String right) { + return new Comparison(ComparisonOperator.GREATER_THAN, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression le(final String left, final String right) { + return new Comparison(ComparisonOperator.LESS_THAN_OR_EQUAL, Literal.of(left), Literal.of(right)); + } + + // [impl->dsn~comparison-operations~1] + public static BooleanExpression ge(final String left, final String right) { + return new Comparison(ComparisonOperator.GREATER_THAN_OR_EQUAL, Literal.of(left), Literal.of(right)); + } + + /** + * Create a logical operation from an operator name and a list of operands + * + * @param operator name of the operator + * @param expressions operands + * @return instance of either {@link And}, {@link Or} or {@link Not} + * @throws IllegalArgumentException if the operator is unknown or null + */ + public static BooleanExpression operation(final String operator, final BooleanExpression... expressions) + throws IllegalArgumentException { + switch (operator.toLowerCase()) { + case "and": + return new And(expressions); + case "or": + return new Or(expressions); + case "not": + if (expressions.length == 1) { + return new Not(expressions[0]); + } else { + throw new IllegalArgumentException( + "Logical \"not\" must have exactly one operand. Got " + expressions.length + "."); + } + default: + throw new IllegalArgumentException( + "Unknown boolean connector \"" + operator + "\". Must be one of \"and\" or \"or\"."); + } + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Comparison.java b/src/main/java/com/exasol/sql/expression/Comparison.java new file mode 100644 index 00000000..0a6381af --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Comparison.java @@ -0,0 +1,51 @@ +package com.exasol.sql.expression; + +//[impl->dsn~comparison-operations~1] +public class Comparison extends AbstractBooleanExpression { + private final ComparisonOperator operator; + private final Literal leftOperand; + private final Literal rightOperand; + + public Comparison(final ComparisonOperator equal, final Literal leftOperand, final Literal rightOperand) { + this.operator = equal; + this.leftOperand = leftOperand; + this.rightOperand = rightOperand; + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } + + /** + * Get the left-hand side operator of the comparison + * + * @return left operator + */ + public AbstractBooleanExpression getLeftOperand() { + return this.leftOperand; + } + + /** + * Get the right-hand side operator of the comparison + * + * @return right operator + */ + public AbstractBooleanExpression getRightOperand() { + return this.rightOperand; + } + + /** + * Get the comparison operator + * + * @return comparison operator + */ + public ComparisonOperator getOperator() { + return this.operator; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/ComparisonOperator.java b/src/main/java/com/exasol/sql/expression/ComparisonOperator.java new file mode 100644 index 00000000..034ab5e8 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/ComparisonOperator.java @@ -0,0 +1,43 @@ +package com.exasol.sql.expression; + +import java.beans.Expression; + +/** + * This enum represents the different types of {@link Comparison}s that can be + * used in {@link Expression}s. + */ +public enum ComparisonOperator { + // [impl->dsn~comparison-operations~1] + EQUAL("="), NOT_EQUAL("<>"), GREATER_THAN(">"), GREATER_THAN_OR_EQUAL(">="), LESS_THAN("<"), LESS_THAN_OR_EQUAL("<="); + + private final String operatorSymbol; + + private ComparisonOperator(final String operatorSymbol) { + this.operatorSymbol = operatorSymbol; + } + + /** + * Returns the operator symbol that represents the comparison. + * + * @return operator symbol + */ + @Override + public String toString() { + return this.operatorSymbol; + } + + /** + * Get the {@link ComparisonOperator} for the provided symbol + * + * @param operatorSymbol symbol that represents the operator + * @return operator + */ + public static ComparisonOperator ofSymbol(final String operatorSymbol) { + for (final ComparisonOperator operator : ComparisonOperator.values()) { + if (operator.operatorSymbol.equals(operatorSymbol)) { + return operator; + } + } + throw new IllegalArgumentException("Unknown comparison operator \"" + operatorSymbol + "\""); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java index c3e5b55e..14657429 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -106,4 +106,24 @@ private void endParenthesis(final BooleanExpression expression) { public String render() { return this.builder.toString(); } + + @Override + public void visit(final Comparison comparison) { + connect(comparison); + if (!comparison.isRoot()) { + startParenthesis(); + } + comparison.getLeftOperand().accept(this); + this.builder.append(" "); + this.builder.append(comparison.getOperator().toString()); + this.builder.append(" "); + comparison.getRightOperand().accept(this); + } + + @Override + public void leave(final Comparison comparison) { + if (!comparison.isRoot()) { + endParenthesis(comparison); + } + } } \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java new file mode 100644 index 00000000..4b7f4afe --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java @@ -0,0 +1,56 @@ +package com.exasol.sql.rendering; + +import com.exasol.sql.Fragment; +import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; + +/** + * Abstract base class for SQL fragment renderers + */ +public abstract class AbstractFragmentRenderer implements FragmentRenderer { + private final StringBuilder builder = new StringBuilder(); + protected final StringRendererConfig config; + private Fragment lastVisited; + + public AbstractFragmentRenderer(final StringRendererConfig config) { + this.config = config; + this.lastVisited = null; + } + + @Override + public String render() { + return this.builder.toString(); + } + + protected void appendKeyWord(final String keyword) { + append(this.config.produceLowerCase() ? keyword.toLowerCase() : keyword); + } + + protected StringBuilder append(final String string) { + return this.builder.append(string); + } + + protected void setLastVisited(final Fragment fragment) { + this.lastVisited = fragment; + } + + protected void appendSpace() { + append(" "); + } + + protected void appendCommaWhenNeeded(final Fragment fragment) { + if (this.lastVisited.getClass().equals(fragment.getClass())) { + append(", "); + } + } + + protected void appendRenderedExpression(final BooleanExpression expression) { + final BooleanExpressionRenderer expressionRenderer = new BooleanExpressionRenderer(); + expression.accept(expressionRenderer); + append(expressionRenderer.render()); + } + + protected void append(final int number) { + this.builder.append(number); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/FragmentRenderer.java b/src/main/java/com/exasol/sql/rendering/FragmentRenderer.java new file mode 100644 index 00000000..b2c50341 --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/FragmentRenderer.java @@ -0,0 +1,10 @@ +package com.exasol.sql.rendering; + +public interface FragmentRenderer { + /** + * Render an SQL statement to a string. + * + * @return rendered string + */ + public String render(); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java b/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java deleted file mode 100644 index 71dbf2e1..00000000 --- a/src/main/java/com/exasol/sql/rendering/SqlStatementRenderer.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.exasol.sql.rendering; - -import java.util.Optional; - -import com.exasol.sql.Fragment; -import com.exasol.sql.FragmentVisitor; -import com.exasol.sql.dql.*; -import com.exasol.sql.expression.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 index 2ab8bf7d..07d99554 100644 --- a/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java +++ b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java @@ -3,8 +3,7 @@ import com.exasol.sql.StatementFactory; /** - * This class implements a parameter object containing the configuration options - * for the {@link StatementFactory}. + * This class implements a parameter object containing the configuration options for the {@link StatementFactory}. */ public class StringRendererConfig { private final boolean lowerCase; @@ -40,8 +39,7 @@ public StringRendererConfig build() { /** * Define whether the statement should be produced in lower case * - * @param lowerCase set to true if the statement should be produced - * in lower case + * @param lowerCase set to true if the statement should be produced in lower case * @return this instance for fluent programming */ public Builder lowerCase(final boolean lowerCase) { @@ -49,4 +47,13 @@ public Builder lowerCase(final boolean lowerCase) { return this; } } + + /** + * Create the default configuration. + * + * @return default configuration + */ + public static StringRendererConfig createDefault() { + return new Builder().build(); + } } \ No newline at end of file diff --git a/src/test/java/com/exasol/dql/rendering/TestJoin.java b/src/test/java/com/exasol/dql/rendering/TestJoin.java deleted file mode 100644 index fed3eb20..00000000 --- a/src/test/java/com/exasol/dql/rendering/TestJoin.java +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 02d6d7dc..00000000 --- a/src/test/java/com/exasol/dql/rendering/TestLimit.java +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 472d980b..00000000 --- a/src/test/java/com/exasol/dql/rendering/TestSelect.java +++ /dev/null @@ -1,74 +0,0 @@ -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 deleted file mode 100644 index 0771ae54..00000000 --- a/src/test/java/com/exasol/dql/rendering/TestSqlStatementRenderer.java +++ /dev/null @@ -1,17 +0,0 @@ -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/BooleanExpressionRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java index 22549ff6..65e627c8 100644 --- a/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java +++ b/src/test/java/com/exasol/hamcrest/BooleanExpressionRenderResultMatcher.java @@ -2,9 +2,9 @@ import org.hamcrest.Description; +import com.exasol.sql.dql.rendering.SelectRenderer; import com.exasol.sql.expression.BooleanExpression; import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; -import com.exasol.sql.rendering.SqlStatementRenderer; import com.exasol.sql.rendering.StringRendererConfig; /** @@ -55,7 +55,7 @@ public static BooleanExpressionRenderResultMatcher rendersTo(final String expect * Factory method for {@link BooleanExpressionRenderResultMatcher} * * @param config configuration settings for the - * {@link SqlStatementRenderer} + * {@link SelectRenderer} * @param expectedText text that represents the expected rendering result * @return the matcher */ diff --git a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java index 03f54d94..e4a19988 100644 --- a/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java +++ b/src/test/java/com/exasol/hamcrest/SqlFragmentRenderResultMatcher.java @@ -3,24 +3,27 @@ import org.hamcrest.Description; import com.exasol.sql.Fragment; -import com.exasol.sql.rendering.SqlStatementRenderer; +import com.exasol.sql.dml.InsertFragment; +import com.exasol.sql.dml.rendering.InsertRenderer; +import com.exasol.sql.dql.SelectFragment; +import com.exasol.sql.dql.rendering.SelectRenderer; import com.exasol.sql.rendering.StringRendererConfig; /** - * This class implements a matcher for the results of rendering SQL statements - * to text. + * This class implements a matcher for the results of rendering SQL statements to text. */ public class SqlFragmentRenderResultMatcher extends AbstractRenderResultMatcher { - private final SqlStatementRenderer renderer; + + private final StringRendererConfig config; private SqlFragmentRenderResultMatcher(final String expectedText) { super(expectedText); - this.renderer = new SqlStatementRenderer(); + this.config = StringRendererConfig.createDefault(); } private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final String expectedText) { super(expectedText); - this.renderer = new SqlStatementRenderer(config); + this.config = config; } /** @@ -30,8 +33,19 @@ private SqlFragmentRenderResultMatcher(final StringRendererConfig config, final */ @Override public boolean matchesSafely(final Fragment fragment) { - ((Fragment) fragment.getRoot()).accept(this.renderer); - this.renderedText = this.renderer.render(); + final Fragment root = fragment.getRoot(); + if (root instanceof SelectFragment) { + final SelectRenderer renderer = new SelectRenderer(this.config); + ((SelectFragment) root).accept(renderer); + this.renderedText = renderer.render(); + } else if (root instanceof InsertFragment) { + final InsertRenderer renderer = new InsertRenderer(this.config); + ((InsertFragment) root).accept(renderer); + this.renderedText = renderer.render(); + } else { + throw new UnsupportedOperationException( + "Don't know how to render fragment of type\"" + root.getClass().getName() + "\"."); + } return this.renderedText.equals(this.expectedText); } @@ -53,8 +67,7 @@ public static SqlFragmentRenderResultMatcher rendersTo(final String expectedText /** * Factory method for {@link SqlFragmentRenderResultMatcher} * - * @param config configuration settings for the - * {@link SqlStatementRenderer} + * @param config configuration settings for the {@link SelectRenderer} * @param expectedText text that represents the expected rendering result * @return the matcher */ diff --git a/src/test/java/com/exasol/sql/dml/TestInsert.java b/src/test/java/com/exasol/sql/dml/TestInsert.java new file mode 100644 index 00000000..2894aa20 --- /dev/null +++ b/src/test/java/com/exasol/sql/dml/TestInsert.java @@ -0,0 +1,32 @@ +package com.exasol.sql.dml; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; + +class TestInsert { + private static final String TABLE_NAME = "person"; + private Insert insert; + + @BeforeEach + void beforeEach() { + this.insert = StatementFactory.getInstance().insertInto(TABLE_NAME); + } + + // [utest->dsn~insert-statements~1] + @Test + void testInsert() { + assertThat(this.insert, instanceOf(Insert.class)); + } + + // [utest->dsn~insert-statements~1] + @Test + void testInsertTableName() { + assertThat(this.insert.getTableName(), equalTo(TABLE_NAME)); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java new file mode 100644 index 00000000..b5492f01 --- /dev/null +++ b/src/test/java/com/exasol/sql/dml/rendering/TestInsertRendering.java @@ -0,0 +1,32 @@ +package com.exasol.sql.dml.rendering; + +import static com.exasol.hamcrest.SqlFragmentRenderResultMatcher.rendersTo; +import static org.hamcrest.MatcherAssert.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.exasol.sql.StatementFactory; +import com.exasol.sql.dml.Insert; + +class TestInsertRendering { + private static final String PERSON = "person"; + private Insert insert; + + @BeforeEach + void beforeEach() { + this.insert = StatementFactory.getInstance().insertInto(PERSON); + } + + // [dsn~rendering.sql.insert~1] + @Test + void testInsert() { + assertThat(this.insert, rendersTo("INSERT INTO person")); + } + + // [dsn~rendering.sql.insert~1] + @Test + void testInsertFields() { + assertThat(this.insert.field("a", "b"), rendersTo("INSERT INTO person (a, b)")); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/dql/TestSelect.java b/src/test/java/com/exasol/sql/dql/TestSelect.java new file mode 100644 index 00000000..3ec9176d --- /dev/null +++ b/src/test/java/com/exasol/sql/dql/TestSelect.java @@ -0,0 +1,30 @@ +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; +import com.exasol.sql.dql.Select; + +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/TestSelectRendering.java b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java new file mode 100644 index 00000000..e640566e --- /dev/null +++ b/src/test/java/com/exasol/sql/dql/rendering/TestSelectRendering.java @@ -0,0 +1,62 @@ +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 TestSelectRendering { + private Select select; + + @BeforeEach + void beforeEach() { + this.select = StatementFactory.getInstance().select(); + } + + // [dsn~rendering.sql.select~1] + @Test + void testSelectAll() { + assertThat(this.select.all(), rendersTo("SELECT *")); + } + + // [dsn~rendering.sql.select~1] + @Test + void testSelectFieldNames() { + assertThat(this.select.field("a", "b"), rendersTo("SELECT a, b")); + } + + // [dsn~rendering.sql.select~1] + @Test + void testSelectChainOfFieldNames() { + assertThat(this.select.field("a", "b").field("c"), rendersTo("SELECT a, b, c")); + } + + // [dsn~rendering.sql.select~1] + @Test + void testSelectFromTable() { + assertThat(this.select.all().from().table("persons"), rendersTo("SELECT * FROM persons")); + } + + // [dsn~rendering.sql.select~1] + @Test + void testSelectFromMultipleTable() { + assertThat(this.select.all().from().table("table1").table("table2"), rendersTo("SELECT * FROM table1, table2")); + } + + // [dsn~rendering.sql.select~1] + @Test + void testSelectFromTableAs() { + assertThat(this.select.all().from().tableAs("table", "t"), rendersTo("SELECT * FROM table AS t")); + } + + // [dsn~rendering.sql.select~1] + @Test + void testSelectFromMultipleTableAs() { + assertThat(this.select.all().from().tableAs("table1", "t1").tableAs("table2", "t2"), + rendersTo("SELECT * FROM table1 AS t1, table2 AS t2")); + } +} \ 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..9c7e77d3 --- /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"))); + } + + // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] + @Test + void testOperationFromComparisonOperatorString() { + assertThat(BooleanTerm.compare("a", "<>", "b"), instanceOf(Comparison.class)); + } + + // [impl->dsn~boolean-operation.comparison.constructing-from-strings~1] + @Test + void testOperationFromComparisonOperatorEnum() { + assertThat(BooleanTerm.compare("a", ComparisonOperator.NOT_EQUAL, "b"), instanceOf(Comparison.class)); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java new file mode 100644 index 00000000..819b28d3 --- /dev/null +++ b/src/test/java/com/exasol/sql/expression/TestComparisonOperator.java @@ -0,0 +1,24 @@ +package com.exasol.sql.expression; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class TestComparisonOperator { + @Test + void testToString() { + assertThat(ComparisonOperator.EQUAL.toString(), equalTo("=")); + } + + @Test + void testOfSymbol() { + assertThat(ComparisonOperator.ofSymbol("<>"), equalTo(ComparisonOperator.NOT_EQUAL)); + } + + @Test + void testOfUnknownSymbolThrowsException() { + assertThrows(IllegalArgumentException.class, () -> ComparisonOperator.ofSymbol("§")); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java index 5d58da54..5e8129a2 100644 --- a/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java +++ b/src/test/java/com/exasol/sql/expression/rendering/TestBooleanExpressionRenderer.java @@ -4,77 +4,118 @@ 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 = new StringRendererConfig.Builder().lowerCase(true).build(); assertThat(expression, rendersWithConfigTo(config, "(not(a) or b) and (c or d)")); } + + // [utest->dsn~comparison-operations~1] + @Test + void 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/model/diagrams/class/cl_fragments.plantuml b/src/uml/diagrams/class/cl_fragments.plantuml similarity index 93% rename from model/diagrams/class/cl_fragments.plantuml rename to src/uml/diagrams/class/cl_fragments.plantuml index 0dbc4c44..1f6fef13 100644 --- a/model/diagrams/class/cl_fragments.plantuml +++ b/src/uml/diagrams/class/cl_fragments.plantuml @@ -1,5 +1,5 @@ @startuml -!include ../exasol.skin +'!include ../exasol.skin together { interface Fragment <> 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/model/diagrams/class/cl_visitor.plantuml b/src/uml/diagrams/class/cl_visitor.plantuml similarity index 100% rename from model/diagrams/class/cl_visitor.plantuml rename to src/uml/diagrams/class/cl_visitor.plantuml diff --git a/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