diff --git a/.classpath b/.classpath new file mode 100644 index 00000000..ebfacabc --- /dev/null +++ b/.classpath @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..357db2cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/bin/ +/target/ +**/*.md.html +**/*.bak +**/*.swp +**/*.log +**/*.out \ No newline at end of file diff --git a/.project b/.project new file mode 100644 index 00000000..547c8446 --- /dev/null +++ b/.project @@ -0,0 +1,18 @@ + + + sql-statement-builder + This module provides a Builder for SQL statements that helps creating the correct structure and validates variable parts of the statements. NO_M2ECLIPSE_SUPPORT: Project files created with the maven-eclipse-plugin are not supported in M2Eclipse. + + + + org.eclipse.jdt.core.javabuilder + + + org.eclipse.m2e.core.maven2Builder + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + \ No newline at end of file diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..f9fe3459 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/test/java=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..b8947ec6 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..a8dd7a62 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: java + +jdk: + - openjdk8 + - openjdk11 + +before_script: + - version=$(grep -oP '(?<=^ )[^<]*' pom.xml) + +script: + - mvn clean install diff --git a/README.md b/README.md index 3ae35b3a..93f2004e 100644 --- a/README.md +++ b/README.md @@ -1 +1,74 @@ -# sql-statement-builder \ No newline at end of file +# sql-statement-builder + +[![Build Status](https://travis-ci.com/EXASOL/sql-statement-builder.svg?branch=develop)](https://travis-ci.com/EXASOL/sql-statement-builder) + +The Exasol SQL Statement Builder abstracts programmatic creation of SQL statements and is intended to replace ubiquitous string concatenation solutions which make the code hard to read and are prone to error and security risks. + +Goals: + +1. Foster clean and readable code +1. Allow for thorough validation of dynamic parts +1. Detect as many errors as possible at *compile time* +1. Don't repeat yourself (DRY) +1. Allow extension for different SQL dialects + +## Usage + +```java +import com.exasol.sql.StatementFactory; +import com.exasol.sql.SqlStatement; +import com.exasol.sql.rendering.SelectRenderer; + +SqlStatement statement = StatementFactory.getInstance() + .select().field("firstname", "lastname") + .from().table("person"); + +String statementText = SqlStatementRenderer.render(statement); +``` + +## Development + +The following sub-sections provide information about building and extending the project. + +### Build Time Dependencies + +The list below show all build time dependencies in alphabetical order. Note that except the Maven build tool all required modules are downloaded automatically by Maven. + +| Dependency | Purpose | License | +------------------------------------------------------------|--------------------------------------------------------|-------------------------------- +| [Apache Maven](https://maven.apache.org/) | Build tool | Apache License 2.0 | +| [Equals Verifier](https://github.com/jqno/equalsverifier) | Automatic contract checker for `equals()` and `hash()` | Apache License 2.0 | +| [Hamcrest](http://hamcrest.org/) | Advanced matchers for JUnit | GNU BSD-3-Clause | +| [JUnit 5](https://junit.org/junit5/) | Unit testing framework | Eclipse Public License 1.0 | +| [Mockito](http://site.mockito.org/) | Mocking framework | MIT License | + +### Planned Milestones + +The milestones listed below are a rough outline and might be subject to change depending on which constructs are needed more. The plan will be updated accordingly. + +#### M1 + +* Basic support for Data Query Language (DQL) statement constructs (SELECT, FROM, JOIN, WHERE) +* Rendering to string +* Exasol Dialect only + +#### M2 + +* Validation for constructs from M1 + +(Later milestones will always include validation of the newly learned milestones) + +#### M3 + +* Scalar functions + +#### M4 + +* Sub-Selects including validation + +#### Later Milstones (very coarse) + +* Data Manipulation Language (DML) statements +* Data Definition Language (DDL) statements +* Support for Standard SQL +* Support for other dialects (help welcome!) diff --git a/doc/design.md b/doc/design.md new file mode 100644 index 00000000..1b8837ef --- /dev/null +++ b/doc/design.md @@ -0,0 +1,181 @@ +# 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. + +## Solution Strategy + +### Fluent Programming + +#### Statement Construction With Fluent Programming +`dsn~statement-construction-with-fluent-programming~1` + +All statement builders use the "fluent programming" model, where the return type of each builder step determines the possible next structural elements that can be added. + +Comment: + +This is a design principle that cuts across the whole project. Therefore locating it in a single test or implementation part makes no sense. + +Covers: + +* `req~statement-structure-limited-at-compile-time~1` + +## Runtime View + +### Building Select Statements + +#### Accessing the Clauses That Make Up a SELECT Statement +`dsn~select-statement.out-of-order-clauses~1` + +`SELECT` commands allow attaching the following clauses in any order: + +* `FROM` clause +* `WHERE` clause +* `LIMIT` clause + +Covers: + +* `req~statement-structure.step-wise~1` + +Needs: impl, utest + +Tags: Select Statement Builder + +### Building Boolean Expressions + +#### Forwarded Requirements + +* `dsn --> impl, utest: req~boolean-operators~1` +* `dsn --> impl, utest: req~comparison-operations~1` + +#### 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 + +### 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 --> impl, utest: req~rendering.sql.configurable-case~1` +* `dsn --> impl, utest: req~rendering.sql.select~1` +* `dsn --> impl, utest: req~rendering.sql.insert~1` + +#### Renderer add Double Quotes for Schema, Table and Column Identifiers +`dsn~rendering.add-double-quotes-for-schema-table-and-column-identifiers~1` + +The renderer sets the following identifiers in double quotes if configured: + +* Schema identifiers +* Table identifiers +* Column identifiers (except the asterisks) + +Comment: + +Examples are `"my_schema"."my_table"."my_field"`, `"MY_TABLE"."MyField"` and `"MyTable".*` + +Covers: + +* `req~rendering.sql.confiugrable-identifier-quoting~1` + +Needs: impl, utest + +### Exasol Dialect Specific + +#### Converting from 64 bit Integers to INTERVAL DAY TO SECOND +`dsn~exasol.converting-int-to-interval-day-to-second~1` + +The data converter converts integers to `INTERVAL DAY TO SECOND`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL DAY TO SECOND From Strings +`dsn~exasol.parsing-interval-day-to-second-from-strings~1` + +The data converter can parse `INTERVAL DAY TO SECOND` from strings in the following format: + + interval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] + + hours = ( "2" "0" - "3" ) / ( [ "0" / "1" ] DIGIT ) + + minutes = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) + + seconds = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) + + milliseconds = 1*3DIGIT + +Examples are `12:30`, `12:30.081` or `100 12:30:00.081`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Converting from 64 bit Integers to INTERVAL YEAR TO MONTH +`dsn~exasol.converting-int-to-interval-year-to-month~1` + +The data converter converts integers to `INTERVAL YEAR TO MONTH`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL YEAR TO MONTH From Strings +`dsn~exasol.parsing-interval-year-to-month-from-strings~1` + +The data converter can parse `INTERVAL YEAR TO MONTH` from strings in the following format: + + interval-y2m = days "-" months + + days = 1*9DIGIT + + months = ( "1" "0" - "2" ) / DIGIT + +Examples are `0-1` and `100-11`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md new file mode 100644 index 00000000..f6287272 --- /dev/null +++ b/doc/system_requirements.md @@ -0,0 +1,272 @@ +# 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 + +### Data Conversion +`feat~data-conversion~1` + +ESB converts between values of compatible data types. + +Rationale: + +Different databases and related tools use different ways to store and process similar data types. A collection of well-tested converters saves the API users time and trouble. + +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~statement-definition~1](#statement-definition) + +Needs: dsn + +#### Statement Structure Limited at Compile-time +`req~statement-structure-limited-at-compile-time~1` + +ESB lets users create only valid statement structures at compile-time. + +Rationale: + +If users can't get illegal structures to compile, they don't need to spend time debugging them later. + +Covers: + +* [feat~compile-time-error-checking~1](#compile-time-error-checking) + +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 + +#### Configurable Identifier Quoting +`req~rendering.sql.confiugrable-identifier-quoting~1` + +ESB allows users to choose whether the following identifiers should be quoted in the rendered query: + +* Schema identifiers +* Table identifiers +* Column identifiers + +Rationale: + +The Exasol database for example requires identifiers to be enclosed in double quotes in order to enable case sensitivity. + +Covers: + +* [feat~sql-string-rendering~1](#sql-string-rendering) + +Needs: dsn + +#### 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 + +### Exasol Dialect Specific Requirements + +###### Integer - Interval Conversion +`req~integer-interval-conversion~1` + +ESB converts values of type `INTERVAL` to integer and vice-versa. + +Rationale: + +Neighboring systems of an Exasol database often do not have equivalent data types, so conversion to a primitive data type is required. + +Covers: + +* [feat~data-conversion~1](#data-conversion) + +Needs: dsn \ No newline at end of file diff --git a/launch/sql-statement-builder mvn package.launch b/launch/sql-statement-builder mvn package.launch new file mode 100644 index 00000000..e2007574 --- /dev/null +++ b/launch/sql-statement-builder mvn package.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/launch/sql-statment-builder all tests.launch b/launch/sql-statment-builder all tests.launch new file mode 100644 index 00000000..c0bc8ef8 --- /dev/null +++ b/launch/sql-statment-builder all tests.launch @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..d891ef7e --- /dev/null +++ b/pom.xml @@ -0,0 +1,65 @@ + + 4.0.0 + sql-statement-builder + sql-statement-builder + 0.1.0 + Exasol SQL Statement Builder + This module provides a Builder for SQL statements that helps creating the correct structure and validates variable parts of the statements. + + UTF-8 + 1.8 + 5.3.1 + 1.3.1 + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.platform + junit-platform-launcher + ${junit.platform.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + org.hamcrest + hamcrest-all + 1.3 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + + \ No newline at end of file diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java new file mode 100644 index 00000000..bb55524b --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -0,0 +1,141 @@ +package com.exasol.datatype.interval; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class implements the Exasol-proprietary data type INTERVAL DAY(x) TO SECONDS(y). It supports + * conversions to and from strings and from milliseconds. + * + *

+ * In Exasol this data type represents a time difference consisting of the following components: + *

+ * + * + * Since milliseconds are the highest resolution, each interval can also be expressed as a total number of milliseconds. + * This is also the recommended way to represent the interval values in other systems which do not natively support this + * data type. + */ +public class IntervalDayToSecond { + private static final long MILLIS_PER_SECOND = 1000L; + private static final long SECONDS_PER_MINUTE = 60L; + private static final long MINUTES_PER_HOUR = 60L; + private static final long HOURS_PER_DAY = 24L; + private static final long MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND; + private static final long MILLIS_PER_HOUR = MINUTES_PER_HOUR * MILLIS_PER_MINUTE; + private static final long MILLIS_PER_DAY = HOURS_PER_DAY * MILLIS_PER_HOUR; + private static final int DAYS_MATCHING_GROUP = 1; + private static final int HOURS_MATCHING_GROUP = 2; + private static final int MINUTES_MATCHING_GROUP = 3; + private static final int SECONDS_MATCHING_GROUP = 4; + private static final int MILLIS_MATCHING_GROUP = 5; + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(?:(\\d{1,9})\\s+)?" // days + + "(\\d{1,2})" // hours + + ":(\\d{1,2})" // minutes + + "(?::(\\d{1,2})" // seconds + + "(?:\\.(\\d{1,3}))?)?" // milliseconds + ); + private final long value; + + private IntervalDayToSecond(final long value) { + this.value = value; + } + + private IntervalDayToSecond(final String text) { + final Matcher matcher = INTERVAL_PATTERN.matcher(text); + if (matcher.matches()) { + this.value = MILLIS_PER_DAY * parseMatchingGroupToLong(matcher, DAYS_MATCHING_GROUP) // + + MILLIS_PER_HOUR * parseMatchingGroupToLong(matcher, HOURS_MATCHING_GROUP) // + + MILLIS_PER_MINUTE * parseMatchingGroupToLong(matcher, MINUTES_MATCHING_GROUP) // + + MILLIS_PER_SECOND * parseMatchingGroupToLong(matcher, SECONDS_MATCHING_GROUP) // + + parseMatchingGroupToLong(matcher, MILLIS_MATCHING_GROUP); + } else { + throw new IllegalArgumentException( + "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\""); + } + } + + private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) { + return (matcher.group(groupNumber) == null) ? 0 : Long.parseLong(matcher.group(groupNumber)); + } + + @Override + public String toString() { + return hasDays() // + ? String.format("%d %d:%02d:%02d.%03d", getDays(), getHours(), getMinutes(), getSeconds(), getMillis()) // + : String.format("%d:%02d:%02d.%03d", getHours(), getMinutes(), getSeconds(), getMillis()); + } + + private boolean hasDays() { + return this.value >= MILLIS_PER_DAY; + } + + private long getDays() { + return this.value / MILLIS_PER_DAY; + } + + private long getHours() { + return this.value / MILLIS_PER_HOUR % HOURS_PER_DAY; + } + + private long getMinutes() { + return this.value / MILLIS_PER_MINUTE % MINUTES_PER_HOUR; + } + + private long getSeconds() { + return this.value / MILLIS_PER_SECOND % SECONDS_PER_MINUTE; + } + + private long getMillis() { + return this.value % MILLIS_PER_SECOND; + } + + /** + * Create an {@link IntervalDayToSecond} from a number of milliseconds + * + * @param value total length of the interval in milliseconds + * @return interval with milliseconds resolution + */ + // [impl->dsn~exasol.converting-int-to-interval-day-to-second~1] + public static IntervalDayToSecond ofMillis(final long value) { + return new IntervalDayToSecond(value); + } + + /** + * Parse an {@link IntervalDayToSecond} from a string + * + *

+ * The accepted format is: + *

+ *

+ * [dddddddd ]hh:mm[:ss[.SSS]] + *

+ * Where + *

+ *
+ *
d
+ *
day, 1-9 digits, optional
+ *
h
+ *
hours, 1-2 digits, mandatory
+ *
m
+ *
minutes, 1-2 digits, mandatory
+ *
s
+ *
seconds, 1-2 digits, optional
+ *
S
+ *
milliseconds, 1-3 digits, optional
+ *
+ * + * @param text string representing an interval + * @return interval with milliseconds resolution + */ + // [impl->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + public static IntervalDayToSecond parse(final String text) { + return new IntervalDayToSecond(text); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java new file mode 100644 index 00000000..613a615a --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java @@ -0,0 +1,97 @@ +package com.exasol.datatype.interval; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class implements the Exasol-proprietary data type INTERVAL YEAR(x) TO MONTH(y). It supports + * conversions to and from strings and from a number of months. + * + *

+ * In Exasol this data type represents a time difference consisting of the following components: + *

+ * + * + * Since months are the highest resolution, each interval can also be expressed as a total number of months. This is + * also the recommended way to represent the interval values in other systems which do not natively support this data + * type. + */ +public class IntervalYearToMonth { + private static final long MONTHS_PER_YEAR = 12L; + private static final int YEARS_MATCHING_GROUP = 1; + private static final int MONTHS_MATCHING_GROUP = 2; + private static final Pattern INTERVAL_PATTERN = Pattern.compile("(\\d{1,9})-(\\d{1,2})"); + private final long value; + + private IntervalYearToMonth(final long value) { + this.value = value; + } + + private IntervalYearToMonth(final String text) { + final Matcher matcher = INTERVAL_PATTERN.matcher(text); + if (matcher.matches()) { + this.value = MONTHS_PER_YEAR * parseMatchingGroupToLong(matcher, YEARS_MATCHING_GROUP) // + + parseMatchingGroupToLong(matcher, MONTHS_MATCHING_GROUP); + } else { + throw new IllegalArgumentException( + "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\""); + } + } + + private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) { + return Long.parseLong(matcher.group(groupNumber)); + } + + @Override + public String toString() { + return String.format("%d-%02d", getYears(), getMonths()); + } + + private long getYears() { + return this.value / MONTHS_PER_YEAR; + } + + private long getMonths() { + return this.value % MONTHS_PER_YEAR; + } + + /** + * Create an {@link IntervalDayToSecond} from a number of months + * + * @param value total length of the interval in months + * @return interval with months resolution + */ + // [impl->dsn~exasol.converting-int-to-interval-year-to-month~1] + public static IntervalYearToMonth ofMonths(final long value) { + return new IntervalYearToMonth(value); + } + + /** + * Parse an {@link IntervalDayToSecond} from a string + * + *

+ * The accepted format is: + *

+ *

+ * YYYYYYYYY:MM + *

+ * Where + *

+ *
+ *
Y
+ *
years, 1-9 digits, mandatory
+ *
M
+ *
months, 1-2 digits, mandatory
+ *
+ * + * @param text string representing an interval + * @return interval with months resolution + */ + // [impl->dsn~exasol.parsing-interval-year-to-month-from-strings~1] + public static IntervalYearToMonth parse(final String text) { + return new IntervalYearToMonth(text); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/AbstractFragment.java b/src/main/java/com/exasol/sql/AbstractFragment.java new file mode 100644 index 00000000..374af2d6 --- /dev/null +++ b/src/main/java/com/exasol/sql/AbstractFragment.java @@ -0,0 +1,22 @@ +package com.exasol.sql; + +/** + * Common base class for SQL statement fragments + */ +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 Fragment getRoot() { + return (this.root == null) ? this : this.root; + } +} \ 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 new file mode 100644 index 00000000..deaa007c --- /dev/null +++ b/src/main/java/com/exasol/sql/Fragment.java @@ -0,0 +1,14 @@ +package com.exasol.sql; + +/** + * 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 new file mode 100644 index 00000000..e0816758 --- /dev/null +++ b/src/main/java/com/exasol/sql/FragmentVisitor.java @@ -0,0 +1,10 @@ +package com.exasol.sql; + +/** + * This interface represents a visitor for SQL statement fragments. + */ +public interface FragmentVisitor { + public void visit(final Field field); + + public void visit(Table table); +} \ 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/SqlStatement.java b/src/main/java/com/exasol/sql/SqlStatement.java new file mode 100644 index 00000000..bdbceaf6 --- /dev/null +++ b/src/main/java/com/exasol/sql/SqlStatement.java @@ -0,0 +1,8 @@ +package com.exasol.sql; + +/** + * This interface represents an SQL statement. + */ +public interface SqlStatement extends Fragment { + +} diff --git a/src/main/java/com/exasol/sql/StatementFactory.java b/src/main/java/com/exasol/sql/StatementFactory.java new file mode 100644 index 00000000..87cdfe74 --- /dev/null +++ b/src/main/java/com/exasol/sql/StatementFactory.java @@ -0,0 +1,46 @@ +package com.exasol.sql; + +import com.exasol.sql.dml.Insert; +import com.exasol.sql.dql.Select; + +/** + * The {@link StatementFactory} implements an factory for SQL statements. + */ +public final class StatementFactory { + private static StatementFactory instance; + + /** + * Get an instance of a {@link StatementFactory} + * + * @return the existing instance otherwise creates one. + */ + public static synchronized StatementFactory getInstance() { + if (instance == null) { + instance = new StatementFactory(); + } + return instance; + } + + private StatementFactory() { + // prevent instantiation outside singleton + } + + /** + * Create a {@link Select} statement + * + * @return a new instance of a {@link Select} statement + */ + public Select select() { + return new Select(); + } + + /** + * 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/Table.java b/src/main/java/com/exasol/sql/Table.java new file mode 100644 index 00000000..009476ee --- /dev/null +++ b/src/main/java/com/exasol/sql/Table.java @@ -0,0 +1,59 @@ +package com.exasol.sql; + +import java.util.Optional; + +/** + * This class represents a {@link Table} in an SQL Statement + */ +public class Table extends AbstractFragment implements TableReference, GenericFragment { + private final String name; + private final Optional as; + + /** + * 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 Fragment root, final String name) { + super(root); + this.name = name; + this.as = Optional.empty(); + } + + /** + * 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 Fragment root, final String name, final String as) { + super(root); + this.name = name; + this.as = Optional.of(as); + } + + /** + * Get the name of the table + * + * @return table name + */ + public String getName() { + return this.name; + } + + /** + * Get the correlation name (i.e. an alias) of the table. + * + * @return correlation name + */ + public Optional getAs() { + return this.as; + } + + @Override + 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/UnnamedPlaceholder.java b/src/main/java/com/exasol/sql/UnnamedPlaceholder.java new file mode 100644 index 00000000..415429d9 --- /dev/null +++ b/src/main/java/com/exasol/sql/UnnamedPlaceholder.java @@ -0,0 +1,10 @@ +package com.exasol.sql; + +import com.exasol.sql.expression.*; + +public class UnnamedPlaceholder extends AbstractValueExpression implements ValueExpression { + @Override + public void accept(final ValueExpressionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file 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..49100441 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/Insert.java @@ -0,0 +1,107 @@ +package com.exasol.sql.dml; + +import com.exasol.sql.*; +import com.exasol.sql.dql.Select; + +/** + * This class implements an SQL {@link Select} statement + */ +// [impl->dsn~insert-statements~1] +public class Insert extends AbstractFragment implements SqlStatement, InsertFragment { + private final Table table; + private InsertFields insertFields; + private InsertValues insertValues; + + /** + * 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.insertFields == null) { + this.insertFields = new InsertFields(this); + } + this.insertFields.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(); + } + + /** + * Insert a list of concrete values + * + * @param values values to be inserted + * @return this for fluent programming + */ + // [impl->dsn~values-as-insert-source~1] + public synchronized Insert values(final Object... values) { + if (this.insertValues == null) { + this.insertValues = new InsertValues(this); + } + this.insertValues.add(values); + return this; + } + + /** + * Add an unnamed value placeholder to the value list (this is useful for prepared statements) + * + * @return this for fluent programming + */ + // [impl->dsn~values-as-insert-source~1] + public synchronized Insert valuePlaceholder() { + if (this.insertValues == null) { + this.insertValues = new InsertValues(this); + } + this.insertValues.addPlaceholder(); + return this; + } + + /** + * Add a given number unnamed value placeholder to the value list (this is useful for prepared statements) + * + * @param amount number of placeholders to be added + * @return this for fluent programming + */ + // [impl->dsn~values-as-insert-source~1] + public synchronized Insert valuePlaceholders(final int amount) { + if (this.insertValues == null) { + this.insertValues = new InsertValues(this); + } + for (int i = 0; i < amount; ++i) { + this.insertValues.addPlaceholder(); + } + return this; + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + if (this.table != null) { + this.table.accept(visitor); + } + if (this.insertFields != null) { + this.insertFields.accept(visitor); + } + if (this.insertValues != null) { + this.insertValues.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/InsertValues.java b/src/main/java/com/exasol/sql/dml/InsertValues.java new file mode 100644 index 00000000..aa5e4456 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertValues.java @@ -0,0 +1,55 @@ +package com.exasol.sql.dml; + +import java.util.ArrayList; +import java.util.List; + +import com.exasol.sql.*; +import com.exasol.sql.expression.Value; +import com.exasol.sql.expression.ValueExpression; + +/** + * Container class for values to be inserted by an INSERT statement. + */ +public class InsertValues extends AbstractFragment implements InsertFragment { + private final List values = new ArrayList<>(); + + /** + * Create a new instance of {@link InsertValues + * + * @param root root SQL statement + */ + public InsertValues(final Fragment root) { + super(root); + } + + /** + * Add one or more values + * + * @param values values + */ + public void add(final Object... values) { + for (final Object value : values) { + this.getValues().add(new Value(value)); + } + } + + /** + * Get the values + * + * @return value + */ + public List getValues() { + return this.values; + } + + @Override + public void accept(final InsertVisitor visitor) { + visitor.visit(this); + // sub-expression left out intentionally + visitor.leave(this); + } + + public void addPlaceholder() { + this.values.add(new UnnamedPlaceholder()); + } +} \ 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..d4ce8970 --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/InsertVisitor.java @@ -0,0 +1,15 @@ +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); + + public void visit(InsertValues insertValues); + + public void leave(InsertValues insertValues); +} \ 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..97c2905d --- /dev/null +++ b/src/main/java/com/exasol/sql/dml/rendering/InsertRenderer.java @@ -0,0 +1,87 @@ +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.expression.ValueExpression; +import com.exasol.sql.rendering.AbstractFragmentRenderer; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * The {@link InsertRenderer} turns SQL statement structures in to SQL strings. + */ +// [impl->dsn~rendering.sql.insert~1] +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) { + appendAutoQuoted(table.getName()); + setLastVisited(table); + } + + @Override + public void visit(final Field field) { + appendCommaWhenNeeded(field); + appendAutoQuoted(field.getName()); + setLastVisited(field); + } + + @Override + public void visit(final InsertFields insertFields) { + append(" ("); + setLastVisited(insertFields); + } + + @Override + public void leave(final InsertFields insertFields) { + append(")"); + } + + @Override + public void visit(final InsertValues insertValues) { + appendKeyWord(" VALUES ("); + for (final ValueExpression expression : insertValues.getValues()) { + appendCommaWhenNeeded(insertValues); + appendRenderedValueExpression(expression); + setLastVisited(insertValues); + } + } + + @Override + public void leave(final InsertValues insertValues) { + append(")"); + } + + /** + * Create an {@link InsertRenderer} using the default renderer configuration + * + * @return insert renderer + */ + public static InsertRenderer create() { + return create(StringRendererConfig.createDefault()); + } + + /** + * Create an {@link InsertRenderer} + * + * @param config renderer configuration + * @return insert renderer + */ + public static InsertRenderer create(final StringRendererConfig config) { + return new InsertRenderer(config); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/FromClause.java b/src/main/java/com/exasol/sql/dql/FromClause.java new file mode 100644 index 00000000..bf952297 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/FromClause.java @@ -0,0 +1,153 @@ +package com.exasol.sql.dql; + +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 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(final Fragment root) { + super(root); + } + + /** + * Create a {@link FromClause} from a table name + * + * @param name table name + * @return new instance + */ + public FromClause table(final String name) { + this.tables.add(new Table(getRoot(), name)); + return this; + } + + /** + * Create a {@link FromClause} from a table name and an alias + * + * @param name table name + * @param as table alias + * @return new instance + */ + public FromClause tableAs(final String name, final String as) { + this.tables.add(new Table(getRoot(), name, as)); + return this; + } + + /** + * Create a new {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause join(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.DEFAULT, name, specification)); + return this; + } + + /** + * Create a new inner {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause innerJoin(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.INNER, name, specification)); + return this; + } + + /** + * Create a new left {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause leftJoin(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.LEFT, name, specification)); + return this; + } + + /** + * Create a new right {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause rightJoin(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.RIGHT, name, specification)); + return this; + } + + /** + * Create a new full {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause fullJoin(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.FULL, name, specification)); + return this; + } + + /** + * Create a new left outer {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause leftOuterJoin(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.LEFT_OUTER, name, specification)); + return this; + } + + /** + * Create a new right outer {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause rightOuterJoin(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.RIGHT_OUTER, name, specification)); + return this; + } + + /** + * Create a new full outer {@link Join} that belongs to a FROM clause + * + * @param name name of the table to be joined + * @param specification join conditions + * @return parent FROM clause + */ + public FromClause fullOuterJoin(final String name, final String specification) { + this.joins.add(new Join(getRoot(), JoinType.FULL_OUTER, name, specification)); + return this; + } + + @Override + 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 new file mode 100644 index 00000000..bfed5d67 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Join.java @@ -0,0 +1,60 @@ +package com.exasol.sql.dql; + +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 SelectFragment { + private final JoinType type; + private final String name; + private final String specification; + + /** + * 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 Fragment root, final JoinType type, final String name, final String specification) { + super(root); + this.type = type; + this.name = name; + this.specification = specification; + } + + /** + * Get the type of the join + * + * @return join type (e.g. INNER or LEFT) + */ + public JoinType getType() { + return this.type; + } + + /** + * Get the name of the joined table + * + * @return name of the joined table + */ + public String getName() { + return this.name; + } + + /** + * Get the join specification + * + * @return join specification + */ + public String getSpecification() { + return this.specification; + } + + @Override + 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/JoinType.java b/src/main/java/com/exasol/sql/dql/JoinType.java new file mode 100644 index 00000000..050849fc --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/JoinType.java @@ -0,0 +1,27 @@ +package com.exasol.sql.dql; + +/** + * This class represents the {@link Join} types supported by SQL. + * + *
+ * DEFAULT = INNER     : ( (*) )
+ * LEFT = LEFT_OUTER   : (*(*) )
+ * RIGHT = RIGHT_OUTER : ( (*)*)
+ * FULL = FULL_OUTER   : (*(*)*)
+ * 
+ */ +public enum JoinType { + DEFAULT(""), INNER("INNER"), LEFT("LEFT"), RIGHT("RIGHT"), FULL("FULL"), LEFT_OUTER("LEFT OUTER"), + RIGHT_OUTER("RIGHT OUTER"), FULL_OUTER("FULL OUTER"); + + private final String text; + + private JoinType(final String text) { + this.text = text; + } + + @Override + public String toString() { + return this.text; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/LimitClause.java b/src/main/java/com/exasol/sql/dql/LimitClause.java new file mode 100644 index 00000000..08be0050 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/LimitClause.java @@ -0,0 +1,73 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.AbstractFragment; +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. + */ +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 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 SqlStatement root, final int offset, final int count) { + super(root); + this.offset = offset; + this.count = count; + } + + /** + * Get the offset row for the limit + * + * @return first row which should be handed back + */ + public int getOffset() { + return this.offset; + } + + /** + * Get the maximum number of rows to be handed back + * + * @return maximum number of rows + */ + public int getCount() { + return this.count; + } + + /** + * Check if the limit clause has an offset + * + * @return true if the limit clause has an offset + */ + public boolean hasOffset() { + return this.offset > 0; + } + + @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 new file mode 100644 index 00000000..5cf6b9cb --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/Select.java @@ -0,0 +1,126 @@ +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, 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(null); + } + + /** + * Add a wildcard field for all involved fields. + * + * @return this instance for fluent programming + */ + public Select all() { + this.fields.add(new Field(this, "*")); + return this; + } + + /** + * Add one or more named fields. + * + * @param names field name + * @return this instance for fluent programming + */ + public Select field(final String... names) { + for (final String name : names) { + this.fields.add(new Field(this, name)); + } + return this; + } + + /** + * Get the {@link FromClause} of this select statement + * + * @return from clause + */ + // [impl->dsn~select-statement.out-of-order-clauses~1] + public synchronized FromClause from() { + if (this.fromClause == null) { + this.fromClause = new FromClause(this); + } + return this.fromClause; + } + + /** + * 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 + */ + // [impl->dsn~select-statement.out-of-order-clauses~1] + 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; + } + + /** + * 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 thisdsn~select-statement.out-of-order-clauses~1] + public synchronized Select limit(final int offset, 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, offset, count); + return this; + } + + /** + * Create a new {@link WhereClause} + * + * @param expression boolean expression that defines the filter criteria + * @return new instance + */ + // [impl->dsn~select-statement.out-of-order-clauses~1] + public synchronized Select where(final BooleanExpression expression) { + if (this.whereClause == null) { + this.whereClause = new WhereClause(this, expression); + } + return this; + } + + @Override + public void accept(final SelectVisitor visitor) { + visitor.visit(this); + for (final Field field : this.fields) { + field.accept(visitor); + } + if (this.fromClause != null) { + this.fromClause.accept(visitor); + } + if (this.whereClause != null) { + this.whereClause.accept(visitor); + } + if (this.limitClause != null) { + this.limitClause.accept(visitor); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/SelectFragment.java b/src/main/java/com/exasol/sql/dql/SelectFragment.java new file mode 100644 index 00000000..005efd73 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/SelectFragment.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.Fragment; + +/** + * This is the common interface for all fragments of a SELECT statement. + */ +public interface SelectFragment extends Fragment { + /** + * Accept a visitor (e.g. a renderer or validator) + * + * @param visitor visitor to accept + */ + public void accept(SelectVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/SelectVisitor.java b/src/main/java/com/exasol/sql/dql/SelectVisitor.java new file mode 100644 index 00000000..070c4b08 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/SelectVisitor.java @@ -0,0 +1,15 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.FragmentVisitor; + +public interface SelectVisitor extends FragmentVisitor { + public void visit(final Select select); + + public void visit(FromClause fromClause); + + public void visit(Join join); + + public void visit(LimitClause limitClause); + + public void visit(WhereClause whereClause); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/dql/WhereClause.java b/src/main/java/com/exasol/sql/dql/WhereClause.java new file mode 100644 index 00000000..80935738 --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/WhereClause.java @@ -0,0 +1,38 @@ +package com.exasol.sql.dql; + +import com.exasol.sql.AbstractFragment; +import com.exasol.sql.SqlStatement; +import com.exasol.sql.expression.BooleanExpression; + +/** + * This class represents the where clause of an SQL statement. It contains the filter criteria in form of a + * {@link BooleanExpression}. + */ +public class WhereClause extends AbstractFragment implements SelectFragment { + private final BooleanExpression expression; + + /** + * Create a new instance of a {@link WhereClause} + * + * @param root SQL statement this WHERE clause belongs to + * @param expression + */ + public WhereClause(final SqlStatement root, final BooleanExpression expression) { + super(root); + this.expression = expression; + } + + /** + * Get the boolean expression defining the filter criteria + * + * @return boolean expression + */ + public BooleanExpression getExpression() { + return this.expression; + } + + @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/rendering/SelectRenderer.java b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java new file mode 100644 index 00000000..93e2dead --- /dev/null +++ b/src/main/java/com/exasol/sql/dql/rendering/SelectRenderer.java @@ -0,0 +1,106 @@ +package com.exasol.sql.dql.rendering; + +import java.util.Optional; + +import com.exasol.sql.Field; +import com.exasol.sql.Table; +import com.exasol.sql.dql.*; +import com.exasol.sql.rendering.AbstractFragmentRenderer; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * The {@link SelectRenderer} turns SQL statement structures in to SQL strings. + */ +// [impl->dsn~rendering.sql.select~1] +public class SelectRenderer extends AbstractFragmentRenderer implements SelectVisitor { + /** + * Create a new {@link SelectRenderer} with custom render settings. + * + * @param config render configuration settings + */ + public SelectRenderer(final StringRendererConfig config) { + super(config); + } + + @Override + public void visit(final Select select) { + appendKeyWord("SELECT "); + setLastVisited(select); + } + + @Override + public void visit(final Field field) { + appendCommaWhenNeeded(field); + appendAutoQuoted(field.getName()); + setLastVisited(field); + } + + @Override + public void visit(final FromClause fromClause) { + appendKeyWord(" FROM "); + setLastVisited(fromClause); + } + + @Override + public void visit(final Table table) { + appendCommaWhenNeeded(table); + appendAutoQuoted(table.getName()); + final Optional 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 "); + appendAutoQuoted(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 an {@link SelectRenderer} using the default renderer configuration + * + * @return select renderer + */ + public static SelectRenderer create() { + return create(StringRendererConfig.createDefault()); + } + + /** + * Create an {@link SelectRenderer} + * + * @param config renderer configuration + * @return select renderer + */ + public static SelectRenderer create(final StringRendererConfig config) { + return new SelectRenderer(config); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java b/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java new file mode 100644 index 00000000..03040d2c --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/AbstractBooleanExpression.java @@ -0,0 +1,46 @@ +package com.exasol.sql.expression; + +import com.exasol.util.AbstractBottomUpTreeNode; +import com.exasol.util.TreeNode; + +/** + * Abstract base class for all types of BooleanExpressions + */ +public abstract class AbstractBooleanExpression extends AbstractBottomUpTreeNode implements BooleanExpression { + protected AbstractBooleanExpression() { + super(); + } + + protected AbstractBooleanExpression(final BooleanExpression expression) { + super(expression); + } + + protected AbstractBooleanExpression(final BooleanExpression... expressions) { + super(expressions); + } + + @Override + public void accept(final BooleanExpressionVisitor visitor) { + acceptConcrete(visitor); + for (final TreeNode child : this.getChildren()) { + ((BooleanExpression) child).accept(visitor); + } + dismissConcrete(visitor); + } + + /** + * Sub-classes must override this method so that the visitor knows the type of + * the visited class at compile time. + * + * @param visitor visitor to accept + */ + public abstract void acceptConcrete(final BooleanExpressionVisitor visitor); + + /** + * Sub-classes must override this method so that the visitor knows the type of + * the visited class at compile time. + * + * @param visitor visitor to accept + */ + public abstract void dismissConcrete(final BooleanExpressionVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/AbstractValueExpression.java b/src/main/java/com/exasol/sql/expression/AbstractValueExpression.java new file mode 100644 index 00000000..fcf1f26f --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/AbstractValueExpression.java @@ -0,0 +1,15 @@ +package com.exasol.sql.expression; + +import com.exasol.util.AbstractBottomUpTreeNode; + +/** + * Abstract base class for all types of value expressions + */ +public abstract class AbstractValueExpression extends AbstractBottomUpTreeNode implements ValueExpression { + /** + * Create a new instance of a {@link AbstractValueExpression} + */ + public AbstractValueExpression() { + super(); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/And.java b/src/main/java/com/exasol/sql/expression/And.java new file mode 100644 index 00000000..7e1ddd5a --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/And.java @@ -0,0 +1,34 @@ +package com.exasol.sql.expression; + +/** + * This class represents + */ +public class And extends AbstractBooleanExpression { + /** + * Create a new {@link And} instance + * + * @param expressions boolean expressions to be connected by a logical AND + */ + public And(final BooleanExpression... expressions) { + super(expressions); + } + + /** + * Create a new {@link And} instance + * + * @param strings string literals to be connected by a logical AND + */ + public And(final String... strings) { + this(Literal.toBooleanExpressions(strings)); + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpression.java b/src/main/java/com/exasol/sql/expression/BooleanExpression.java new file mode 100644 index 00000000..2bf25f9b --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanExpression.java @@ -0,0 +1,15 @@ +package com.exasol.sql.expression; + +import com.exasol.util.TreeNode; + +/** + * Common interface for all types of boolean expressions + */ +public interface BooleanExpression extends TreeNode { + /** + * Accept a visitor + * + * @param visitor visitor to accept + */ + public void accept(final BooleanExpressionVisitor visitor); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java new file mode 100644 index 00000000..279574a2 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanExpressionVisitor.java @@ -0,0 +1,26 @@ +package com.exasol.sql.expression; + +/** + * Visitor interface for a {@link BooleanTerm} + */ +public interface BooleanExpressionVisitor { + public void visit(Not not); + + public void visit(Literal literal); + + public void visit(And and); + + public void leave(Not not); + + public void leave(Literal literal); + + public void leave(And and); + + public void visit(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 new file mode 100644 index 00000000..a66e81e3 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/BooleanTerm.java @@ -0,0 +1,116 @@ +package com.exasol.sql.expression; + +// [impl->dsn~boolean-operators~1] +public abstract class BooleanTerm extends AbstractBooleanExpression { + private BooleanTerm() { + super(); + } + + public static BooleanExpression not(final String string) { + return new Not(string); + } + + public static BooleanExpression not(final BooleanExpression expression) { + return new Not(expression); + } + + public static BooleanExpression and(final String... strings) { + return new And(strings); + } + + public static BooleanExpression and(final BooleanExpression expression, final String string) { + return new And(expression, Literal.of(string)); + } + + public static BooleanExpression and(final String literal, final BooleanExpression expression) { + return new And(Literal.of(literal), expression); + } + + public static BooleanExpression and(final BooleanExpression... expressions) { + return new And(expressions); + } + + public static BooleanExpression or(final String... strings) { + return new Or(strings); + } + + public static BooleanExpression or(final BooleanExpression expression, final String string) { + return new Or(expression, Literal.of(string)); + } + + public static BooleanExpression or(final String literal, final BooleanExpression expression) { + return new Or(Literal.of(literal), expression); + } + + public static BooleanExpression or(final BooleanExpression... expressions) { + return new Or(expressions); + } + + // [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)); + } + + // [impl->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..0fdf2eb3 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Comparison.java @@ -0,0 +1,52 @@ +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; + + // [impl->dsn~boolean-operation.comparison.constructing-from-enum~1] + 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/Literal.java b/src/main/java/com/exasol/sql/expression/Literal.java new file mode 100644 index 00000000..879212a5 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Literal.java @@ -0,0 +1,48 @@ +package com.exasol.sql.expression; + +public class Literal extends AbstractBooleanExpression { + private final String literal; + + private Literal(final String literal) { + this.literal = literal; + } + + /** + * Create a new {@link Literal} instance from a String + * + * @param string the string to be turned into a literal + * @return new Literal instance + */ + public static Literal of(final String string) { + return new Literal(string); + } + + @Override + public String toString() { + return this.literal; + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } + + /** + * Map an array of {@link String} to and array of BooleanExpressions + * + * @param strings + * @return + */ + public static BooleanExpression[] toBooleanExpressions(final String[] strings) { + final BooleanExpression[] literals = new BooleanExpression[strings.length]; + for (int i = 0; i < strings.length; ++i) { + literals[i] = Literal.of(strings[i]); + } + return literals; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Not.java b/src/main/java/com/exasol/sql/expression/Not.java new file mode 100644 index 00000000..f67bddde --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Not.java @@ -0,0 +1,34 @@ +package com.exasol.sql.expression; + +/** + * This class implements the logical unary NOT + */ +public class Not extends AbstractBooleanExpression { + /** + * Create a new instance of a unary {@link Not} from a string literal + * + * @param string string literal to be negated + */ + protected Not(final String string) { + super(Literal.of(string)); + } + + /** + * Create a new instance of a unary {@link Not} from a boolean expression + * + * @param expression boolean expression literal to be negated + */ + public Not(final BooleanExpression expression) { + super(expression); + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Or.java b/src/main/java/com/exasol/sql/expression/Or.java new file mode 100644 index 00000000..8fdc36bd --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Or.java @@ -0,0 +1,34 @@ +package com.exasol.sql.expression; + +/** + * This class represents + */ +public class Or extends AbstractBooleanExpression { + /** + * Create a new {@link Or} instance + * + * @param expressions boolean expressions to be connected by a logical Or + */ + public Or(final BooleanExpression... expressions) { + super(expressions); + } + + /** + * Create a new {@link Or} instance + * + * @param strings string literals to be connected by a logical Or + */ + public Or(final String... strings) { + this(Literal.toBooleanExpressions(strings)); + } + + @Override + public void acceptConcrete(final BooleanExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void dismissConcrete(final BooleanExpressionVisitor visitor) { + visitor.leave(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/Value.java b/src/main/java/com/exasol/sql/expression/Value.java new file mode 100644 index 00000000..c98e39f1 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/Value.java @@ -0,0 +1,31 @@ +package com.exasol.sql.expression; + +/** + * This class represents a concrete value link a number or a text. + */ +public class Value extends AbstractValueExpression { + private final Object value; + + /** + * Create a new instance of a {@link Value} + * + * @param value contained value + */ + public Value(final Object value) { + this.value = value; + } + + /** + * Get the value + * + * @return value + */ + public Object get() { + return this.value; + } + + @Override + public void accept(final ValueExpressionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/ValueExpression.java b/src/main/java/com/exasol/sql/expression/ValueExpression.java new file mode 100644 index 00000000..e705c907 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/ValueExpression.java @@ -0,0 +1,7 @@ +package com.exasol.sql.expression; + +import com.exasol.util.TreeNode; + +public interface ValueExpression extends TreeNode { + void accept(ValueExpressionVisitor visitor); +} diff --git a/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java b/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java new file mode 100644 index 00000000..f780eb51 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/ValueExpressionVisitor.java @@ -0,0 +1,12 @@ +package com.exasol.sql.expression; + +import com.exasol.sql.UnnamedPlaceholder; + +/** + * Visitor interface for a {@link BooleanTerm} + */ +public interface ValueExpressionVisitor { + void visit(Value value); + + void visit(UnnamedPlaceholder unnamedPlaceholder); +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java new file mode 100644 index 00000000..47af626e --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/AbstractExpressionRenderer.java @@ -0,0 +1,60 @@ +package com.exasol.sql.expression.rendering; + +import java.util.Stack; + +import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * Common base class for expression renderers + */ +public class AbstractExpressionRenderer { + protected final StringRendererConfig config; + protected final StringBuilder builder = new StringBuilder(); + protected final Stack connectorStack = new Stack<>(); + + public AbstractExpressionRenderer(final StringRendererConfig config) { + this.config = config; + } + + protected void appendKeyword(final String keyword) { + this.builder.append(this.config.useLowerCase() ? keyword.toLowerCase() : keyword); + } + + protected void connect(final BooleanExpression expression) { + if (expression.isChild() && !expression.isFirstSibling()) { + appendConnector(); + } + } + + private void appendConnector() { + if (!this.connectorStack.isEmpty()) { + appendKeyword(this.connectorStack.peek()); + } + } + + protected void appendLiteral(final String string) { + this.builder.append(string); + } + + protected void startParenthesis() { + this.builder.append("("); + } + + protected void endParenthesis(final BooleanExpression expression) { + this.builder.append(")"); + } + + /** + * Render expression to a string + * + * @return rendered string + */ + public String render() { + return this.builder.toString(); + } + + protected void append(final String string) { + this.builder.append(string); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java new file mode 100644 index 00000000..bb112496 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/BooleanExpressionRenderer.java @@ -0,0 +1,91 @@ +package com.exasol.sql.expression.rendering; + +import com.exasol.sql.expression.*; +import com.exasol.sql.rendering.StringRendererConfig; + +public class BooleanExpressionRenderer extends AbstractExpressionRenderer implements BooleanExpressionVisitor { + public BooleanExpressionRenderer(final StringRendererConfig config) { + super(config); + } + + public BooleanExpressionRenderer() { + this(StringRendererConfig.builder().build()); + } + + @Override + public void visit(final Not not) { + connect(not); + appendKeyword("NOT"); + startParenthesis(); + } + + @Override + public void leave(final Not not) { + endParenthesis(not); + } + + @Override + public void visit(final And and) { + connect(and); + this.connectorStack.push(" AND "); + if (!and.isRoot()) { + startParenthesis(); + } + } + + @Override + public void leave(final And and) { + if (!and.isRoot()) { + endParenthesis(and); + } + this.connectorStack.pop(); + } + + @Override + public void visit(final Or or) { + connect(or); + this.connectorStack.push(" OR "); + if (!or.isRoot()) { + startParenthesis(); + } + } + + @Override + public void leave(final Or or) { + if (!or.isRoot()) { + endParenthesis(or); + } + this.connectorStack.pop(); + } + + @Override + public void visit(final Literal literal) { + connect(literal); + appendLiteral(literal.toString()); + } + + @Override + public void leave(final Literal literal) { + // intentionally empty + } + + @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/expression/rendering/ValueExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java new file mode 100644 index 00000000..53eac8d7 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java @@ -0,0 +1,32 @@ +package com.exasol.sql.expression.rendering; + +import com.exasol.sql.UnnamedPlaceholder; +import com.exasol.sql.expression.Value; +import com.exasol.sql.expression.ValueExpressionVisitor; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * Renderer for common value expressions + */ +public class ValueExpressionRenderer extends AbstractExpressionRenderer implements ValueExpressionVisitor { + public ValueExpressionRenderer(final StringRendererConfig config) { + super(config); + } + + @Override + public void visit(final Value value) { + final Object object = value.get(); + if (object instanceof String) { + append("'"); + append((String) object); + append("'"); + } else { + this.builder.append(value.get().toString()); + } + } + + @Override + public void visit(final UnnamedPlaceholder unnamedPlaceholder) { + append("?"); + } +} \ 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..ef7c8f02 --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/AbstractFragmentRenderer.java @@ -0,0 +1,99 @@ +package com.exasol.sql.rendering; + +import com.exasol.sql.Fragment; +import com.exasol.sql.expression.BooleanExpression; +import com.exasol.sql.expression.ValueExpression; +import com.exasol.sql.expression.rendering.BooleanExpressionRenderer; +import com.exasol.sql.expression.rendering.ValueExpressionRenderer; + +/** + * 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; + } + + // [impl->dsn~rendering.sql.configurable-case~1] + protected void appendKeyWord(final String keyword) { + append(this.config.useLowerCase() ? 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); + } + + protected void appendRenderedValueExpression(final ValueExpression expression) { + final ValueExpressionRenderer renderer = new ValueExpressionRenderer(this.config); + expression.accept(renderer); + append(renderer.render()); + } + + // [impl->dsn~rendering.add-double-quotes-for-schema-table-and-column-identifiers~1] + protected void appendAutoQuoted(final String identifier) { + if (this.config.useQuotes()) { + appendQuoted(identifier); + } else { + append(identifier); + } + } + + private void appendQuoted(final String identifier) { + boolean first = true; + for (final String part : identifier.split("\\.")) { + if (!first) { + append("."); + } + quoteIdentiferPart(part); + first = false; + } + } + + private void quoteIdentiferPart(final String part) { + if ("*".equals(part)) { + append("*"); + } else { + if (!part.startsWith("\"")) { + append("\""); + } + append(part); + if (!part.endsWith("\"")) { + append("\""); + } + } + } + + @Override + public String render() { + return this.builder.toString(); + } +} \ 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/StringRendererConfig.java b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java new file mode 100644 index 00000000..cbc79551 --- /dev/null +++ b/src/main/java/com/exasol/sql/rendering/StringRendererConfig.java @@ -0,0 +1,94 @@ +package com.exasol.sql.rendering; + +import com.exasol.sql.StatementFactory; + +/** + * This class implements a parameter object containing the configuration options for the {@link StatementFactory}. + */ +public class StringRendererConfig { + private final boolean lowerCase; + private final boolean quote; + + private StringRendererConfig(final Builder builder) { + this.lowerCase = builder.lowerCase; + this.quote = builder.quote; + } + + /** + * Get whether the statements should be produced in lower case. + * + * @return true if statements are produced in lower case + */ + public boolean useLowerCase() { + return this.lowerCase; + } + + /** + * Get whether identifiers should be enclosed in double quotation marks. + * + * @return true if should be enclosed in quotes + */ + public boolean useQuotes() { + return this.quote; + } + + /** + * Create the default configuration. + * + * @return default configuration + */ + public static StringRendererConfig createDefault() { + return builder().build(); + } + + /** + * Get a builder for {@link StringRendererConfig} + * + * @return builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link StringRendererConfig} + */ + public static class Builder { + private boolean lowerCase = false; + private boolean quote = false; + + private Builder() { + } + + /** + * Create a new instance of a {@link StringRendererConfig} + * + * @return new instance + */ + public StringRendererConfig build() { + return new StringRendererConfig(this); + } + + /** + * Define whether the statement should be produced in lower case + * + * @param lowerCase set to true if the statement should be produced in lower case + * @return this instance for fluent programming + */ + public Builder lowerCase(final boolean lowerCase) { + this.lowerCase = lowerCase; + return this; + } + + /** + * Define whether schema, table and field identifiers should be enclosed in double quotation marks. + * + * @param quote set to true if identifiers should be enclosed in quotes + * @return this instance for fluent programming + */ + public Builder quoteIdentifiers(final boolean quote) { + this.quote = quote; + return this; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java new file mode 100644 index 00000000..a71be4f9 --- /dev/null +++ b/src/main/java/com/exasol/util/AbstractBottomUpTreeNode.java @@ -0,0 +1,105 @@ +package com.exasol.util; + +import java.util.*; + +/** + * This is an abstract base class for nodes in a tree structure. + */ +public abstract class AbstractBottomUpTreeNode implements TreeNode { + private TreeNode parent = null; + private final List children; + + /** + * Create a new instance of a {@link AbstractBottomUpTreeNode} that serves as + * leaf node for a tree. + */ + public AbstractBottomUpTreeNode() { + this.children = Collections.emptyList(); + } + + /** + * Create a new instance of a {@link AbstractBottomUpTreeNode}. + * + * @param children child nodes to be linked to this node. + */ + public AbstractBottomUpTreeNode(final List children) { + this.children = children; + for (final TreeNode child : children) { + assignThisAsParentTo(child); + } + } + + /** + * Create a new instance of a {@link AbstractBottomUpTreeNode}. + * + * @param children child nodes to be linked to this node. + */ + public AbstractBottomUpTreeNode(final TreeNode... children) { + this(Arrays.asList(children)); + } + + private void assignThisAsParentTo(final TreeNode child) { + assertChildType(child); + final TreeNode existingParent = child.getParent(); + if (existingParent == null) { + ((AbstractBottomUpTreeNode) child).parent = this; + } else { + throw new IllegalStateException( + "Tried to link node \"" + child.toString() + "\" in bottom-up tree to parent \"" + this.toString() + + "\" which already has a parent \"" + existingParent + "\""); + } + } + + private void assertChildType(final TreeNode child) { + if (!(child instanceof AbstractBottomUpTreeNode)) { + throw new IllegalArgumentException("A bottom up tree can only be constructed from nodes of type \"" + + AbstractBottomUpTreeNode.class.getName() + "\" but got an object of type \"" + + child.getClass().getName() + "\""); + } + } + + @Override + public TreeNode getRoot() { + if (getParent() == null) { + return this; + } else { + return getParent().getRoot(); + } + } + + @Override + public TreeNode getParent() { + return this.parent; + } + + @Override + public void addChild(final TreeNode child) { + throw new UnsupportedOperationException("Node \"" + child.toString() + + "\" can only be added as child node in parent constructor in a bottom-up tree."); + } + + @Override + public List getChildren() { + return this.children; + } + + @Override + public TreeNode getChild(final int index) { + return this.children.get(index); + } + + @Override + public boolean isRoot() { + return (this == getRoot()); + } + + @Override + public boolean isChild() { + return (this.parent != null); + } + + @Override + public boolean isFirstSibling() { + return (this.parent != null) && (this.getParent().getChild(0) == this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/util/AbstractTreeNode.java b/src/main/java/com/exasol/util/AbstractTreeNode.java new file mode 100644 index 00000000..02227d82 --- /dev/null +++ b/src/main/java/com/exasol/util/AbstractTreeNode.java @@ -0,0 +1,82 @@ +package com.exasol.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is an abstract base class for nodes in a tree structure. + */ +public abstract class AbstractTreeNode implements TreeNode { + private TreeNode root; + private TreeNode parent; + private final List children = new ArrayList<>(); + + /** + * Create a new instance of a {@link AbstractTreeNode} that serves as root for a + * tree. + */ + public AbstractTreeNode() { + this.root = this; + this.parent = null; + } + + /** + * Link to a parent node + * + * @param parent the parent to which this node will be linked as a child + * + * @throws IllegalArgumentException if parent is null or parent and + * child are identical + */ + public void setParent(final TreeNode parent) throws IllegalArgumentException { + if (parent == null) { + throw new IllegalArgumentException("Parent tree node cannot be NULL."); + } else if (parent == this) { + throw new IllegalArgumentException("Parent tree node cannot be the same as child tree node."); + } else { + this.parent = parent; + this.root = this.parent.getRoot(); + } + } + + @Override + public TreeNode getRoot() { + return this.root; + } + + @Override + public TreeNode getParent() { + return this.parent; + } + + @Override + public void addChild(final TreeNode child) { + this.children.add(child); + ((AbstractTreeNode) child).setParent(this); + } + + @Override + public List getChildren() { + return this.children; + } + + @Override + public TreeNode getChild(final int index) { + return this.children.get(index); + } + + @Override + public boolean isRoot() { + return (this == getRoot()); + } + + @Override + public boolean isChild() { + return (this.parent != null); + } + + @Override + public boolean isFirstSibling() { + return (this.parent != null) && (this.getParent().getChild(0) == this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/util/TreeNode.java b/src/main/java/com/exasol/util/TreeNode.java new file mode 100644 index 00000000..3ac7d172 --- /dev/null +++ b/src/main/java/com/exasol/util/TreeNode.java @@ -0,0 +1,72 @@ +package com.exasol.util; + +import java.util.List; + +/** + * This class represents a node in a tree structure. + */ +public interface TreeNode { + /** + * Get the root of the tree + * + * @return root node + */ + public TreeNode getRoot(); + + /** + * Get the parent of this node + * + * @return parent node + */ + public TreeNode getParent(); + + /** + * Add a child node below this node. Children are registered in the order in + * which they are added. + *

+ * Important: this also automatically creates a link in the + * opposite direction. All implementations must adhere to this rule. + * + * @param child child node + */ + public void addChild(TreeNode child); + + /** + * Get all child nodes of this node + * + * @param child child nodes + */ + public List getChildren(); + + /** + * Get child node by position in the list of siblings. The position depends on + * the order in which the children were added. + * + * @param index position in the list of siblings + * @return child node at position + * @throws IndexOutOfBoundsException if the index is out of range (index < 0 || + * index >= size()) + */ + public TreeNode getChild(int index) throws IndexOutOfBoundsException; + + /** + * Check whether this node is the root of the tree. + * + * @return true if this node is the root + */ + public boolean isRoot(); + + /** + * Check whether this node is a child node + * + * @return true if the node is a child of another node + */ + public boolean isChild(); + + /** + * Check whether a child is the first in the list of siblings + * + * @return true if the child is the first in the list of siblings + */ + public boolean isFirstSibling(); +} \ No newline at end of file diff --git a/src/test/java/com/exasol/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