From fd2ca2a7fdac1ce7b3997c19734c2f0d60cab85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ch=CF=80?= Date: Thu, 30 Sep 2021 10:02:11 +0200 Subject: [PATCH] Feature #72: Support aggregate and analytics functions (#122) * Increment minor version, add change notes * Update changelog version to 4.5.0 * Rename constant to match naming conventions * Add AnalyticFunction with keyword support * Remove duplicate code * Document creating analytic functions * Initial support for OVER clause * Add order by to over clause * Move analytic & aggregate functions to new enum * Implement units and exclusion for over clause * Apply suggestions from code review Co-authored-by: jakobbraun * Remove WindowClause This will be implemented in #128 * Update dependencies in changelog * Add javadoc * Add unit tests * Update user guide * Update list of aggregate & analytic functions * Format function names and add links to the documentation * Update release date * Apply suggestions from code review Co-authored-by: jakobbraun * Implement review findings Co-authored-by: jakobbraun --- dependencies.md | 131 ++--- doc/changes/changelog.md | 2 +- .../{changes_4.4.3.md => changes_4.5.0.md} | 18 +- .../list_of_supported_exasol_functions.md | 469 +++++++++--------- doc/user_guide/statements/select.md | 144 ++++++ pom.xml | 7 +- .../exasol/sql/dql/select/GroupByClause.java | 6 +- .../com/exasol/sql/dql/select/Select.java | 8 +- .../dql/select/rendering/SelectRenderer.java | 11 +- .../expression/function/FunctionVisitor.java | 6 +- .../function/exasol/AnalyticFunction.java | 112 +++++ .../exasol/ExasolAggregateFunction.java | 6 +- .../ExasolAnalyticAggregateFunctions.java | 265 ++++++++++ .../exasol/ExasolAnalyticFunction.java | 12 +- .../function/exasol/ExasolFunction.java | 16 +- .../function/exasol/OverClause.java | 112 +++++ .../function/exasol/WindowFrameClause.java | 214 ++++++++ .../rendering/OverClauseRenderer.java | 102 ++++ .../rendering/ValueExpressionRenderer.java | 39 +- .../exasol/hamcrest/ExceptionAssertions.java | 42 ++ .../select/rendering/TestSelectRendering.java | 144 +++++- .../rendering/OverClauseRendererTest.java | 64 +++ 22 files changed, 1584 insertions(+), 346 deletions(-) rename doc/changes/{changes_4.4.3.md => changes_4.5.0.md} (51%) create mode 100644 src/main/java/com/exasol/sql/expression/function/exasol/AnalyticFunction.java create mode 100644 src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticAggregateFunctions.java create mode 100644 src/main/java/com/exasol/sql/expression/function/exasol/OverClause.java create mode 100644 src/main/java/com/exasol/sql/expression/function/exasol/WindowFrameClause.java create mode 100644 src/main/java/com/exasol/sql/expression/rendering/OverClauseRenderer.java create mode 100644 src/test/java/com/exasol/hamcrest/ExceptionAssertions.java create mode 100644 src/test/java/com/exasol/sql/expression/rendering/OverClauseRendererTest.java diff --git a/dependencies.md b/dependencies.md index 02b4d3f2..b65f8415 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,73 +1,80 @@ # Dependencies +## Compile Dependencies + +| Dependency | License | +| ------------------------- | -------- | +| [error-reporting-java][0] | [MIT][1] | + ## Test Dependencies -| Dependency | License | -| ------------------------------- | -------------------------------- | -| [JUnit Jupiter (Aggregator)][0] | [Eclipse Public License v2.0][1] | -| [Hamcrest All][2] | [New BSD License][3] | -| [mockito-core][4] | [The MIT License][5] | -| [mockito-junit-jupiter][4] | [The MIT License][5] | -| [EqualsVerifier][8] | [Apache License, Version 2.0][9] | +| Dependency | License | +| ------------------------------- | --------------------------------- | +| [JUnit Jupiter (Aggregator)][2] | [Eclipse Public License v2.0][3] | +| [Hamcrest All][4] | [New BSD License][5] | +| [mockito-core][6] | [The MIT License][7] | +| [mockito-junit-jupiter][6] | [The MIT License][7] | +| [EqualsVerifier][10] | [Apache License, Version 2.0][11] | ## Plugin Dependencies | Dependency | License | | ------------------------------------------------------- | ------------------------------------- | -| [Apache Maven Compiler Plugin][10] | [Apache License, Version 2.0][11] | -| [JaCoCo :: Maven Plugin][12] | [Eclipse Public License 2.0][13] | -| [Maven Surefire Plugin][14] | [Apache License, Version 2.0][11] | -| [Apache Maven Source Plugin][16] | [Apache License, Version 2.0][11] | -| [Apache Maven Javadoc Plugin][18] | [Apache License, Version 2.0][11] | -| [Apache Maven GPG Plugin][20] | [Apache License, Version 2.0][11] | -| [OpenFastTrace Maven Plugin][22] | [GNU General Public License v3.0][23] | -| [org.sonatype.ossindex.maven:ossindex-maven-plugin][24] | [ASL2][9] | -| [Versions Maven Plugin][26] | [Apache License, Version 2.0][11] | -| [Apache Maven Enforcer Plugin][28] | [Apache License, Version 2.0][11] | -| [Nexus Staging Maven Plugin][30] | [Eclipse Public License][31] | -| [Project keeper maven plugin][32] | [MIT][33] | -| [Maven Failsafe Plugin][34] | [Apache License, Version 2.0][11] | -| [Apache Maven Deploy Plugin][36] | [Apache License, Version 2.0][9] | -| [error-code-crawler-maven-plugin][38] | [MIT][33] | -| [Reproducible Build Maven Plugin][40] | [Apache 2.0][9] | -| [Apache Maven Clean Plugin][42] | [Apache License, Version 2.0][11] | -| [Apache Maven Resources Plugin][44] | [Apache License, Version 2.0][11] | -| [Apache Maven JAR Plugin][46] | [Apache License, Version 2.0][11] | -| [Apache Maven Install Plugin][48] | [Apache License, Version 2.0][9] | -| [Apache Maven Site Plugin][50] | [Apache License, Version 2.0][11] | +| [Apache Maven Compiler Plugin][12] | [Apache License, Version 2.0][13] | +| [JaCoCo :: Maven Plugin][14] | [Eclipse Public License 2.0][15] | +| [Maven Surefire Plugin][16] | [Apache License, Version 2.0][13] | +| [Apache Maven Source Plugin][18] | [Apache License, Version 2.0][13] | +| [Apache Maven Javadoc Plugin][20] | [Apache License, Version 2.0][13] | +| [Apache Maven GPG Plugin][22] | [Apache License, Version 2.0][13] | +| [OpenFastTrace Maven Plugin][24] | [GNU General Public License v3.0][25] | +| [org.sonatype.ossindex.maven:ossindex-maven-plugin][26] | [ASL2][11] | +| [Versions Maven Plugin][28] | [Apache License, Version 2.0][13] | +| [Apache Maven Enforcer Plugin][30] | [Apache License, Version 2.0][13] | +| [Nexus Staging Maven Plugin][32] | [Eclipse Public License][33] | +| [Project keeper maven plugin][34] | [MIT][1] | +| [Maven Failsafe Plugin][36] | [Apache License, Version 2.0][13] | +| [Apache Maven Deploy Plugin][38] | [Apache License, Version 2.0][11] | +| [error-code-crawler-maven-plugin][40] | [MIT][1] | +| [Reproducible Build Maven Plugin][42] | [Apache 2.0][11] | +| [Apache Maven Clean Plugin][44] | [Apache License, Version 2.0][13] | +| [Apache Maven Resources Plugin][46] | [Apache License, Version 2.0][13] | +| [Apache Maven JAR Plugin][48] | [Apache License, Version 2.0][13] | +| [Apache Maven Install Plugin][50] | [Apache License, Version 2.0][11] | +| [Apache Maven Site Plugin][52] | [Apache License, Version 2.0][13] | -[32]: https://github.com/exasol/project-keeper-maven-plugin -[2]: https://github.com/hamcrest/JavaHamcrest -[9]: http://www.apache.org/licenses/LICENSE-2.0.txt -[14]: https://maven.apache.org/surefire/maven-surefire-plugin/ -[30]: http://www.sonatype.com/public-parent/nexus-maven-plugins/nexus-staging/nexus-staging-maven-plugin/ -[4]: https://github.com/mockito/mockito -[33]: https://opensource.org/licenses/MIT -[34]: https://maven.apache.org/surefire/maven-failsafe-plugin/ -[26]: http://www.mojohaus.org/versions-maven-plugin/ -[10]: https://maven.apache.org/plugins/maven-compiler-plugin/ -[44]: https://maven.apache.org/plugins/maven-resources-plugin/ -[22]: https://github.com/itsallcode/openfasttrace-maven-plugin -[42]: https://maven.apache.org/plugins/maven-clean-plugin/ -[13]: https://www.eclipse.org/legal/epl-2.0/ -[31]: http://www.eclipse.org/legal/epl-v10.html -[12]: https://www.jacoco.org/jacoco/trunk/doc/maven.html -[5]: https://github.com/mockito/mockito/blob/main/LICENSE -[40]: http://zlika.github.io/reproducible-build-maven-plugin -[50]: https://maven.apache.org/plugins/maven-site-plugin/ -[23]: https://www.gnu.org/licenses/gpl-3.0.html -[11]: https://www.apache.org/licenses/LICENSE-2.0.txt -[28]: https://maven.apache.org/enforcer/maven-enforcer-plugin/ -[1]: https://www.eclipse.org/legal/epl-v20.html -[3]: http://www.opensource.org/licenses/bsd-license.php -[48]: http://maven.apache.org/plugins/maven-install-plugin/ -[0]: https://junit.org/junit5/ -[24]: https://sonatype.github.io/ossindex-maven/maven-plugin/ -[20]: https://maven.apache.org/plugins/maven-gpg-plugin/ -[8]: http://www.jqno.nl/equalsverifier -[16]: https://maven.apache.org/plugins/maven-source-plugin/ -[36]: http://maven.apache.org/plugins/maven-deploy-plugin/ -[18]: https://maven.apache.org/plugins/maven-javadoc-plugin/ -[38]: https://github.com/exasol/error-code-crawler-maven-plugin -[46]: https://maven.apache.org/plugins/maven-jar-plugin/ +[34]: https://github.com/exasol/project-keeper-maven-plugin +[0]: https://github.com/exasol/error-reporting-java +[4]: https://github.com/hamcrest/JavaHamcrest +[11]: http://www.apache.org/licenses/LICENSE-2.0.txt +[16]: https://maven.apache.org/surefire/maven-surefire-plugin/ +[32]: http://www.sonatype.com/public-parent/nexus-maven-plugins/nexus-staging/nexus-staging-maven-plugin/ +[1]: https://opensource.org/licenses/MIT +[6]: https://github.com/mockito/mockito +[36]: https://maven.apache.org/surefire/maven-failsafe-plugin/ +[28]: http://www.mojohaus.org/versions-maven-plugin/ +[12]: https://maven.apache.org/plugins/maven-compiler-plugin/ +[46]: https://maven.apache.org/plugins/maven-resources-plugin/ +[24]: https://github.com/itsallcode/openfasttrace-maven-plugin +[44]: https://maven.apache.org/plugins/maven-clean-plugin/ +[15]: https://www.eclipse.org/legal/epl-2.0/ +[33]: http://www.eclipse.org/legal/epl-v10.html +[14]: https://www.jacoco.org/jacoco/trunk/doc/maven.html +[7]: https://github.com/mockito/mockito/blob/main/LICENSE +[42]: http://zlika.github.io/reproducible-build-maven-plugin +[52]: https://maven.apache.org/plugins/maven-site-plugin/ +[25]: https://www.gnu.org/licenses/gpl-3.0.html +[13]: https://www.apache.org/licenses/LICENSE-2.0.txt +[30]: https://maven.apache.org/enforcer/maven-enforcer-plugin/ +[3]: https://www.eclipse.org/legal/epl-v20.html +[5]: http://www.opensource.org/licenses/bsd-license.php +[50]: http://maven.apache.org/plugins/maven-install-plugin/ +[2]: https://junit.org/junit5/ +[26]: https://sonatype.github.io/ossindex-maven/maven-plugin/ +[22]: https://maven.apache.org/plugins/maven-gpg-plugin/ +[10]: http://www.jqno.nl/equalsverifier +[18]: https://maven.apache.org/plugins/maven-source-plugin/ +[38]: http://maven.apache.org/plugins/maven-deploy-plugin/ +[20]: https://maven.apache.org/plugins/maven-javadoc-plugin/ +[40]: https://github.com/exasol/error-code-crawler-maven-plugin +[48]: https://maven.apache.org/plugins/maven-jar-plugin/ diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md index 3d7f896a..40fac860 100644 --- a/doc/changes/changelog.md +++ b/doc/changes/changelog.md @@ -1,6 +1,6 @@ # Changes -* [4.4.3](changes_4.4.3.md) +* [4.5.0](changes_4.5.0.md) * [4.4.2](changes_4.4.2.md) * [4.4.1](changes_4.4.1.md) * [4.4.0](changes_4.4.0.md) diff --git a/doc/changes/changes_4.4.3.md b/doc/changes/changes_4.5.0.md similarity index 51% rename from doc/changes/changes_4.4.3.md rename to doc/changes/changes_4.5.0.md index d4b21291..f56bbfc1 100644 --- a/doc/changes/changes_4.4.3.md +++ b/doc/changes/changes_4.5.0.md @@ -1,6 +1,16 @@ -# Exasol SQL Statement Builder 4.4.3, released 2021-09-?? +# Exasol SQL Statement Builder 4.5.0, released 2021-09-30 -Code name: Internal refactorings on "More Predicates" +Code name: Support more Aggregate and Analytic functions + +## Summary + +This release supports all aggregate and analytics functsions provided by Exasol, e.g. `GROUPING[_ID]`, `PERCENTILE_CONT`, `NTH_VALUE` and many more. See the [ticket](https://github.com/exasol/sql-statement-builder/issues/72) for a complete list. + +We also added support for the keywords `DISTINCT` and `ANY` as well as the [over_clause](https://docs.exasol.com/sql_references/functions/analyticfunctions.htm?Highlight=over_clause) for analytic functions. See the [user guide](../user_guide/statements/select.md#analytic-functions) for how to use the name API. + +## Features + +* #72: Added support for more Aggregate and Analytic functions ## Refactoring @@ -13,6 +23,10 @@ Code name: Internal refactorings on "More Predicates" ## Dependency Updates +### Compile Dependency Updates + +* Added `com.exasol:error-reporting-java:0.4.0` + ### Test Dependency Updates * Updated `nl.jqno.equalsverifier:equalsverifier:3.6.1` to `3.7.1` diff --git a/doc/user_guide/list_of_supported_exasol_functions.md b/doc/user_guide/list_of_supported_exasol_functions.md index 1e68fb86..375db5dc 100644 --- a/doc/user_guide/list_of_supported_exasol_functions.md +++ b/doc/user_guide/list_of_supported_exasol_functions.md @@ -1,6 +1,6 @@ # List of Supported Exasol Functions -Here you can find a list of supported(or partly supported) functions for the Exasol database. +Here you can find a list of supported(or partly supported) functions for the Exasol database. See also the [alphabetical list of functions supported by Exasol](https://docs.exasol.com/sql_references/functions/all_functions.htm). - [Scalar Functions](#scalar-functions) - [Numeric Functions](#numeric-functions) @@ -10,254 +10,281 @@ Here you can find a list of supported(or partly supported) functions for the Exa - [Bitwise Function](#bitwise-function) - [Conversion Functions](#conversion-functions) - [Other Scalar Functions](#other-scalar-functions) +- [Aggregate and Analytic Functions](#aggregate-and-analytic-functions) - [Aggregate Functions](#aggregate-functions) - [Analytic Functions](#analytic-functions) ## Scalar Functions +Also see the list of [scalar functions](https://docs.exasol.com/sql_references/functions/scalarfunctions.htm) supported by Exasol. + ### Numeric Functions -- ABS -- ACOS -- ASIN -- ATAN -- ATAN2 -- CEIL -- COS -- COSH -- COT -- DEGREES -- DIV -- EXP -- FLOOR -- LN -- LOG -- LOG10 -- LOG2 -- MOD -- PI -- POWER -- RADIANS -- RANDOM -- ROUND -- SIGN -- SIN -- SINH -- SQRT -- TAN -- TANH -- TRUNC +- `ABS` +- `ACOS` +- `ASIN` +- `ATAN` +- `ATAN2` +- `CEIL` +- `COS` +- `COSH` +- `COT` +- `DEGREES` +- `DIV` +- `EXP` +- `FLOOR` +- `LN` +- `LOG` +- `LOG10` +- `LOG2` +- `MOD` +- `PI` +- `POWER` +- `RADIANS` +- `RANDOM` +- `ROUND` +- `SIGN` +- `SIN` +- `SINH` +- `SQRT` +- `TAN` +- `TANH` +- `TRUNC` ### String Functions -- ASCII -- BIT_LENGTH -- CHARACTER_LENGTH -- CHAR -- COLOGNE_PHONETIC -- CONCAT -- DUMP -- EDIT_DISTANCE -- INITCAP -- INSERT -- INSTR -- LCASE -- LEFT -- LENGTH -- LOCATE -- LOWER -- LPAD -- LTRIM -- MID -- OCTET_LENGTH -- REGEXP_INSTR -- REGEXP_REPLACE -- REGEXP_SUBSTR -- REPEAT -- REPLACE -- REVERSE -- RIGHT -- RPAD -- RTRIM -- SOUNDEX -- SUBSTR -- TRANSLATE -- TRIM -- UCASE -- UNICODE -- UNICODECHR -- UPPER +- `ASCII` +- `BIT_LENGTH` +- `CHARACTER_LENGTH` +- `CHAR` +- `COLOGNE_PHONETIC` +- `CONCAT` +- `DUMP` +- `EDIT_DISTANCE` +- `INITCAP` +- `INSERT` +- `INSTR` +- `LCASE` +- `LEFT` +- `LENGTH` +- `LOCATE` +- `LOWER` +- `LPAD` +- `LTRIM` +- `MID` +- `OCTET_LENGTH` +- `REGEXP_INSTR` +- `REGEXP_REPLACE` +- `REGEXP_SUBSTR` +- `REPEAT` +- `REPLACE` +- `REVERSE` +- `RIGHT` +- `RPAD` +- `RTRIM` +- `SOUNDEX` +- `SUBSTR` +- `TRANSLATE` +- `TRIM` +- `UCASE` +- `UNICODE` +- `UNICODECHR` +- `UPPER` ### Date/Time Functions -- ADD_DAYS -- ADD_HOURS -- ADD_MINUTES -- ADD_MONTHS -- ADD_SECONDS -- ADD_WEEKS -- ADD_YEARS -- CONVERT_TZ -- CURRENT_DATE -- CURRENT_TIMESTAMP -- DAY -- DAYS_BETWEEN -- DBTIMEZONE -- FROM_POSIX_TIME -- HOUR -- HOURS_BETWEEN -- LOCALTIMESTAMP -- MINUTE -- MINUTES_BETWEEN -- MONTH -- MONTHS_BETWEEN -- NOW -- NUMTODSINTERVAL -- NUMTOYMINTERVAL -- POSIX_TIME -- SECOND -- SECONDS_BETWEEN -- SESSIONTIMEZONE -- SYSDATE -- SYSTIMESTAMP -- WEEK -- YEAR -- YEARS_BETWEEN +- `ADD_DAYS` +- `ADD_HOURS` +- `ADD_MINUTES` +- `ADD_MONTHS` +- `ADD_SECONDS` +- `ADD_WEEKS` +- `ADD_YEARS` +- `CONVERT_TZ` +- `CURRENT_DATE` +- `CURRENT_TIMESTAMP` +- `DAY` +- `DAYS_BETWEEN` +- `DBTIMEZONE` +- `FROM_POSIX_TIME` +- `HOUR` +- `HOURS_BETWEEN` +- `LOCALTIMESTAMP` +- `MINUTE` +- `MINUTES_BETWEEN` +- `MONTH` +- `MONTHS_BETWEEN` +- `NOW` +- `NUMTODSINTERVAL` +- `NUMTOYMINTERVAL` +- `POSIX_TIME` +- `SECOND` +- `SECONDS_BETWEEN` +- `SESSIONTIMEZONE` +- `SYSDATE` +- `SYSTIMESTAMP` +- `WEEK` +- `YEAR` +- `YEARS_BETWEEN` ### Geospatial Functions -- ST_AREA -- ST_BOUNDARY -- ST_BUFFER -- ST_CENTROID -- ST_CONTAINS -- ST_CONVEXHULL -- ST_CROSSES -- ST_DIFFERENCE -- ST_DIMENSION -- ST_DISJOINT -- ST_DISTANCE -- ST_ENDPOINT -- ST_ENVELOPE -- ST_EQUALS -- ST_EXTERIORRING -- ST_FORCE2D -- ST_GEOMETRYN -- ST_GEOMETRYTYPE -- ST_INTERIORRINGN -- ST_INTERSECTION -- ST_INTERSECTS -- ST_ISCLOSED -- ST_ISEMPTY -- ST_ISRING -- ST_ISSIMPLE -- ST_LENGTH -- ST_NUMGEOMETRIES -- ST_NUMINTERIORRINGS -- ST_NUMPOINTS -- ST_OVERLAPS -- ST_SETSRID -- ST_POINTN -- ST_STARTPOINT -- ST_SYMDIFFERENCE -- ST_TOUCHES -- ST_TRANSFORM -- ST_UNION -- ST_WITHIN -- ST_X -- ST_Y +Also see the [documentation for geospatial data](https://docs.exasol.com/sql_references/geospatialdata/geospatialdata_overview.htm). + +- `ST_AREA` +- `ST_BOUNDARY` +- `ST_BUFFER` +- `ST_CENTROID` +- `ST_CONTAINS` +- `ST_CONVEXHULL` +- `ST_CROSSES` +- `ST_DIFFERENCE` +- `ST_DIMENSION` +- `ST_DISJOINT` +- `ST_DISTANCE` +- `ST_ENDPOINT` +- `ST_ENVELOPE` +- `ST_EQUALS` +- `ST_EXTERIORRING` +- `ST_FORCE2D` +- `ST_GEOMETRYN` +- `ST_GEOMETRYTYPE` +- `ST_INTERIORRINGN` +- `ST_INTERSECTION` +- `ST_INTERSECTS` +- `ST_ISCLOSED` +- `ST_ISEMPTY` +- `ST_ISRING` +- `ST_ISSIMPLE` +- `ST_LENGTH` +- `ST_NUMGEOMETRIES` +- `ST_NUMINTERIORRINGS` +- `ST_NUMPOINTS` +- `ST_OVERLAPS` +- `ST_SETSRID` +- `ST_POINTN` +- `ST_STARTPOINT` +- `ST_SYMDIFFERENCE` +- `ST_TOUCHES` +- `ST_TRANSFORM` +- `ST_UNION` +- `ST_WITHIN` +- `ST_X` +- `ST_Y` ### Bitwise Function -- BIT_AND -- BIT_CHECK -- BIT_LROTATE -- BIT_LSHIFT -- BIT_NOT -- BIT_OR -- BIT_RROTATE -- BIT_RSHIFT -- BIT_SET -- BIT_TO_NUM -- BIT_XOR +- `BIT_AND` +- `BIT_CHECK` +- `BIT_LROTATE` +- `BIT_LSHIFT` +- `BIT_NOT` +- `BIT_OR` +- `BIT_RROTATE` +- `BIT_RSHIFT` +- `BIT_SET` +- `BIT_TO_NUM` +- `BIT_XOR` ### Conversion Functions -- IS_NUMBER -- IS_DATE -- IS_TIMESTAMP -- IS_BOOLEAN -- IS_DSINTERVAL -- IS_YMINTERVAL -- TO_CHAR -- TO_DATE -- TO_DSINTERVAL -- TO_NUMBER -- TO_TIMESTAMP -- TO_YMINTERVAL +- `IS_NUMBER` +- `IS_DATE` +- `IS_TIMESTAMP` +- `IS_BOOLEAN` +- `IS_DSINTERVAL` +- `IS_YMINTERVAL` +- `TO_CHAR` +- `TO_DATE` +- `TO_DSINTERVAL` +- `TO_NUMBER` +- `TO_TIMESTAMP` +- `TO_YMINTERVAL` ### Other Scalar Functions -- COALESCE -- CURRENT_SCHEMA -- CURRENT_SESSION -- CURRENT_STATEMENT -- CURRENT_USER -- DECODE -- GREATEST -- HASH_MD5 -- HASH_SHA -- HASH_SHA256 -- HASH_SHA512 -- HASH_TIGER -- IPROC -- LEAST -- NULLIF -- NULLIFZERO -- NPROC -- NVL -- NVL2 -- ROWNUM -- ROWID -- SCOPE_USER -- SYS_GUID -- USER -- VALUE2PROC -- ZEROIFNULL +- `COALESCE` +- `CURRENT_SCHEMA` +- `CURRENT_SESSION` +- `CURRENT_STATEMENT` +- `CURRENT_USER` +- `DECODE` +- `GREATEST` +- `HASH_MD5` +- `HASH_SHA` +- `HASH_SHA256` +- `HASH_SHA512` +- `HASH_TIGER` +- `IPROC` +- `LEAST` +- `NULLIF` +- `NULLIFZERO` +- `NPROC` +- `NVL` +- `NVL2` +- `ROWNUM` +- `ROWID` +- `SCOPE_USER` +- `SYS_GUID` +- `USER` +- `VALUE2PROC` +- `ZEROIFNULL` + +## Aggregate and Analytic Functions + +Some analytic functions use special syntax. See the [documentation](https://docs.exasol.com/sql_references/functions/analyticfunctions.htm) for details. Also see the [list of aggregate functions](https://docs.exasol.com/sql_references/functions/aggregatefunctions.htm). + +- `ANY` +- `AVG` +- `CORR` +- `COUNT` +- `COVAR_POP` +- `COVAR_SAMP` +- `EVERY` +- `FIRST_VALUE` +- `GROUP_CONCAT` +- `LAST_VALUE` +- `LISTAGG` +- `MAX` +- `MEDIAN` +- `MIN` +- `MUL` +- REGR_FUNCTIONS + - `REGR_AVGX` + - `REGR_AVGY` + - `REGR_COUNT` + - `REGR_INTERCEPT` + - `REGR_R2` + - `REGR_SLOPE` + - `REGR_SXX` + - `REGR_SXY` + - `REGR_SYY` +- `SOME` +- `STDDEV` +- `STDDEV_POP` +- `STDDEV_SAMP` +- `SUM` +- `VAR_POP` +- `VAR_SAMP` +- `VARIANCE` ## Aggregate Functions -- APPROXIMATE_COUNT_DISTINCT -- AVG -- CORR -- COUNT -- COVAR_POP -- COVAR_SAMP -- FIRST_VALUE -- GROUP_CONCAT -- LAST_VALUE -- MAX -- MEDIAN -- MIN -- REGR_SLOPE -- REGR_INTERCEPT -- REGR_COUNT -- REGR_R2 -- REGR_AVGX -- REGR_AVGY -- REGR_SXX -- REGR_SXY -- REGR_SYY -- STDDEV -- STDDEV_POP -- STDDEV_SAMP -- SUM -- VAR_POP -- VAR_SAMP -- VARIANCE +- `APPROXIMATE_COUNT_DISTINCT` +- `GROUPING` +- `GROUPING_ID` ## Analytic Functions -- ANY -- EVERY -- LISTAGG \ No newline at end of file +- `CUME_DIST` +- `DENSE_RANK` +- `LAG` +- `LEAD` +- `NTH_VALUE` +- `NTILE` +- `PERCENT_RANK` +- `PERCENTILE_CONT` +- `PERCENTILE_DISC` +- `RANK` +- `RATIO_TO_REPORT` +- `ROW_NUMBER` diff --git a/doc/user_guide/statements/select.md b/doc/user_guide/statements/select.md index c29e6a23..6c31628b 100644 --- a/doc/user_guide/statements/select.md +++ b/doc/user_guide/statements/select.md @@ -70,6 +70,8 @@ A udf takes a name of function and any number of [`ValueExpression`](../../../sr selectWithEmits.from().table("people"); ``` +- To add special functions (e.g. analytic functions) to a statement you can use the `function()` method that takes a `Function` as argument. See the [section about creating functions](#creating-functions) for details. + - An `arithmetic expression` is a binary value expression using one of the following arithmetic operators: `+`, `-`, `*`, `/`. Add an arithmetic expression using an `arithmeticExpression( ... )` method. You can also set a name for a derived field that contains an arithmetic expression. @@ -165,3 +167,145 @@ select.from().table("t"); select.orderBy(column("t", "city"), column("t", "price")) .nullsFirst().asc(); ``` + +## Creating Functions + +When you need to use special functions like analytic functions, you can add them to a statement like this: + +```java +Function function = ... // create function +Select select = factory.select().function(function, ""); +``` + +### Analytic functions + +Exasols [analytic functions](https://docs.exasol.com/sql_references/functions/analyticfunctions.htm) support a special syntax. You can specify the keywords `DISTINCT` and `ALL` as well as an `OVER` clause. + +To create a new `AnalyticFunction` use the following code to use it in a `SELECT` statement: + +```java +AnalyticFunction function = AnalyticFunction.of(ExasolAnalyticAggregateFunctions.ANY, + BooleanTerm.lt(column("age"), integerLiteral(30))); +// configure the function +Select select = factory.select().function(function, ""); +``` + +#### Keywords `DISTINCT` and `ALL` + +You create an analytic function with a keyword `DISTINCT` or `ALL` like this: + +```java +AnalyticFunction function = ... +function.keywordDistinct(); +// or +function.keywordAll(); +``` + +Example: + +```java +AnalyticFunction function = AnalyticFunction.of(ExasolAnalyticAggregateFunctions.ANY, + BooleanTerm.lt(column("age"), integerLiteral(30))) + .keywordDistinct(); +// -> ANY(DISTINCT(age < 30)) +``` + +#### Adding an `OVER` clause + +You can create and configure the `OverClause` directly or use a configurator lambda: + +```java +OverClause over = new OverClause().windowName("window1"); +// configure over clause +function.over(over); + +// or use the configurator lambda: +function.over(over -> over.windowName("window1")); +``` + +The `OverClause` offers four methods for configuration: + +* `windowName()` +* `orderBy()` +* `partitionBy()` +* `windowFrame()` + +##### `windowName()` + +Add a named window like this: + +```java +over.windowName("window1"); +// -> OVER(window1) +``` + +##### `orderBy()` + +You can add an `ORDER BY` clause like this: + +```java +over.orderBy(new OrderByClause(select, column("city"), column("price")).asc().nullsFirst()); +// -> OVER(ORDER BY city, price ASC NULLS FIRST) +``` + +##### `partitionBy()` + +You can partition by one or more columns: + +```java +over.partitionBy(column("city"), column("price")); +// -> OVER(PARTITION BY city, price) +``` + +##### `windowFrame()` + +To add a window frame clause use a configurator lambda: + +```java +over.windowFrame(frame -> frame.type(WindowFrameType.ROWS) /* ... */); +``` + +Window frames consist of three parts: + +1. The mandatory window frame unit type (`ROWS`, `RANGE` or `GROUPS`) + + You specify the type like this: + + ```java + over.windowFrame(frame -> frame.type(WindowFrameType.ROWS) /* ... */); + // -> OVER(ROWS ...) + ``` + +2. The mandatory unit specification. This can bei either a single condition or a `BETWEEN ... AND` range: + + * Specify a single condition like this: + + ```java + over.windowFrame(frame -> frame.type(WindowFrameType.ROWS) + .unit(UnitType.CURRENT_ROW)); + // -> ROWS CURRENT ROW + + over.windowFrame(frame -> frame.type(WindowFrameType.ROWS) + .unit(integerLiteral(1), UnitType.PRECEEDING))); + // -> ROWS 1 PRECEEDING + ``` + * Specify a range like this: + + ```java + over.windowFrame(frame -> frame.type(WindowFrameType.ROWS) + .unitBetween(UnitType.UNBOUNDED_PRECEEDING, UnitType.UNBOUNDED_FOLLOWING)); + // -> ROWS BETWEEN UNBOUNDED PRECEEDING AND UNBOUNDED FOLLOWING + + over.windowFrame(frame -> frame.type(WindowFrameType.ROWS) + .unitBetween(column("col1"), UnitType.PRECEEDING, column("col2"), UnitType.FOLLOWING)); + // -> ROWS BETWEEN col1 PRECEEDING AND col2 FOLLOWING + ``` + +3. An optional exclusion: + + ```java + over.windowFrame(frame -> frame.type(WindowFrameType.ROWS) + .unit(UnitType.CURRENT_ROW) + .exclude(WindowFrameExclusionType.NO_OTHERS)); + // -> ROWS CURRENT ROW EXCLUDE NO OTHERS + ``` diff --git a/pom.xml b/pom.xml index 625742b8..ef5ab6d9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.exasol sql-statement-builder - 4.4.3 + 4.5.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. @@ -65,6 +65,11 @@ + + com.exasol + error-reporting-java + 0.4.0 + org.junit.jupiter junit-jupiter diff --git a/src/main/java/com/exasol/sql/dql/select/GroupByClause.java b/src/main/java/com/exasol/sql/dql/select/GroupByClause.java index b1b7b7e5..22952e88 100644 --- a/src/main/java/com/exasol/sql/dql/select/GroupByClause.java +++ b/src/main/java/com/exasol/sql/dql/select/GroupByClause.java @@ -19,7 +19,7 @@ public class GroupByClause extends AbstractFragment implements SelectFragment { /** * Create a new instance of a {@link GroupByClause}. * - * @param rootStatement SQL statement this {@code GROUP BY} clause belongs to + * @param rootStatement SQL statement this {@code GROUP BY} clause belongs to * @param columnReferences column references for the {@code GROUP BY} clause */ public GroupByClause(final SqlStatement rootStatement, final ColumnReference... columnReferences) { @@ -54,9 +54,9 @@ public Select having(final BooleanExpression booleanExpression) { } /** - * Get the boolean expression. + * Get the "having" boolean expression. * - * @return boolean expression + * @return "having" boolean expression */ public BooleanExpression getHavingBooleanExpression() { return this.booleanExpression; diff --git a/src/main/java/com/exasol/sql/dql/select/Select.java b/src/main/java/com/exasol/sql/dql/select/Select.java index 0b074f69..bfb1410c 100644 --- a/src/main/java/com/exasol/sql/dql/select/Select.java +++ b/src/main/java/com/exasol/sql/dql/select/Select.java @@ -74,9 +74,7 @@ public Select function(final FunctionName functionName, final ValueExpression... public Select function(final FunctionName functionName, final String derivedColumnName, final ValueExpression... valueExpressions) { final Function function = ExpressionTerm.function(functionName, valueExpressions); - final DerivedColumn derivedColumn = new DerivedColumn(this, function, derivedColumnName); - this.derivedColumns.add(derivedColumn); - return this; + return this.function(function, derivedColumnName); } /** @@ -99,8 +97,7 @@ public Select function(final Function function, final String derivedColumnName) * @return {@code this} instance for fluent programming */ public Select function(final Function function) { - function(function, ""); - return this; + return function(function, ""); } /** @@ -111,7 +108,6 @@ public Select function(final Function function) { * @param valueExpressions zero or more value expressions * @return {@code this} instance for fluent programming */ - public Select udf(final String functionName, final ColumnsDefinition emitsColumnsDefinition, final ValueExpression... valueExpressions) { final Function udf = ExpressionTerm.udf(functionName, emitsColumnsDefinition, valueExpressions); diff --git a/src/main/java/com/exasol/sql/dql/select/rendering/SelectRenderer.java b/src/main/java/com/exasol/sql/dql/select/rendering/SelectRenderer.java index 474e7353..3c4e24eb 100644 --- a/src/main/java/com/exasol/sql/dql/select/rendering/SelectRenderer.java +++ b/src/main/java/com/exasol/sql/dql/select/rendering/SelectRenderer.java @@ -2,10 +2,7 @@ import java.util.List; -import com.exasol.sql.DerivedColumn; -import com.exasol.sql.Table; -import com.exasol.sql.ValueTable; -import com.exasol.sql.ValueTableRow; +import com.exasol.sql.*; import com.exasol.sql.dql.select.*; import com.exasol.sql.expression.BooleanExpression; import com.exasol.sql.rendering.AbstractFragmentRenderer; @@ -152,9 +149,9 @@ public void leave(final ValueTable valueTable) { appendAutoQuoted(valueTable.getTableNameAlias()); startParenthesis(); final List columnNameAliases = valueTable.getColumnNameAliases(); - for (int i = 0; i < columnNameAliases.size(); i++) { - appendAutoQuoted(columnNameAliases.get(i)); - if (i < columnNameAliases.size() - 1) { + for (int index = 0; index < columnNameAliases.size(); index++) { + appendAutoQuoted(columnNameAliases.get(index)); + if (index < (columnNameAliases.size() - 1)) { append(", "); } } diff --git a/src/main/java/com/exasol/sql/expression/function/FunctionVisitor.java b/src/main/java/com/exasol/sql/expression/function/FunctionVisitor.java index 77fb0dcf..1db9642b 100644 --- a/src/main/java/com/exasol/sql/expression/function/FunctionVisitor.java +++ b/src/main/java/com/exasol/sql/expression/function/FunctionVisitor.java @@ -1,8 +1,6 @@ package com.exasol.sql.expression.function; -import com.exasol.sql.expression.function.exasol.CastExasolFunction; -import com.exasol.sql.expression.function.exasol.ExasolFunction; -import com.exasol.sql.expression.function.exasol.ExasolUdf; +import com.exasol.sql.expression.function.exasol.*; /** * Visitor interface for {@link Function}. @@ -13,4 +11,6 @@ public interface FunctionVisitor { public void visit(ExasolUdf function); public void visit(CastExasolFunction castFunction); + + public void visit(AnalyticFunction analyticFunction); } diff --git a/src/main/java/com/exasol/sql/expression/function/exasol/AnalyticFunction.java b/src/main/java/com/exasol/sql/expression/function/exasol/AnalyticFunction.java new file mode 100644 index 00000000..af623315 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/function/exasol/AnalyticFunction.java @@ -0,0 +1,112 @@ +package com.exasol.sql.expression.function.exasol; + +import java.util.Arrays; +import java.util.List; +import java.util.function.UnaryOperator; + +import com.exasol.sql.expression.ValueExpression; +import com.exasol.sql.expression.function.AbstractFunction; +import com.exasol.sql.expression.function.FunctionVisitor; + +/** + * This class represents an analytic function in the Exasol database that supports keywords {@code DISTINCT} and + * {@code ALL} and the over clause. + */ +public class AnalyticFunction extends AbstractFunction { + + public enum Keyword { + DISTINCT, ALL + } + + private Keyword keyword; + private OverClause overClause; + + private AnalyticFunction(final ExasolAnalyticAggregateFunctions functionName, + final List valueExpressions) { + super(functionName.toString(), valueExpressions); + } + + /** + * Create a new {@link AnalyticFunction} instance. + * + * @param functionName name of the function + * @param valueExpressions zero or more value expressions + * @return new {@link AnalyticFunction} + */ + public static AnalyticFunction of(final ExasolAnalyticAggregateFunctions functionName, + final ValueExpression... valueExpressions) { + return new AnalyticFunction(functionName, Arrays.asList(valueExpressions)); + } + + /** + * Add keyword {@code DISTINCT} to the function call + * + * @return this {@link AnalyticFunction} for fluent programming + */ + public AnalyticFunction keywordDistinct() { + return this.keyword(Keyword.DISTINCT); + } + + /** + * Add keyword {@code ALL} to the function call + * + * @return this {@link AnalyticFunction} for fluent programming + */ + public AnalyticFunction keywordAll() { + return this.keyword(Keyword.ALL); + } + + private AnalyticFunction keyword(final Keyword keyword) { + this.keyword = keyword; + return this; + } + + /** + * Get the keyword for the function call, may be {@code null}. + * + * @return keyword for the function call + */ + public Keyword getKeyword() { + return this.keyword; + } + + /** + * Add the given over clause to the function call. + * + * @param overClause over clause to add + * @return this {@link AnalyticFunction} for fluent programming + */ + public AnalyticFunction over(final OverClause overClause) { + this.overClause = overClause; + return this; + } + + /** + * Add an {@code OVER} clause to the function call. You configure the clause in the given lambda. + * + * @param configurator lambda configuring the {@link OverClause}. + * @return this {@link AnalyticFunction} for fluent programming + */ + public AnalyticFunction over(final UnaryOperator configurator) { + return this.over(configurator.apply(new OverClause())); + } + + /** + * Get the "over clause" appended to the function call, may be {@code null}. + * + * @return "over clause" appended to the function call + */ + public OverClause getOverClause() { + return this.overClause; + } + + @Override + public boolean hasParenthesis() { + return true; + } + + @Override + public void accept(final FunctionVisitor visitor) { + visitor.visit(this); + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAggregateFunction.java b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAggregateFunction.java index 2e30739c..c8fd6ad9 100644 --- a/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAggregateFunction.java +++ b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAggregateFunction.java @@ -5,11 +5,9 @@ /** * This class is a list of Aggregate Functions that the Exasol database supports. * - *

- * Currently unsupported functions: GROUPING, PERCENTILE_CONT, PERCENTILE_DISC, OVER clause for all aggregate functions - * and keywords. See github issue # 72. - *

+ * @deprecated Use enum {@link ExasolAnalyticAggregateFunctions}. */ +@Deprecated(since = "4.5.0", forRemoval = true) public enum ExasolAggregateFunction implements FunctionName { APPROXIMATE_COUNT_DISTINCT, AVG, CORR, COUNT, COVAR_POP, COVAR_SAMP, FIRST_VALUE, GROUP_CONCAT, LAST_VALUE, MAX, MEDIAN, MIN, REGR_SLOPE, REGR_INTERCEPT, REGR_COUNT, REGR_R2, REGR_AVGX, REGR_AVGY, REGR_SXX, REGR_SXY, REGR_SYY, diff --git a/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticAggregateFunctions.java b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticAggregateFunctions.java new file mode 100644 index 00000000..545664d8 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticAggregateFunctions.java @@ -0,0 +1,265 @@ +package com.exasol.sql.expression.function.exasol; + +import com.exasol.sql.expression.function.FunctionName; + +/** + * This enum is a list of Analytic + * and Aggregate Functions that + * the Exasol database supports. + */ +public enum ExasolAnalyticAggregateFunctions implements FunctionName { + /* + * Analytic & aggregate functions + */ + /** + * See + * documentation. + */ + ANY, + /** + * See + * documentation. + */ + AVG, + /** + * See + * documentation. + */ + CORR, + /** + * See + * documentation. + */ + COUNT, + /** + * See documentation. + */ + COVAR_POP, + /** + * See documentation. + */ + COVAR_SAMP, + /** + * See + * documentation. + */ + EVERY, + /** + * See documentation. + */ + FIRST_VALUE, + /** + * See documentation. + */ + GROUP_CONCAT, + /** + * See documentation. + */ + LAST_VALUE, + /** + * See documentation. + */ + LISTAGG, + /** + * See + * documentation. + */ + MAX, + /** + * See documentation. + */ + MEDIAN, + /** + * See + * documentation. + */ + MIN, + /** + * See + * documentation. + */ + MUL, + + /* + * REGR_FUNCTIONS + */ + + /** + * See documentation. + */ + REGR_AVGX, + /** + * See documentation. + */ + REGR_AVGY, + /** + * See documentation. + */ + REGR_COUNT, + /** + * See documentation. + */ + REGR_INTERCEPT, + /** + * See documentation. + */ + REGR_R2, + /** + * See documentation. + */ + REGR_SLOPE, + /** + * See documentation. + */ + REGR_SXX, + /** + * See documentation. + */ + REGR_SXY, + /** + * See documentation. + */ + REGR_SYY, + + /** + * This is an alias for {@link #ANY}. See + * documentation. + */ + SOME, + /** + * See documentation. + */ + STDDEV, + /** + * See documentation. + */ + STDDEV_POP, + /** + * See documentation. + */ + STDDEV_SAMP, + /** + * See + * documentation. + */ + SUM, + /** + * See documentation. + */ + VAR_POP, + /** + * See documentation. + */ + VAR_SAMP, + /** + * See documentation. + */ + VARIANCE, + + /* + * Analytic functions + */ + + /** + * See documentation. + */ + CUME_DIST, + /** + * See documentation. + */ + DENSE_RANK, + /** + * See + * documentation. + */ + LAG, + /** + * See + * documentation. + */ + LEAD, + /** + * See documentation. + */ + NTH_VALUE, + /** + * See + * documentation. + */ + NTILE, + /** + * See documentation. + */ + PERCENT_RANK, + /** + * See documentation. + */ + PERCENTILE_CONT, + /** + * See documentation. + */ + PERCENTILE_DISC, + /** + * See + * documentation. + */ + RANK, + /** + * See documentation. + */ + RATIO_TO_REPORT, + /** + * See documentation. + */ + ROW_NUMBER, + + /* + * Aggregate functions + */ + + /** + * See documentation. + */ + APPROXIMATE_COUNT_DISTINCT, + /** + * See documentation. + */ + GROUPING, + /** + * See documentation. + */ + GROUPING_ID +} diff --git a/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticFunction.java b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticFunction.java index 07342bba..3a634370 100644 --- a/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticFunction.java +++ b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolAnalyticFunction.java @@ -4,16 +4,10 @@ /** * This class is a list of Analytic Functions that the Exasol database supports. - *

- * This class covers functions that are not in the {@link ExasolAggregateFunction} list. - *

- *

- * Currently unsupported functions: CUME_DIST, DENSE_RANK, LAG, LEAD, NTH_VALUE, NTILE, NAMED WINDOW CLAUSE, - * PERCENT_RANK, PERCENTILE_CONT, PERCENTILE_DISC, RANK, RATIO_TO_REPORT, ROW_NUMBER, OVER clause for all analytic - * functions, functions' prefixes that goes after parenthesis. See - * github issue # 72. - *

+ * + * @deprecated Use enum {@link ExasolAnalyticAggregateFunctions}. */ +@Deprecated(since = "4.5", forRemoval = true) public enum ExasolAnalyticFunction implements FunctionName { ANY, EVERY, LISTAGG } diff --git a/src/main/java/com/exasol/sql/expression/function/exasol/ExasolFunction.java b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolFunction.java index c0a0211a..4dbb1f70 100644 --- a/src/main/java/com/exasol/sql/expression/function/exasol/ExasolFunction.java +++ b/src/main/java/com/exasol/sql/expression/function/exasol/ExasolFunction.java @@ -1,28 +1,24 @@ package com.exasol.sql.expression.function.exasol; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import com.exasol.sql.expression.ValueExpression; -import com.exasol.sql.expression.function.AbstractFunction; -import com.exasol.sql.expression.function.FunctionName; -import com.exasol.sql.expression.function.FunctionVisitor; +import com.exasol.sql.expression.function.*; /** * This class represents a function in the Exasol database. */ public class ExasolFunction extends AbstractFunction { - private static final List functionsWithoutParenthesis = Arrays.asList("SYSDATE", "CURRENT_SCHEMA", + private static final List FUNCTIONS_WITHOUT_PARENTHESIS = List.of("SYSDATE", "CURRENT_SCHEMA", "CURRENT_SESSION", "CURRENT_STATEMENT", "CURRENT_USER", "ROWNUM", "ROWID", "SCOPE_USER", "USER"); - protected ExasolFunction(final FunctionName functionName, final List valueExpressions) { + private ExasolFunction(final FunctionName functionName, final List valueExpressions) { super(functionName.toString(), valueExpressions); } /** * Create a new {@link ExasolFunction} instance. - * + * * @param functionName name of the function * @return new {@link ExasolFunction} */ @@ -43,7 +39,7 @@ public static ExasolFunction of(final FunctionName functionName, final ValueExpr @Override public boolean hasParenthesis() { - return !functionsWithoutParenthesis.contains(this.functionName); + return !FUNCTIONS_WITHOUT_PARENTHESIS.contains(this.functionName); } @Override diff --git a/src/main/java/com/exasol/sql/expression/function/exasol/OverClause.java b/src/main/java/com/exasol/sql/expression/function/exasol/OverClause.java new file mode 100644 index 00000000..2bfd3c5b --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/function/exasol/OverClause.java @@ -0,0 +1,112 @@ +package com.exasol.sql.expression.function.exasol; + +import static java.util.Arrays.asList; + +import java.util.List; +import java.util.function.UnaryOperator; + +import com.exasol.sql.dql.select.OrderByClause; +import com.exasol.sql.expression.ValueExpression; + +/** + * This represents an {@code OVER} clause of an analytic function in Exasol. See the + * documentation for details. + */ +public class OverClause { + + private String windowName; + private OrderByClause orderByClause; + private WindowFrameClause windowFrameClause; + private List partitionByColumns; + + /** + * Create a new {@link OverClause} with the given window name. + * + * @param windowName window name of the new {@link OverClause} + * @return a new {@link OverClause} + */ + public static OverClause of(final String windowName) { + return new OverClause().windowName(windowName); + } + + /** + * Set the window name for this {@link OverClause}. + * + * @param windowName name of the window. + * @return this {@link OverClause} for fluent programming + */ + public OverClause windowName(final String windowName) { + this.windowName = windowName; + return this; + } + + /** + * Set the {@link OrderByClause} for this {@link OverClause}. + * + * @param orderByClause {@link OrderByClause} for this {@link OverClause} + * @return this {@link OverClause} for fluent programming + */ + public OverClause orderBy(final OrderByClause orderByClause) { + this.orderByClause = orderByClause; + return this; + } + + /** + * Set the columns for the {@code PARTITION BY} clause of this {@link OverClause}. + * + * @param columns columns for the {@code PARTITION BY} clause. + * @return this {@link OverClause} for fluent programming + */ + public OverClause partitionBy(final ValueExpression... columns) { + this.partitionByColumns = asList(columns); + return this; + } + + /** + * Set and configure the {@link WindowFrameClause} for this {@link OverClause}. You configure the clause in the + * given lambda. + * + * @param configurator lambda configuring the {@link WindowFrameClause}. + * @return this {@link OverClause} for fluent programming + */ + public OverClause windowFrame(final UnaryOperator configurator) { + this.windowFrameClause = configurator.apply(new WindowFrameClause()); + return this; + } + + /** + * Get the window name of this {@link OverClause}. + * + * @return window name + */ + public String getWindowName() { + return this.windowName; + } + + /** + * Get the columns of the partition by clause of this {@link OverClause}. + * + * @return partition by columns + */ + public List getPartitionByColumns() { + return this.partitionByColumns; + } + + /** + * Get the order by clause of this {@link OverClause}. + * + * @return the order by clause + */ + public OrderByClause getOrderByClause() { + return this.orderByClause; + } + + /** + * Get the window frame clause of this {@link OverClause}. + * + * @return the window frame clause + */ + public WindowFrameClause getWindowFrameClause() { + return this.windowFrameClause; + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/function/exasol/WindowFrameClause.java b/src/main/java/com/exasol/sql/expression/function/exasol/WindowFrameClause.java new file mode 100644 index 00000000..a6d73f18 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/function/exasol/WindowFrameClause.java @@ -0,0 +1,214 @@ +package com.exasol.sql.expression.function.exasol; + +import com.exasol.sql.expression.ValueExpression; + +/** + * This represents a window frame clause of an analytic function in Exasol. See the + * documentation for details. + */ +public class WindowFrameClause { + + /** + * The type of a window frame clause + */ + public enum WindowFrameType { + ROWS, RANGE, GROUPS + } + + private WindowFrameClause.WindowFrameType type; + private WindowFrameUnitClause unit1; + private WindowFrameUnitClause unit2; + private WindowFrameExclusionType exclusion; + + WindowFrameClause() { + // package private constructor + } + + /** + * Set the type of this {@link WindowFrameClause}. + * + * @param type type of this {@link WindowFrameClause} + * @return this {@link WindowFrameClause} for fluent programming + */ + public WindowFrameClause type(final WindowFrameType type) { + this.type = type; + return this; + } + + /** + * Set the unit type of this {@link WindowFrameClause}. + * + * @param unitType unit type of this {@link WindowFrameClause} + * @return this {@link WindowFrameClause} for fluent programming + */ + public WindowFrameClause unit(final UnitType unitType) { + return unit(null, unitType); + } + + /** + * Set the unit type of this {@link WindowFrameClause}. + * + * @param expression expression for the unit. Only required for unit types {@link UnitType#PRECEEDING} and + * {@link UnitType#FOLLOWING} + * @param unitType unit type of this {@link WindowFrameClause} + * @return this {@link WindowFrameClause} for fluent programming + */ + public WindowFrameClause unit(final ValueExpression expression, final UnitType unitType) { + this.unit1 = new WindowFrameUnitClause(unitType, expression); + return this; + } + + /** + * Set the unit type of this {@link WindowFrameClause} to {@code BETWEEN ... AND ...}. + * + * @param unitType1 {@code BETWEEN} unit + * @param unitType2 {@code AND} unit + * @return this {@link WindowFrameClause} for fluent programming + */ + public WindowFrameClause unitBetween(final UnitType unitType1, final UnitType unitType2) { + return this.unitBetween(null, unitType1, null, unitType2); + } + + /** + * Set the unit type of this {@link WindowFrameClause} to {@code BETWEEN ... AND ...}. + * + * @param unitType1 {@code BETWEEN} unit + * @param expression1 {@code BETWEEN} expression. Only required for unit types {@link UnitType#PRECEEDING} and + * {@link UnitType#FOLLOWING} + * @param unitType2 {@code AND} unit + * @param expression2 {@code AND} expression. Only required for unit types {@link UnitType#PRECEEDING} and + * {@link UnitType#FOLLOWING} + * @return this {@link WindowFrameClause} for fluent programming + */ + public WindowFrameClause unitBetween(final ValueExpression expression1, final UnitType unitType1, + final ValueExpression expression2, final UnitType unitType2) { + this.unit1 = new WindowFrameUnitClause(unitType1, expression1); + this.unit2 = new WindowFrameUnitClause(unitType2, expression2); + return this; + } + + /** + * Set the exclusion type of this {@link WindowFrameClause}. + * + * @param exclusion exclusion type. + * @return this {@link WindowFrameClause} for fluent programming + */ + public WindowFrameClause exclude(final WindowFrameExclusionType exclusion) { + this.exclusion = exclusion; + return this; + } + + /** + * Get the window frame type. + * + * @return window frame type. + */ + public WindowFrameClause.WindowFrameType getType() { + return this.type; + } + + /** + * Get the {@code BETWEEN} unit. + * + * @return {@code BETWEEN} unit + */ + public WindowFrameUnitClause getUnit1() { + return this.unit1; + } + + /** + * Get the {@code AND} unit. + * + * @return {@code AND} unit + */ + public WindowFrameUnitClause getUnit2() { + return this.unit2; + } + + /** + * Get the exclusion type. + * + * @return exclusion type + */ + public WindowFrameExclusionType getExclusion() { + return this.exclusion; + } + + /** + * Represents a unit type. + */ + public enum UnitType { + + UNBOUNDED_PRECEEDING("UNBOUNDED PRECEEDING"), UNBOUNDED_FOLLOWING("UNBOUNDED FOLLOWING"), + PRECEEDING("PRECEEDING"), FOLLOWING("FOLLOWING"), CURRENT_ROW("CURRENT ROW"); + + private final String sqlKeyword; + + private UnitType(final String sqlKeyword) { + this.sqlKeyword = sqlKeyword; + } + + /** + * Get the keyword for rendering to SQL. + * + * @return keyword for rendering to SQL. + */ + public String getSqlKeyword() { + return this.sqlKeyword; + } + } + + /** + * Represents a window frame unit. + */ + public static class WindowFrameUnitClause { + + private final UnitType type; + private final ValueExpression expression; + + private WindowFrameUnitClause(final UnitType type, final ValueExpression expression) { + this.type = type; + this.expression = expression; + } + + /** + * Get the type. + * + * @return type + */ + public UnitType getType() { + return this.type; + } + + /** + * Get the expression. + * + * @return expression + */ + public ValueExpression getExpression() { + return this.expression; + } + } + + /** + * Represents the type of a window frame exclusion. + */ + public enum WindowFrameExclusionType { + CURRENT_ROW("CURRENT ROW"), TIES("TIES"), GROUP("GROUP"), NO_OTHERS("NO OTHERS"); + + private final String sqlKeyword; + + private WindowFrameExclusionType(final String sqlKeyword) { + this.sqlKeyword = sqlKeyword; + } + + /** + * Get the keyword for rendering to SQL. + * + * @return keyword for rendering to SQL. + */ + public String getSqlKeyword() { + return this.sqlKeyword; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/exasol/sql/expression/rendering/OverClauseRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/OverClauseRenderer.java new file mode 100644 index 00000000..90248e47 --- /dev/null +++ b/src/main/java/com/exasol/sql/expression/rendering/OverClauseRenderer.java @@ -0,0 +1,102 @@ +package com.exasol.sql.expression.rendering; + +import static com.exasol.errorreporting.ExaError.messageBuilder; + +import java.util.List; +import java.util.function.Consumer; + +import com.exasol.sql.dql.select.OrderByClause; +import com.exasol.sql.dql.select.rendering.SelectRenderer; +import com.exasol.sql.expression.ValueExpression; +import com.exasol.sql.expression.function.exasol.OverClause; +import com.exasol.sql.expression.function.exasol.WindowFrameClause; +import com.exasol.sql.expression.function.exasol.WindowFrameClause.*; +import com.exasol.sql.rendering.StringRendererConfig; + +/** + * A renderer for {@link OverClause}. + */ +class OverClauseRenderer extends AbstractExpressionRenderer { + + OverClauseRenderer(final StringRendererConfig config) { + super(config); + } + + void visit(final OverClause overClause) { + append(" "); + appendKeyword("OVER"); + append("("); + if (overClause.getWindowName() != null) { + append(overClause.getWindowName()); + } + appendPartition(overClause.getPartitionByColumns()); + if (overClause.getOrderByClause() != null) { + appendOrderBy(overClause.getOrderByClause()); + } + if (overClause.getWindowFrameClause() != null) { + appendWindowFrame(overClause.getWindowFrameClause()); + } + append(")"); + } + + private void appendPartition(final List columns) { + if ((columns == null) || columns.isEmpty()) { + return; + } + appendKeyword(" PARTITION BY "); + render(renderer -> renderer.visit(columns)); + } + + private void render(final Consumer action) { + final ValueExpressionRenderer valueExpressionRenderer = new ValueExpressionRenderer(this.config); + action.accept(valueExpressionRenderer); + append(valueExpressionRenderer.render()); + } + + private void appendOrderBy(final OrderByClause orderByClause) { + final SelectRenderer selectRenderer = new SelectRenderer(this.config); + orderByClause.accept(selectRenderer); + append(selectRenderer.render()); + } + + private void appendWindowFrame(final WindowFrameClause windowFrameClause) { + append(" "); + final WindowFrameType type = windowFrameClause.getType(); + if (type == null) { + throw new IllegalStateException(messageBuilder("E-ESB-3") // + .message("Type not defined.") // + .mitigation("Set type the window frame.").toString()); + } + appendKeyword(type.name()); + if (windowFrameClause.getUnit1() == null) { + throw new IllegalStateException(messageBuilder("E-ESB-1") + .message("First unit not defined. At lease one unit is required for a window frame").toString()); + } + append(" "); + if (windowFrameClause.getUnit2() == null) { + renderUnit(windowFrameClause.getUnit1()); + } else { + appendKeyword("BETWEEN "); + renderUnit(windowFrameClause.getUnit1()); + appendKeyword(" AND "); + renderUnit(windowFrameClause.getUnit2()); + } + if (windowFrameClause.getExclusion() != null) { + appendKeyword(" EXCLUDE "); + appendKeyword(windowFrameClause.getExclusion().getSqlKeyword()); + } + } + + private void renderUnit(final WindowFrameUnitClause unit) { + if ((unit.getType() == UnitType.PRECEEDING) || (unit.getType() == UnitType.FOLLOWING)) { + if (unit.getExpression() == null) { + throw new IllegalStateException(messageBuilder("E-ESB-2") + .message("Expression is required for window frame units PRECEEDING and FOLLOWING.") + .mitigation("Add expression for unit types PRECEEDING and FOLLOWING.").toString()); + } + render(renderer -> renderer.visit(unit.getExpression())); + append(" "); + } + appendKeyword(unit.getType().getSqlKeyword()); + } +} diff --git a/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java index 815ddcea..68be99a5 100644 --- a/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java +++ b/src/main/java/com/exasol/sql/expression/rendering/ValueExpressionRenderer.java @@ -9,16 +9,10 @@ import com.exasol.sql.dql.select.Select; import com.exasol.sql.dql.select.rendering.SelectRenderer; import com.exasol.sql.expression.*; -import com.exasol.sql.expression.comparison.Comparison; -import com.exasol.sql.expression.comparison.ComparisonVisitor; -import com.exasol.sql.expression.comparison.LikeComparison; -import com.exasol.sql.expression.comparison.SimpleComparison; -import com.exasol.sql.expression.function.AbstractFunction; -import com.exasol.sql.expression.function.Function; -import com.exasol.sql.expression.function.FunctionVisitor; -import com.exasol.sql.expression.function.exasol.CastExasolFunction; -import com.exasol.sql.expression.function.exasol.ExasolFunction; -import com.exasol.sql.expression.function.exasol.ExasolUdf; +import com.exasol.sql.expression.comparison.*; +import com.exasol.sql.expression.function.*; +import com.exasol.sql.expression.function.exasol.*; +import com.exasol.sql.expression.function.exasol.AnalyticFunction.Keyword; import com.exasol.sql.expression.literal.*; import com.exasol.sql.expression.predicate.*; import com.exasol.sql.rendering.ColumnsDefinitionRenderer; @@ -173,7 +167,7 @@ public void visit(final BetweenPredicate betweenPredicate) { } private void appendSelect(final Select select) { - final SelectRenderer selectRenderer = SelectRenderer.create(config); + final SelectRenderer selectRenderer = SelectRenderer.create(this.config); select.accept(selectRenderer); append(selectRenderer.render()); } @@ -267,15 +261,18 @@ public void visit(final NullLiteral nullLiteral) { @Override public void visit(final ExasolFunction function) { - renderFunction(function); + renderFunction(function, null); } - private void renderFunction(final AbstractFunction function) { + private void renderFunction(final AbstractFunction function, final Keyword keyword) { appendKeyword(function.getFunctionName()); if (function.hasParenthesis()) { startParenthesis(); } ++this.nestedLevel; + if (keyword != null) { + this.appendKeyword(keyword.name()); + } this.visit(function.getParameters().toArray(ValueExpression[]::new)); --this.nestedLevel; if (function.hasParenthesis()) { @@ -285,10 +282,24 @@ private void renderFunction(final AbstractFunction function) { @Override public void visit(final ExasolUdf function) { - renderFunction(function); + renderFunction(function, null); appendEmitsWhenNecessary(function); } + @Override + public void visit(final AnalyticFunction analyticFunction) { + renderFunction(analyticFunction, analyticFunction.getKeyword()); + if (analyticFunction.getOverClause() != null) { + renderOverClause(analyticFunction.getOverClause()); + } + } + + private void renderOverClause(final OverClause overClause) { + final OverClauseRenderer overClauseRenderer = new OverClauseRenderer(this.config); + overClauseRenderer.visit(overClause); + this.append(overClauseRenderer.render()); + } + @Override public void visit(final CastExasolFunction castFunction) { appendKeyword("CAST"); diff --git a/src/test/java/com/exasol/hamcrest/ExceptionAssertions.java b/src/test/java/com/exasol/hamcrest/ExceptionAssertions.java new file mode 100644 index 00000000..338b9c1d --- /dev/null +++ b/src/test/java/com/exasol/hamcrest/ExceptionAssertions.java @@ -0,0 +1,42 @@ +package com.exasol.hamcrest; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.hamcrest.Matcher; +import org.junit.jupiter.api.function.Executable; + +/** + * Helper assertion methods for verifying exceptions. + */ +public class ExceptionAssertions { + private ExceptionAssertions() { + // not instantiable + } + + /** + * Verify that the given executable throws an exception with the expected message. + * + * @param expectedExceptionType expected type of the thrown exception + * @param executable executable that is expected to throw an exception + * @param messageMatcher hamcrest matcher for the exception message + */ + public static void assertThrowsWithMessage(final Class expectedExceptionType, + final Executable executable, final Matcher messageMatcher) { + final Throwable exception = assertThrows(expectedExceptionType, executable); + assertThat("wrong exception message", exception.getMessage(), messageMatcher); + } + + /** + * Verify that the given executable throws an exception with the expected message. + * + * @param expectedExceptionType expected type of the thrown exception + * @param executable executable that is expected to throw an exception + * @param expectedMessage exception message + */ + public static void assertThrowsWithMessage(final Class expectedExceptionType, + final Executable executable, final String expectedMessage) { + assertThrowsWithMessage(expectedExceptionType, executable, equalTo(expectedMessage)); + } +} diff --git a/src/test/java/com/exasol/sql/dql/select/rendering/TestSelectRendering.java b/src/test/java/com/exasol/sql/dql/select/rendering/TestSelectRendering.java index 0b482be9..fe9d6ad5 100644 --- a/src/test/java/com/exasol/sql/dql/select/rendering/TestSelectRendering.java +++ b/src/test/java/com/exasol/sql/dql/select/rendering/TestSelectRendering.java @@ -7,16 +7,24 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.*; import com.exasol.datatype.type.Varchar; import com.exasol.sql.StatementFactory; import com.exasol.sql.ValueTable; +import com.exasol.sql.dql.select.OrderByClause; import com.exasol.sql.dql.select.Select; import com.exasol.sql.expression.*; import com.exasol.sql.expression.function.exasol.*; +import com.exasol.sql.expression.function.exasol.AnalyticFunction.Keyword; +import com.exasol.sql.expression.function.exasol.WindowFrameClause.*; import com.exasol.sql.expression.literal.NullLiteral; import com.exasol.sql.rendering.StringRendererConfig; @@ -156,18 +164,38 @@ void testSelectCastFunction() { @Test void testSelectAggregateFunctionCoalesce() { final Select select = StatementFactory.getInstance().select() // - .function(ExasolAggregateFunction.APPROXIMATE_COUNT_DISTINCT, "COUNT_APPR", column("customer_id")); + .function(ExasolAnalyticAggregateFunctions.APPROXIMATE_COUNT_DISTINCT, "COUNT_APPR", + column("customer_id")); select.from().table("orders"); select.where(BooleanTerm.gt(column("price"), integerLiteral(1000))); assertThat(select, rendersTo("SELECT APPROXIMATE_COUNT_DISTINCT(customer_id) COUNT_APPR FROM orders WHERE price > 1000")); } + @Test + void testSelectAggregateFunctionCountStar() { + final Select select = StatementFactory.getInstance().select() // + .function(ExasolAnalyticAggregateFunctions.COUNT, "COUNT", column("*")); + select.from().table("orders"); + assertThat(select, rendersTo("SELECT COUNT(*) COUNT FROM orders")); + } + + @Test + void testSelectAnalyticFunctionWithoutArgument() { + final Select select = StatementFactory.getInstance().select() // + .field("department") // + .function(ExasolAnalyticAggregateFunctions.ANY, " ANY_ "); + select.from().table("employee_table"); + select.groupBy(column("department")); + assertThat(select, rendersTo("SELECT department, ANY() ANY_ FROM employee_table GROUP BY department")); + } + @Test void testSelectAnalyticFunction() { final Select select = StatementFactory.getInstance().select() // .field("department") // - .function(ExasolAnalyticFunction.ANY, " ANY_ ", BooleanTerm.lt(column("age"), integerLiteral(30))); + .function(ExasolAnalyticAggregateFunctions.ANY, " ANY_ ", + BooleanTerm.lt(column("age"), integerLiteral(30))); select.from().table("employee_table"); select.groupBy(column("department")); assertThat(select, @@ -175,7 +203,117 @@ void testSelectAnalyticFunction() { } @Test - void testSelectTwoScalatFunctions() { + void testSelectAnalyticFunctionWithMultipleArgs() { + final Select select = StatementFactory.getInstance().select() // + .field("department") // + .function(ExasolAnalyticAggregateFunctions.ANY, " ANY_ ", // + BooleanTerm.lt(column("age"), integerLiteral(30)), + BooleanTerm.gt(column("age"), integerLiteral(40))); + select.from().table("employee_table"); + select.groupBy(column("department")); + assertThat(select, rendersTo( + "SELECT department, ANY((age < 30), (age > 40)) ANY_ FROM employee_table GROUP BY department")); + } + + @ParameterizedTest + @CsvSource(nullValues = "NULL", value = { // + "NULL, ''", // + "DISTINCT, DISTINCT", // + "ALL, ALL" // + }) + void testSelectAnalyticFunctionWithKeyword( + final com.exasol.sql.expression.function.exasol.AnalyticFunction.Keyword keyword, + final String expectedKeyword) { + final AnalyticFunction function = AnalyticFunction.of(ExasolAnalyticAggregateFunctions.ANY, + BooleanTerm.lt(column("age"), integerLiteral(30))); + if (keyword == Keyword.DISTINCT) { + function.keywordDistinct(); + } + if (keyword == Keyword.ALL) { + function.keywordAll(); + } + final Select select = StatementFactory.getInstance().select() // + .field("department") // + .function(function, "ANY_"); + select.from().table("employee_table"); + select.groupBy(column("department")); + assertThat(select, rendersTo("SELECT department, ANY(" + expectedKeyword + + "(age < 30)) ANY_ FROM employee_table GROUP BY department")); + } + + static Stream overClauseArguments() { + return Stream.of(arguments(null, ""), // + arguments(OverClause.of("window1"), " OVER(window1)"), + arguments(OverClause.of("window1").orderBy(new OrderByClause(null, column("dep"))), + " OVER(window1 ORDER BY dep)"), + arguments(OverClause.of(null).orderBy(new OrderByClause(null, column("dep"))), " OVER( ORDER BY dep)"), + arguments(OverClause.of("window1").orderBy(new OrderByClause(null, column("dep")).asc().nullsFirst()), + " OVER(window1 ORDER BY dep ASC NULLS FIRST)"), + arguments(OverClause.of("window1").partitionBy(), " OVER(window1)"), + arguments(OverClause.of("window1").partitionBy(column("col")), " OVER(window1 PARTITION BY col)"), + arguments(OverClause.of("window1").partitionBy(column("col1"), column("col2")), + " OVER(window1 PARTITION BY col1, col2)"), + arguments( + OverClause.of("window1") + .windowFrame(frame -> frame.type(WindowFrameType.ROWS).unit(UnitType.CURRENT_ROW)), + " OVER(window1 ROWS CURRENT ROW)"), + arguments(OverClause.of("window1").windowFrame( + frame -> frame.type(WindowFrameType.ROWS).unit(integerLiteral(42), UnitType.PRECEEDING)), + " OVER(window1 ROWS 42 PRECEEDING)"), + arguments( + OverClause.of("window1") + .windowFrame(frame -> frame.type(WindowFrameType.ROWS) + .unitBetween(UnitType.UNBOUNDED_PRECEEDING, UnitType.UNBOUNDED_FOLLOWING)), + " OVER(window1 ROWS BETWEEN UNBOUNDED PRECEEDING AND UNBOUNDED FOLLOWING)"), + arguments( + OverClause.of("window1") + .windowFrame(frame -> frame.type(WindowFrameType.ROWS).unitBetween(column("col1"), + UnitType.PRECEEDING, column("col2"), UnitType.FOLLOWING)), + " OVER(window1 ROWS BETWEEN col1 PRECEEDING AND col2 FOLLOWING)"), + + arguments( + OverClause.of("window1") + .windowFrame(frame -> frame.type(WindowFrameType.ROWS).unit(UnitType.CURRENT_ROW) + .exclude(WindowFrameExclusionType.NO_OTHERS)), + " OVER(window1 ROWS CURRENT ROW EXCLUDE NO OTHERS)"), + arguments( + OverClause.of("window1") + .windowFrame(frame -> frame.type(WindowFrameType.ROWS) + .unitBetween(column("col1"), UnitType.CURRENT_ROW, column("col2"), + UnitType.CURRENT_ROW) + .exclude(WindowFrameExclusionType.CURRENT_ROW)), + " OVER(window1 ROWS BETWEEN CURRENT ROW AND CURRENT ROW EXCLUDE CURRENT ROW)")); + } + + @ParameterizedTest + @MethodSource("overClauseArguments") + void testSelectAnalyticFunctionWithOverClause(final OverClause overClause, final String expectedOverClause) { + final AnalyticFunction function = AnalyticFunction.of(ExasolAnalyticAggregateFunctions.AVG, column("age")) // + .over(overClause); + final Select select = StatementFactory.getInstance().select() // + .field("department") // + .function(function, "_AGE"); + select.from().table("employee_table"); + assertThat(select, rendersTo("SELECT department, AVG(age)" + expectedOverClause + " _AGE FROM employee_table")); + } + + @Test + void testSelectAnalyticFunctionOverClauseConfigurator() { + final AnalyticFunction function = AnalyticFunction.of(ExasolAnalyticAggregateFunctions.AVG, column("age")) // + .over(clause -> clause.windowName("window").partitionBy(column("col1")) + .orderBy(new OrderByClause(null, column("col2")).asc()) + .windowFrame(frame -> frame.type(WindowFrameType.ROWS).unit(UnitType.CURRENT_ROW) + .exclude(WindowFrameExclusionType.CURRENT_ROW))); + final Select select = StatementFactory.getInstance().select() // + .field("department") // + .function(function, "_AGE"); + select.from().table("employee_table"); + assertThat(select, rendersTo( + "SELECT department, AVG(age) OVER(window PARTITION BY col1 ORDER BY col2 ASC ROWS CURRENT ROW EXCLUDE CURRENT ROW) _AGE FROM employee_table")); + } + + @Test + void testSelectTwoScalarFunctions() { final Select select = StatementFactory.getInstance().select() // .function(ExasolScalarFunction.ADD_YEARS, "AY1", stringLiteral("2000-02-29"), integerLiteral(1)) // .function(ExasolScalarFunction.ADD_YEARS, "AY2", stringLiteral("2005-01-31 12:00:00"), diff --git a/src/test/java/com/exasol/sql/expression/rendering/OverClauseRendererTest.java b/src/test/java/com/exasol/sql/expression/rendering/OverClauseRendererTest.java new file mode 100644 index 00000000..099f248a --- /dev/null +++ b/src/test/java/com/exasol/sql/expression/rendering/OverClauseRendererTest.java @@ -0,0 +1,64 @@ +package com.exasol.sql.expression.rendering; + +import static com.exasol.sql.expression.ExpressionTerm.column; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.exasol.hamcrest.ExceptionAssertions; +import com.exasol.sql.expression.function.exasol.OverClause; +import com.exasol.sql.expression.function.exasol.WindowFrameClause.UnitType; +import com.exasol.sql.expression.function.exasol.WindowFrameClause.WindowFrameType; +import com.exasol.sql.rendering.StringRendererConfig; + +class OverClauseRendererTest { + + private OverClauseRenderer renderer; + + @BeforeEach + void setUp() { + this.renderer = new OverClauseRenderer(StringRendererConfig.createDefault()); + } + + @Test + void testVisitEmptyClause() { + assertOverClauseRendered(new OverClause(), " OVER()"); + } + + @Test + void testVisitWindowFrameEmptyFails() { + final OverClause clause = new OverClause().windowFrame(frame -> frame); + ExceptionAssertions.assertThrowsWithMessage(IllegalStateException.class, () -> this.renderer.visit(clause), + "E-ESB-3: Type not defined. Set type the window frame."); + } + + @Test + void testVisitWindowFrameWithoutUnitFails() { + final OverClause clause = new OverClause().windowFrame(frame -> frame.type(WindowFrameType.RANGE)); + ExceptionAssertions.assertThrowsWithMessage(IllegalStateException.class, () -> this.renderer.visit(clause), + "E-ESB-1: First unit not defined. At lease one unit is required for a window frame"); + } + + @Test + void testVisitWindowFrameMissingExpressionFails() { + final OverClause clause = new OverClause() + .windowFrame(frame -> frame.type(WindowFrameType.RANGE).unit(UnitType.PRECEEDING)); + ExceptionAssertions.assertThrowsWithMessage(IllegalStateException.class, () -> this.renderer.visit(clause), + "E-ESB-2: Expression is required for window frame units PRECEEDING and FOLLOWING. Add expression for unit types PRECEEDING and FOLLOWING."); + } + + @Test + void testVisitWindowFrameWithExpressionSucceeds() { + assertOverClauseRendered( + new OverClause().windowFrame( + frame -> frame.type(WindowFrameType.RANGE).unit(column("col2"), UnitType.PRECEEDING)), + " OVER( RANGE col2 PRECEEDING)"); + } + + private void assertOverClauseRendered(final OverClause overClause, final String expected) { + this.renderer.visit(overClause); + assertThat(this.renderer.render(), equalTo(expected)); + } +}