Skip to content

Commit

Permalink
HHH-18731 Add generate_series() set-returning function
Browse files Browse the repository at this point in the history
  • Loading branch information
beikov committed Oct 18, 2024
1 parent 044d914 commit 970b268
Show file tree
Hide file tree
Showing 70 changed files with 3,971 additions and 200 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2958,7 +2958,7 @@ The following set-returning functions are available on many platforms:
| Function | purpose
| <<hql-array-unnest,`unnest()`>> | Turns an array into rows
//| `generate_series()` | Creates a series of values as rows
| <<hql-from-set-returning-functions-generate-series,`generate_series()`>> | Creates a series of values as rows
|===
To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`:
Expand Down Expand Up @@ -2986,6 +2986,43 @@ which is not supported on some databases for user defined functions.
Hibernate ORM tries to emulate this feature by wrapping invocations as lateral subqueries and using `row_number()`,
which may lead to worse performance.
[[hql-from-set-returning-functions-generate-series]]
==== `generate_series` set-returning function
A <<hql-from-set-returning-functions,set-returning function>>, which generates rows from a given start value (inclusive)
up to a given stop value (inclusive). The function has 2 variants:
* `generate_series(numeric, numeric [,numeric])` - Arguments are `start`, `stop` and `step` with a default of `1` for the optional `step` argument
* `generate_series(temporal, temporal, duration)` - Like the numeric variant, but for temporal types and `step` is required
[[hql-generate-series-example]]
====
[source, java, indent=0]
----
include::{srf-example-dir-hql}/GenerateSeriesTest.java[tags=hql-set-returning-function-generate-series-example]
----
====
To obtain the "row number" of a generated value i.e. ordinality, it is possible to use the `index()` function.
[[hql-generate-series-ordinality-example]]
====
[source, java, indent=0]
----
include::{srf-example-dir-hql}/GenerateSeriesTest.java[tags=hql-set-returning-function-generate-series-ordinality-example]
----
====
The `step` argument can be a negative value and progress from a higher `start` value to a lower `stop` value.
[[hql-generate-series-temporal-example]]
====
[source, java, indent=0]
----
include::{srf-example-dir-hql}/GenerateSeriesTest.java[tags=hql-set-returning-function-generate-series-temporal-example]
----
====
[[hql-join]]
=== Declaring joined entities
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.jsonArrayInsert_postgresql();

functionFactory.unnest_postgresql();
functionFactory.generateSeries( null, "ordinality", true );

// Postgres uses # instead of ^ for XOR
functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,18 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlagg();

functionFactory.unnest_emulated();
if ( supportsRecursiveCTE() ) {
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, true );
}
}

/**
* DB2 doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount
* of elements that the series can return.
*/
protected int getMaximumSeriesSize() {
return 10000;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
}

functionFactory.unnest_h2( getMaximumArraySize() );
functionFactory.generateSeries_h2( getMaximumSeriesSize() );
}

/**
Expand All @@ -441,6 +442,16 @@ protected int getMaximumArraySize() {
return 1000;
}

/**
* Since H2 doesn't support ordinality for the {@code system_range} function or {@code lateral},
* it's impossible to use {@code system_range} for non-constant cases.
* Luckily, correlation can be emulated, but requires that there is an upper bound on the amount
* of elements that the series can return.
*/
protected int getMaximumSeriesSize() {
return 10000;
}

@Override
public @Nullable String getDefaultOrdinalityColumnName() {
return "nord";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.unnest_hana();
// functionFactory.json_table();

functionFactory.generateSeries_hana( getMaximumSeriesSize() );

if ( getVersion().isSameOrAfter(2, 0, 20 ) ) {
if ( getVersion().isSameOrAfter( 2, 0, 40 ) ) {
// Introduced in 2.0 SPS 04
Expand All @@ -513,6 +515,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
}
}

/**
* HANA doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with the {@code xmltable} and {@code lpad} functions.
*/
protected int getMaximumSeriesSize() {
return 10000;
}

@Override
public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
return new StandardSqlAstTranslatorFactory() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
}

functionFactory.unnest( "c1", "c2" );
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );

//trim() requires parameters to be cast when used as trim character
functionContributions.getFunctionRegistry().register( "trim", new TrimFunction(
Expand All @@ -288,6 +289,16 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
) );
}

/**
* HSQLDB doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount
* of elements that the series can return.
*/
protected int getMaximumSeriesSize() {
// The maximum recursion depth of HSQLDB
return 258;
}

@Override
public @Nullable String getDefaultOrdinalityColumnName() {
return "c2";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,9 +676,22 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
if ( getMySQLVersion().isSameOrAfter( 8 ) ) {
functionFactory.unnest_emulated();
}
if ( supportsRecursiveCTE() ) {
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, false );
}
}
}

/**
* MySQL doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount
* of elements that the series can return.
*/
protected int getMaximumSeriesSize() {
// The maximum recursion depth of MySQL
return 1000;
}

@Override
public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
super.contributeTypes( typeContributions, serviceRegistry );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,16 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlagg();

functionFactory.unnest_oracle();
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false );
}

/**
* Oracle doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount
* of elements that the series can return.
*/
protected int getMaximumSeriesSize() {
return 10000;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
else {
functionFactory.unnest_postgresql();
}
functionFactory.generateSeries( null, "ordinality", false );
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,13 +440,32 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.leastGreatest();
functionFactory.dateTrunc_datetrunc();
functionFactory.trunc_round_datetrunc();
functionFactory.generateSeries_sqlserver( getMaximumSeriesSize() );
}
else {
functionContributions.getFunctionRegistry().register(
"trunc",
new SqlServerConvertTruncFunction( functionContributions.getTypeConfiguration() )
);
functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" );
if ( supportsRecursiveCTE() ) {
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, false );
}
}
}

/**
* SQL Server doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount
* of elements that the series can return.
*/
protected int getMaximumSeriesSize() {
if ( getVersion().isSameOrAfter( 16 ) ) {
return 10000;
}
else {
// The maximum recursion depth of SQL Server
return 100;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
CommonFunctionFactory functionFactory = new CommonFunctionFactory( functionContributions);

functionFactory.unnest_sybasease();
functionFactory.generateSeries_sybasease( getMaximumSeriesSize() );
}

/**
* Sybase ASE doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with the {@code xmltable} and {@code replicate} functions.
*/
protected int getMaximumSeriesSize() {
// The maximum possible value for replicating an XML tag, so that the resulting string stays below the 16K limit
// https://infocenter.sybase.com/help/index.jsp?topic=/com.sybase.infocenter.dc32300.1570/html/sqlug/sqlug31.htm
return 4094;
}

private static boolean isAnsiNull(DatabaseMetaData databaseMetaData) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.jsonArrayInsert_postgresql();

functionFactory.unnest_postgresql();
functionFactory.generateSeries( null, "ordinality", true );

// Postgres uses # instead of ^ for XOR
functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" )
Expand Down
10 changes: 10 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,16 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
functionFactory.xmlagg();

functionFactory.unnest_emulated();
functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, true );
}

/**
* DB2 doesn't support the {@code generate_series} function or {@code lateral} recursive CTEs,
* so it has to be emulated with a top level recursive CTE which requires an upper bound on the amount
* of elements that the series can return.
*/
protected int getMaximumSeriesSize() {
return 10000;
}

@Override
Expand Down
102 changes: 98 additions & 4 deletions hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalAmount;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
Expand Down Expand Up @@ -5620,11 +5623,102 @@ public void appendDateTimeLiteral(
* {@link Duration}.
*/
public void appendIntervalLiteral(SqlAppender appender, Duration literal) {
final int nano = literal.getNano();
final int secondsPart = literal.toSecondsPart();
final int minutesPart = literal.toMinutesPart();
final int hoursPart = literal.toHoursPart();
final long daysPart = literal.toDaysPart();
enum Unit { day, hour, minute }
final Unit unit;
if ( daysPart != 0 ) {
unit = hoursPart == 0 && minutesPart == 0 && secondsPart == 0 && nano == 0
? Unit.day
: null;
}
else if ( hoursPart != 0 ) {
unit = minutesPart == 0 && secondsPart == 0 && nano == 0
? Unit.hour
: null;
}
else if ( minutesPart != 0 ) {
unit = secondsPart == 0 && nano == 0
? Unit.minute
: null;
}
else {
unit = null;
}
appender.appendSql( "interval '" );
appender.appendSql( literal.getSeconds() );
appender.appendSql( '.' );
appender.appendSql( literal.getNano() );
appender.appendSql( "' second" );
if ( unit != null ) {
appender.appendSql( switch( unit ) {
case day -> daysPart;
case hour -> hoursPart;
case minute -> minutesPart;
});
appender.appendSql( "' " );
appender.appendSql( unit.toString() );
}
else {
appender.appendSql( "interval '" );
appender.appendSql( literal.getSeconds() );
if ( nano > 0 ) {
appender.appendSql( '.' );
appender.appendSql( nano );
}
appender.appendSql( "' second" );
}
}

/**
* Append a literal SQL {@code interval} representing the given Java
* {@link TemporalAmount}.
*/
public void appendIntervalLiteral(SqlAppender appender, TemporalAmount literal) {
if ( literal instanceof Duration duration ) {
appendIntervalLiteral( appender, duration );
}
else if ( literal instanceof Period period ) {
final int years = period.getYears();
final int months = period.getMonths();
final int days = period.getDays();
final boolean parenthesis = years != 0 && months != 0
|| years != 0 && days != 0
|| months != 0 && days != 0;
if ( parenthesis ) {
appender.appendSql( '(' );
}
boolean first = true;
for ( java.time.temporal.TemporalUnit unit : literal.getUnits() ) {
final long value = literal.get( unit );
if ( value != 0 ) {
if ( first ) {
first = false;
}
else {
appender.appendSql( "+" );
}
appender.appendSql( "interval '" );
appender.appendSql( value );
appender.appendSql( "' " );
if ( unit == ChronoUnit.YEARS ) {
appender.appendSql( "year" );
}
else if ( unit == ChronoUnit.MONTHS ) {
appender.appendSql( "month" );
}
else {
assert unit == ChronoUnit.DAYS;
appender.appendSql( "day" );
}
}
}
if ( parenthesis ) {
appender.appendSql( ')' );
}
}
else {
throw new IllegalArgumentException( "Unsupported temporal amount type: " + literal );
}
}

/**
Expand Down
Loading

0 comments on commit 970b268

Please sign in to comment.