Skip to content

Commit 25ddb64

Browse files
committed
HHH-18661 Add unnest() set-returning function and enable XML/JSON based array support on more databases
1 parent 9bb5e64 commit 25ddb64

File tree

225 files changed

+9082
-1026
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

225 files changed

+9082
-1026
lines changed

docker_db.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ use master
434434
go
435435
create login $SYBASE_USER with password $SYBASE_PASSWORD
436436
go
437+
exec sp_configure 'enable xml', 1
438+
go
437439
exec sp_dboption $SYBASE_DB, 'abort tran on log full', true
438440
go
439441
exec sp_dboption $SYBASE_DB, 'allow nulls by default', true

documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc

Lines changed: 107 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
:array-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/array
99
:json-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/json
1010
:xml-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/xml
11+
:srf-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/srf
1112
:extrasdir: extras
1213

1314
This chapter describes Hibernate Query Language (HQL) and Jakarta Persistence Query Language (JPQL).
@@ -1197,32 +1198,33 @@ The following functions deal with SQL array types, which are not supported on ev
11971198
|===
11981199
| Function | Purpose
11991200
1200-
| `array()` | Creates an array based on the passed arguments
1201-
| `array_list()` | Like `array`, but returns the result as `List<?>`
1202-
| `array_agg()` | Aggregates row values into an array
1203-
| `array_position()` | Determines the position of an element in an array
1204-
| `array_positions()` | Determines all positions of an element in an array
1205-
| `array_positions_list()` | Like `array_positions`, but returns the result as `List<Integer>`
1206-
| `array_length()` | Determines the length of an array
1207-
| `array_concat()` | Concatenates array with each other in order
1208-
| `array_prepend()` | Prepends element to array
1209-
| `array_append()` | Appends element to array
1210-
| `array_contains()` | Whether an array contains an element
1211-
| `array_contains_nullable()` | Whether an array contains an element, supporting `null` element
1212-
| `array_includes()` | Whether an array contains another array
1213-
| `array_includes_nullable()` | Whether an array contains another array, supporting `null` elements
1214-
| `array_intersects()` | Whether an array holds at least one element of another array
1215-
| `array_intersects_nullable()` | Whether an array holds at least one element of another array, supporting `null` elements
1216-
| `array_get()` | Accesses the element of an array by index
1217-
| `array_set()` | Creates array copy with given element at given index
1218-
| `array_remove()` | Creates array copy with given element removed
1219-
| `array_remove_index()` | Creates array copy with the element at the given index removed
1220-
| `array_slice()` | Creates a sub-array of the based on lower and upper index
1221-
| `array_replace()` | Creates array copy replacing a given element with another
1222-
| `array_trim()` | Creates array copy trimming the last _N_ elements
1223-
| `array_fill()` | Creates array filled with the same element _N_ times
1224-
| `array_fill_list()` | Like `array_fill`, but returns the result as `List<?>`
1225-
| `array_to_string()` | String representation of array
1201+
| <<hql-array-constructor-functions,`array()`>> | Creates an array based on the passed arguments
1202+
| <<hql-array-constructor-functions,`array_list()`>> | Like `array`, but returns the result as `List<?>`
1203+
| <<hql-array-aggregate-functions,`array_agg()`>> | Aggregates row values into an array
1204+
| <<hql-array-position-functions,`array_position()`>> | Determines the position of an element in an array
1205+
| <<hql-array-positions-functions,`array_positions()`>> | Determines all positions of an element in an array
1206+
| <<hql-array-positions-functions,`array_positions_list()`>> | Like `array_positions`, but returns the result as `List<Integer>`
1207+
| <<hql-array-length-functions,`array_length()`>> | Determines the length of an array
1208+
| <<hql-array-concat-functions,`array_concat()`>> | Concatenates array with each other in order
1209+
| <<hql-array-prepend-functions,`array_prepend()`>> | Prepends element to array
1210+
| <<hql-array-append-functions,`array_append()`>> | Appends element to array
1211+
| <<hql-array-contains-functions,`array_contains()`>> | Whether an array contains an element
1212+
| <<hql-array-contains-functions,`array_contains_nullable()`>> | Whether an array contains an element, supporting `null` element
1213+
| <<hql-array-includes-example,`array_includes()`>> | Whether an array contains another array
1214+
| <<hql-array-includes-example,`array_includes_nullable()`>> | Whether an array contains another array, supporting `null` elements
1215+
| <<hql-array-intersects-functions,`array_intersects()`>> | Whether an array holds at least one element of another array
1216+
| <<hql-array-intersects-functions,`array_intersects_nullable()`>> | Whether an array holds at least one element of another array, supporting `null` elements
1217+
| <<hql-array-get-functions,`array_get()`>> | Accesses the element of an array by index
1218+
| <<hql-array-set-functions,`array_set()`>> | Creates array copy with given element at given index
1219+
| <<hql-array-remove-functions,`array_remove()`>> | Creates array copy with given element removed
1220+
| <<hql-array-remove-index-functions,`array_remove_index()`>> | Creates array copy with the element at the given index removed
1221+
| <<hql-array-slice-functions,`array_slice()`>> | Creates a sub-array of the based on lower and upper index
1222+
| <<hql-array-replace-functions,`array_replace()`>> | Creates array copy replacing a given element with another
1223+
| <<hql-array-trim-functions,`array_trim()`>> | Creates array copy trimming the last _N_ elements
1224+
| <<hql-array-fill-functions,`array_fill()`>> | Creates array filled with the same element _N_ times
1225+
| <<hql-array-fill-functions,`array_fill_list()`>> | Like `array_fill`, but returns the result as `List<?>`
1226+
| <<hql-array-to-string-functions,`array_to_string()`>> | String representation of array
1227+
| <<hql-array-unnest,`unnest()`>> | Turns an array into rows
12261228
|===
12271229
12281230
[[hql-array-constructor-functions]]
@@ -1637,6 +1639,32 @@ include::{array-example-dir-hql}/ArrayToStringTest.java[tags=hql-array-to-string
16371639
----
16381640
====
16391641
1642+
[[hql-array-unnest]]
1643+
===== `unnest()`
1644+
1645+
A <<hql-from-set-returning-functions,set-returning function>>, which turns the single array argument into rows.
1646+
Returns no rows if the array argument is `null` or an empty array.
1647+
The `index()` function can be used to access the 1-based array index of an array element.
1648+
1649+
[[hql-array-unnest-struct-example]]
1650+
====
1651+
[source, java, indent=0]
1652+
----
1653+
include::{array-example-dir-hql}/ArrayUnnestStructTest.java[tags=hql-array-unnest-aggregate-with-ordinality-example]
1654+
----
1655+
====
1656+
1657+
The `lateral` keyword is mandatory if the argument refers to a from node item of the same query level.
1658+
Basic plural attributes can also be joined directly, which is syntax sugar for `lateral unnest(...)`.
1659+
1660+
[[hql-array-unnest-example]]
1661+
====
1662+
[source, java, indent=0]
1663+
----
1664+
include::{array-example-dir-hql}/ArrayUnnestTest.java[tags=hql-array-unnest-example]
1665+
----
1666+
====
1667+
16401668
[[hql-functions-json]]
16411669
==== Functions for dealing with JSON
16421670
@@ -2916,6 +2944,48 @@ The CTE name can be used for a `from` clause root or a `join`, similar to entity
29162944
29172945
Refer to the <<hql-with-cte,with clause>> chapter for details about CTEs.
29182946
2947+
[[hql-from-set-returning-functions]]
2948+
==== Set-returning functions in `from` clause
2949+
2950+
A set-returning function is a function that produces rows instead of a single scalar value
2951+
and is exclusively used in the `from` clause, either as root node or join target.
2952+
2953+
The `index()` function can be used to access the 1-based index of a returned row.
2954+
2955+
The following set-returning functions are available on many platforms:
2956+
2957+
|===
2958+
| Function | purpose
2959+
2960+
| <<hql-array-unnest,`unnest()`>> | Turns an array into rows
2961+
//| `generate_series()` | Creates a series of values as rows
2962+
|===
2963+
2964+
To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`:
2965+
2966+
[[hql-from-set-returning-functions-contributor-example]]
2967+
====
2968+
[source, java, indent=0]
2969+
----
2970+
include::{srf-example-dir-hql}/CustomSetReturningFunctionTest.java[tags=hql-set-returning-function-contributor-example]
2971+
----
2972+
====
2973+
2974+
After that, the function can be used in the `from` clause:
2975+
2976+
[[hql-from-set-returning-functions-custom-example]]
2977+
====
2978+
[source, java, indent=0]
2979+
----
2980+
include::{srf-example-dir-hql}/CustomSetReturningFunctionTest.java[tags=hql-set-returning-function-custom-example]
2981+
----
2982+
====
2983+
2984+
NOTE: The `index()` function represents the idea of the `with ordinality` SQL syntax,
2985+
which is not supported on some databases for user defined functions.
2986+
Hibernate ORM tries to emulate this feature by wrapping invocations as lateral subqueries and using `row_number()`,
2987+
which may lead to worse performance.
2988+
29192989
[[hql-join]]
29202990
=== Declaring joined entities
29212991
@@ -3131,6 +3201,17 @@ Most databases support some flavor of `join lateral`, and Hibernate emulates the
31313201
But emulation is neither very efficient, nor does it support all possible query shapes, so it's important to test on your target database.
31323202
====
31333203
3204+
[[hql-join-set-returning-function]]
3205+
==== Set-returning functions in joins
3206+
3207+
A `join` clause may contain a set-returning function, either:
3208+
3209+
- an uncorrelated set-returning function, which is almost the same as a <<hql-from-set-returning-functions,set-returning function in the `from` clause>>, except that it may have an `on` restriction, or
3210+
- a _lateral join_, which is a correlated set-returning function, and may refer to other roots declared earlier in the same `from` clause.
3211+
3212+
The `lateral` keyword just distinguishes the two cases.
3213+
A lateral join may be an inner or left outer join, but not a right join, nor a full join.
3214+
31343215
[[hql-implicit-join]]
31353216
==== Implicit association joins (path expressions)
31363217

hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.regex.Matcher;
1919
import java.util.regex.Pattern;
2020

21+
import org.checkerframework.checker.nullness.qual.Nullable;
2122
import org.hibernate.LockMode;
2223
import org.hibernate.LockOptions;
2324
import org.hibernate.PessimisticLockException;
@@ -92,7 +93,6 @@
9293
import static org.hibernate.type.SqlTypes.INET;
9394
import static org.hibernate.type.SqlTypes.INTEGER;
9495
import static org.hibernate.type.SqlTypes.JSON;
95-
import static org.hibernate.type.SqlTypes.JSON_ARRAY;
9696
import static org.hibernate.type.SqlTypes.LONG32NVARCHAR;
9797
import static org.hibernate.type.SqlTypes.LONG32VARBINARY;
9898
import static org.hibernate.type.SqlTypes.LONG32VARCHAR;
@@ -263,11 +263,9 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR
263263
if ( getVersion().isSameOrAfter( 20 ) ) {
264264
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INET, "inet", this ) );
265265
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) );
266-
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) );
267266
}
268267
else {
269268
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) );
270-
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) );
271269
}
272270
ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) );
273271
ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) );
@@ -372,23 +370,23 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser
372370
if ( getVersion().isSameOrAfter( 20, 0 ) ) {
373371
jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getInetJdbcType( serviceRegistry ) );
374372
jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) );
375-
jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) );
373+
jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) );
376374
}
377375
else {
378376
jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonJdbcType( serviceRegistry ) );
379-
jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) );
377+
jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) );
380378
}
381379
}
382380
else {
383381
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE );
384382
if ( getVersion().isSameOrAfter( 20, 0 ) ) {
385383
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE );
386384
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE );
387-
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE );
385+
jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE );
388386
}
389387
else {
390388
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE );
391-
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE );
389+
jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSON_INSTANCE );
392390
}
393391
}
394392
}
@@ -398,11 +396,11 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser
398396
if ( getVersion().isSameOrAfter( 20, 0 ) ) {
399397
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE );
400398
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE );
401-
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE );
399+
jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE );
402400
}
403401
else {
404402
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE );
405-
jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE );
403+
jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSON_INSTANCE );
406404
}
407405
}
408406

@@ -424,6 +422,7 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser
424422
)
425423
);
426424

425+
// Replace the standard array constructor
427426
jdbcTypeRegistry.addTypeConstructor( PostgreSQLArrayJdbcTypeConstructor.INSTANCE );
428427
}
429428

@@ -518,6 +517,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
518517
functionFactory.jsonArrayAppend_postgresql( false );
519518
functionFactory.jsonArrayInsert_postgresql();
520519

520+
functionFactory.unnest_postgresql();
521+
521522
// Postgres uses # instead of ^ for XOR
522523
functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" )
523524
.setExactArgumentCount( 2 )
@@ -534,6 +535,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
534535
functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" );
535536
}
536537

538+
@Override
539+
public @Nullable String getDefaultOrdinalityColumnName() {
540+
return "ordinality";
541+
}
542+
537543
@Override
538544
public TimeZoneSupport getTimeZoneSupport() {
539545
return TimeZoneSupport.NORMALIZE;

hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl;
8989
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
9090
import org.hibernate.type.JavaObjectType;
91+
import org.hibernate.type.SqlTypes;
9192
import org.hibernate.type.StandardBasicTypes;
9293
import org.hibernate.type.descriptor.ValueExtractor;
9394
import org.hibernate.type.descriptor.java.JavaType;
@@ -455,6 +456,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio
455456
functionFactory.xmlexists_db2_legacy();
456457
}
457458
functionFactory.xmlagg();
459+
460+
functionFactory.unnest_emulated();
461+
}
462+
463+
@Override
464+
public int getPreferredSqlTypeCodeForArray() {
465+
// Even if DB2 11 supports JSON functions, it's not possible to unnest a JSON array to rows, so stick to XML
466+
return SqlTypes.XML_ARRAY;
458467
}
459468

460469
@Override

hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
import org.hibernate.LockMode;
1111
import org.hibernate.dialect.DatabaseVersion;
1212
import org.hibernate.engine.spi.SessionFactoryImplementor;
13+
import org.hibernate.metamodel.mapping.CollectionPart;
1314
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
15+
import org.hibernate.metamodel.mapping.ModelPart;
1416
import org.hibernate.query.IllegalQueryOperationException;
17+
import org.hibernate.query.derived.AnonymousTupleTableGroupProducer;
1518
import org.hibernate.query.sqm.ComparisonOperator;
1619
import org.hibernate.query.sqm.FetchClauseType;
1720
import org.hibernate.sql.ast.Clause;
1821
import org.hibernate.sql.ast.SqlAstNodeRenderingMode;
1922
import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator;
2023
import org.hibernate.sql.ast.spi.SqlSelection;
2124
import org.hibernate.sql.ast.tree.MutationStatement;
25+
import org.hibernate.sql.ast.tree.SqlAstNode;
2226
import org.hibernate.sql.ast.tree.Statement;
2327
import org.hibernate.sql.ast.tree.delete.DeleteStatement;
2428
import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression;
@@ -28,6 +32,8 @@
2832
import org.hibernate.sql.ast.tree.expression.Literal;
2933
import org.hibernate.sql.ast.tree.expression.SqlTuple;
3034
import org.hibernate.sql.ast.tree.expression.Summarization;
35+
import org.hibernate.sql.ast.tree.from.DerivedTableReference;
36+
import org.hibernate.sql.ast.tree.from.FunctionTableReference;
3137
import org.hibernate.sql.ast.tree.from.NamedTableReference;
3238
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
3339
import org.hibernate.sql.ast.tree.from.TableGroup;
@@ -253,6 +259,34 @@ public void visitQueryPartTableReference(QueryPartTableReference tableReference)
253259
inLateral = oldLateral;
254260
}
255261

262+
@Override
263+
protected void renderDerivedTableReference(DerivedTableReference tableReference) {
264+
if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) {
265+
// No need for a lateral keyword for functions
266+
tableReference.accept( this );
267+
}
268+
else {
269+
super.renderDerivedTableReference( tableReference );
270+
}
271+
}
272+
273+
@Override
274+
public void renderNamedSetReturningFunction(String functionName, List<? extends SqlAstNode> sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) {
275+
final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null );
276+
if ( ordinalitySubPart != null ) {
277+
appendSql( "lateral (select t.*, row_number() over() " );
278+
appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() );
279+
appendSql( " from table(" );
280+
renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode );
281+
append( ") t)" );
282+
}
283+
else {
284+
appendSql( "table(" );
285+
super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode );
286+
append( ')' );
287+
}
288+
}
289+
256290
@Override
257291
public void visitSelectStatement(SelectStatement statement) {
258292
if ( getQueryPartForRowNumbering() == statement.getQueryPart() && inLateral ) {

0 commit comments

Comments
 (0)