From 25ddb64a4c3ba3b6451d1684eea87714e46bacc9 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 9 Oct 2024 14:15:14 +0200 Subject: [PATCH] HHH-18661 Add unnest() set-returning function and enable XML/JSON based array support on more databases --- docker_db.sh | 2 + .../chapters/query/hql/QueryLanguage.adoc | 133 ++++- .../dialect/CockroachLegacyDialect.java | 24 +- .../community/dialect/DB2LegacyDialect.java | 9 + .../dialect/DB2LegacySqlAstTranslator.java | 34 ++ .../dialect/DB2zLegacySqlAstTranslator.java | 28 +- .../community/dialect/H2LegacyDialect.java | 13 +- .../community/dialect/HANALegacyDialect.java | 26 +- .../dialect/HANALegacySqlAstTranslator.java | 89 ++- .../community/dialect/HSQLLegacyDialect.java | 8 + .../dialect/HSQLLegacySqlAstTranslator.java | 13 + .../dialect/MariaDBLegacyDialect.java | 9 +- .../MariaDBLegacySqlAstTranslator.java | 6 + .../community/dialect/MySQLLegacyDialect.java | 11 +- .../dialect/MySQLLegacySqlAstTranslator.java | 23 + .../dialect/OracleLegacyDialect.java | 10 +- .../dialect/OracleLegacySqlAstTranslator.java | 43 +- .../dialect/PostgreSQLLegacyDialect.java | 29 +- .../dialect/SQLServerLegacyDialect.java | 14 +- .../SQLServerLegacySqlAstTranslator.java | 35 +- .../dialect/SybaseASELegacyDialect.java | 17 + .../SybaseASELegacySqlAstTranslator.java | 66 ++- .../dialect/SybaseLegacySqlAstTranslator.java | 2 +- .../org/hibernate/grammars/hql/HqlParser.g4 | 17 +- .../process/spi/MetadataBuildingProcess.java | 39 +- .../hibernate/dialect/CockroachDialect.java | 17 +- .../org/hibernate/dialect/DB2Dialect.java | 9 + .../dialect/DB2SqlAstTranslator.java | 34 ++ .../hibernate/dialect/DB2StructJdbcType.java | 5 + .../dialect/DB2zSqlAstTranslator.java | 28 +- .../java/org/hibernate/dialect/Dialect.java | 10 +- .../java/org/hibernate/dialect/H2Dialect.java | 13 +- .../dialect/H2JsonArrayJdbcType.java | 8 +- .../H2JsonArrayJdbcTypeConstructor.java | 43 ++ .../org/hibernate/dialect/HANADialect.java | 33 +- .../dialect/HANASqlAstTranslator.java | 86 ++- .../org/hibernate/dialect/HSQLDialect.java | 8 + .../dialect/HSQLSqlAstTranslator.java | 13 + .../org/hibernate/dialect/JsonHelper.java | 295 ++++++++-- .../org/hibernate/dialect/MariaDBDialect.java | 9 +- .../dialect/MariaDBSqlAstTranslator.java | 6 + .../MySQLCastingJsonArrayJdbcType.java | 9 +- ...QLCastingJsonArrayJdbcTypeConstructor.java | 43 ++ .../org/hibernate/dialect/MySQLDialect.java | 11 +- .../dialect/MySQLSqlAstTranslator.java | 13 + .../org/hibernate/dialect/OracleDialect.java | 14 +- .../dialect/OracleJsonArrayJdbcType.java | 8 +- .../OracleJsonArrayJdbcTypeConstructor.java | 51 ++ .../dialect/OracleSqlAstTranslator.java | 43 +- .../org/hibernate/dialect/PgJdbcHelper.java | 24 +- .../dialect/PostgreSQLArrayJdbcType.java | 4 +- .../PostgreSQLCastingJsonArrayJdbcType.java | 7 +- ...QLCastingJsonArrayJdbcTypeConstructor.java | 50 ++ .../hibernate/dialect/PostgreSQLDialect.java | 22 +- ...nArrayPGObjectJsonJdbcTypeConstructor.java | 41 ++ .../PostgreSQLJsonArrayPGObjectJsonType.java | 14 - ...ArrayPGObjectJsonbJdbcTypeConstructor.java | 41 ++ .../PostgreSQLJsonArrayPGObjectJsonbType.java | 14 - ...a => PostgreSQLJsonArrayPGObjectType.java} | 13 +- .../SQLServerCastingXmlArrayJdbcType.java | 29 + ...verCastingXmlArrayJdbcTypeConstructor.java | 43 ++ .../dialect/SQLServerCastingXmlJdbcType.java | 43 ++ .../hibernate/dialect/SQLServerDialect.java | 13 +- .../dialect/SQLServerSqlAstTranslator.java | 35 +- .../dialect/SpannerSqlAstTranslator.java | 18 +- .../hibernate/dialect/SybaseASEDialect.java | 17 + .../dialect/SybaseASESqlAstTranslator.java | 81 ++- .../dialect/SybaseSqlAstTranslator.java | 2 +- .../dialect/TiDBSqlAstTranslator.java | 6 + .../java/org/hibernate/dialect/XmlHelper.java | 71 ++- .../dialect/aggregate/AggregateSupport.java | 44 +- .../aggregate/AggregateSupportImpl.java | 9 +- .../aggregate/DB2AggregateSupport.java | 9 +- .../aggregate/OracleAggregateSupport.java | 23 +- .../aggregate/PostgreSQLAggregateSupport.java | 19 +- .../function/CommonFunctionFactory.java | 125 ++-- ...nnestSetReturningFunctionTypeResolver.java | 222 ++++++++ .../function/array/H2UnnestFunction.java | 338 +++++++++++ .../function/array/HANAUnnestFunction.java | 533 ++++++++++++++++++ .../function/array/OracleUnnestFunction.java | 50 ++ .../array/PostgreSQLUnnestFunction.java | 71 +++ .../array/SQLServerUnnestFunction.java | 162 ++++++ .../array/SybaseASEUnnestFunction.java | 93 +++ .../function/array/UnnestFunction.java | 216 +++++++ .../function/json/JsonArrayAggFunction.java | 2 +- .../function/json/JsonArrayFunction.java | 2 +- .../function/xml/SQLServerXmlAggFunction.java | 2 + .../function/xml/XmlElementFunction.java | 30 +- .../function/xml/XmlForestFunction.java | 28 +- .../engine/spi/LazySessionWrapperOptions.java | 68 +++ .../org/hibernate/mapping/BasicValue.java | 1 - .../java/org/hibernate/mapping/Column.java | 32 +- .../metamodel/mapping/SqlTypedMapping.java | 12 +- .../internal/EmbeddableMappingTypeImpl.java | 18 +- .../mapping/internal/SqlTypedMappingImpl.java | 36 +- .../domain/internal/MappingMetamodelImpl.java | 4 +- .../internal/SingularAttributeImpl.java | 23 +- .../query/criteria/CriteriaDefinition.java | 5 + .../criteria/HibernateCriteriaBuilder.java | 31 + .../org/hibernate/query/criteria/JpaFrom.java | 127 +++++ .../query/criteria/JpaFunctionFrom.java | 26 + .../query/criteria/JpaFunctionJoin.java | 37 ++ .../query/criteria/JpaFunctionRoot.java | 15 + .../query/criteria/JpaSelectCriteria.java | 9 + .../criteria/JpaSetReturningFunction.java | 20 + .../spi/HibernateCriteriaBuilderDelegate.java | 19 + ...mousTupleBasicEntityIdentifierMapping.java | 10 + .../AnonymousTupleBasicValuedModelPart.java | 91 ++- ...onymousTupleEmbeddableValuedModelPart.java | 9 +- ...sTupleEmbeddedEntityIdentifierMapping.java | 7 +- .../AnonymousTupleEntityValuedModelPart.java | 7 +- ...eNonAggregatedEntityIdentifierMapping.java | 7 +- ...ymousTupleSqmAssociationPathSourceNew.java | 127 +++++ .../AnonymousTupleSqmPathSourceNew.java | 94 +++ .../AnonymousTupleTableGroupProducer.java | 79 ++- .../query/derived/AnonymousTupleType.java | 173 +++--- .../derived/CteTupleTableGroupProducer.java | 8 +- .../hql/internal/SemanticQueryBuilder.java | 110 +++- .../query/results/FromClauseAccessImpl.java | 12 + .../org/hibernate/query/sqm/NodeBuilder.java | 10 + .../query/sqm/SemanticQueryWalker.java | 9 + .../sqm/StrictJpaComplianceViolation.java | 1 + ...nderingSetReturningFunctionDescriptor.java | 50 ++ ...ractSqmSetReturningFunctionDescriptor.java | 100 ++++ .../query/sqm/function/FunctionKind.java | 3 +- ...amedSqmSetReturningFunctionDescriptor.java | 85 +++ .../SelfRenderingSqmSetReturningFunction.java | 185 ++++++ .../SetReturningFunctionRenderer.java | 40 ++ .../sqm/function/SqmFunctionRegistry.java | 89 ++- .../SqmSetReturningFunctionDescriptor.java | 79 +++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 30 + .../query/sqm/internal/SqmTreePrinter.java | 38 ++ ...SetReturningFunctionDescriptorBuilder.java | 96 ++++ .../SetReturningFunctionTypeResolver.java | 105 ++++ ...tReturningFunctionTypeResolverBuilder.java | 251 +++++++++ .../sqm/spi/BaseSemanticQueryWalker.java | 46 +- .../sqm/sql/BaseSqmToSqlAstConverter.java | 148 ++++- .../query/sqm/sql/SqmToSqlAstConverter.java | 2 +- .../query/sqm/tree/cte/SqmCteTable.java | 11 +- .../sqm/tree/domain/AbstractSqmFrom.java | 85 +++ .../sqm/tree/domain/AbstractSqmPath.java | 6 +- .../sqm/tree/domain/SqmFunctionRoot.java | 140 +++++ .../expression/SqmSetReturningFunction.java | 101 ++++ .../query/sqm/tree/from/SqmFunctionJoin.java | 227 ++++++++ .../sqm/tree/jpa/ParameterCollector.java | 10 + .../tree/select/AbstractSqmSelectQuery.java | 11 + .../hibernate/sql/ast/SqlAstTranslator.java | 11 + .../ColumnQualifierCollectorSqlAstWalker.java | 30 + .../sql/ast/spi/AbstractSqlAstTranslator.java | 118 ++-- .../sql/ast/spi/FromClauseAccess.java | 4 + .../ast/spi/SimpleFromClauseAccessImpl.java | 12 + .../ast/tree/expression/ColumnReference.java | 6 +- .../tree/expression/LiteralAsParameter.java | 6 +- .../sql/ast/tree/from/FunctionTableGroup.java | 9 +- .../ast/tree/from/FunctionTableReference.java | 20 + .../sql/ast/tree/from/TableGroup.java | 15 + .../java/AbstractArrayJavaType.java | 9 +- .../java/spi/BasicCollectionJavaType.java | 36 +- .../type/descriptor/jdbc/ArrayJdbcType.java | 4 +- .../descriptor/jdbc/JdbcTypeIndicators.java | 20 + .../descriptor/jdbc/JsonArrayJdbcType.java | 53 +- .../jdbc/JsonArrayJdbcTypeConstructor.java | 40 ++ ...pe.java => JsonAsStringArrayJdbcType.java} | 49 +- .../JsonAsStringArrayJdbcTypeConstructor.java | 40 ++ .../jdbc/OracleJsonArrayBlobJdbcType.java | 7 +- .../descriptor/jdbc/XmlArrayJdbcType.java | 129 +++++ .../jdbc/XmlArrayJdbcTypeConstructor.java | 40 ++ .../jdbc/XmlAsStringArrayJdbcType.java | 166 ++++++ .../XmlAsStringArrayJdbcTypeConstructor.java | 40 ++ .../descriptor/jdbc/spi/JdbcTypeRegistry.java | 8 + .../component/StructComponentArrayTest.java | 2 +- .../StructComponentAssociationErrorTest.java | 2 +- ...ctNestedComponentAssociationErrorTest.java | 2 +- .../function/array/ArrayAggregateTest.java | 1 + .../test/function/array/ArrayAppendTest.java | 1 + .../test/function/array/ArrayConcatTest.java | 1 + .../ArrayConstructorInSelectClauseTest.java | 1 + .../function/array/ArrayConstructorTest.java | 1 + .../function/array/ArrayContainsTest.java | 1 + .../test/function/array/ArrayFillTest.java | 1 + .../orm/test/function/array/ArrayGetTest.java | 1 + .../function/array/ArrayIncludesTest.java | 1 + .../function/array/ArrayIntersectsTest.java | 1 + .../test/function/array/ArrayLengthTest.java | 1 + .../function/array/ArrayPositionTest.java | 1 + .../function/array/ArrayPositionsTest.java | 1 + .../test/function/array/ArrayPrependTest.java | 1 + .../function/array/ArrayRemoveIndexTest.java | 1 + .../test/function/array/ArrayRemoveTest.java | 1 + .../test/function/array/ArrayReplaceTest.java | 1 + .../orm/test/function/array/ArraySetTest.java | 1 + .../test/function/array/ArraySliceTest.java | 1 + .../function/array/ArrayToStringTest.java | 1 + .../test/function/array/ArrayTrimTest.java | 1 + .../function/array/ArrayUnnestStructTest.java | 287 ++++++++++ .../test/function/array/ArrayUnnestTest.java | 182 ++++++ .../function/json/JsonArrayUnnestTest.java | 281 +++++++++ .../srf/CustomSetReturningFunctionTest.java | 266 +++++++++ .../test/function/xml/XmlArrayUnnestTest.java | 279 +++++++++ ...uctAggregateEmbeddableInheritanceTest.java | 2 +- .../mapping/basic/ByteArrayMappingTests.java | 4 +- .../basic/CharacterArrayMappingTests.java | 4 +- ...haracterArrayNationalizedMappingTests.java | 4 +- .../NestedStructEmbeddableTest.java | 2 +- .../embeddable/StructEmbeddableArrayTest.java | 2 +- .../embeddable/StructEmbeddableTest.java | 2 +- .../StructWithArrayEmbeddableTest.java | 2 +- .../orm/test/type/BasicListTest.java | 19 +- .../orm/test/type/BasicSortedSetTest.java | 19 +- .../orm/test/type/BooleanArrayTest.java | 22 +- .../orm/test/type/DateArrayTest.java | 20 +- .../orm/test/type/DoubleArrayTest.java | 21 +- .../orm/test/type/EnumArrayTest.java | 18 +- .../orm/test/type/EnumSetConverterTest.java | 21 +- .../hibernate/orm/test/type/EnumSetTest.java | 19 +- .../orm/test/type/FloatArrayTest.java | 20 +- .../orm/test/type/IntegerArrayTest.java | 21 +- .../orm/test/type/LongArrayTest.java | 21 +- .../orm/test/type/ShortArrayTest.java | 21 +- .../orm/test/type/StringArrayTest.java | 20 +- .../orm/test/type/TimeArrayTest.java | 20 +- .../orm/test/type/TimestampArrayTest.java | 20 +- .../orm/junit/DialectFeatureChecks.java | 301 ++++++---- migration-guide.adoc | 22 + release-announcement.adoc | 25 + 225 files changed, 9086 insertions(+), 1030 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonJdbcTypeConstructor.java delete mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbJdbcTypeConstructor.java delete mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java rename hibernate-core/src/main/java/org/hibernate/dialect/{AbstractPostgreSQLJsonArrayPGObjectType.java => PostgreSQLJsonArrayPGObjectType.java} (84%) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/engine/spi/LazySessionWrapperOptions.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionFrom.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionJoin.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionRoot.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSetReturningFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmAssociationPathSourceNew.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSourceNew.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingSetReturningFunctionDescriptor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSetReturningFunctionDescriptor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmSetReturningFunctionDescriptor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmSetReturningFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/function/SetReturningFunctionRenderer.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmSetReturningFunctionDescriptor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/NamedSetReturningFunctionDescriptorBuilder.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/SetReturningFunctionTypeResolver.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/SetReturningFunctionTypeResolverBuilder.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionRoot.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmSetReturningFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFunctionJoin.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/internal/ColumnQualifierCollectorSqlAstWalker.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcTypeConstructor.java rename hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/{JsonArrayAsStringJdbcType.java => JsonAsStringArrayJdbcType.java} (74%) create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonAsStringArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcTypeConstructor.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayUnnestStructTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayUnnestTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayUnnestTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/srf/CustomSetReturningFunctionTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/xml/XmlArrayUnnestTest.java diff --git a/docker_db.sh b/docker_db.sh index 5ba987987e..e24a056e6e 100755 --- a/docker_db.sh +++ b/docker_db.sh @@ -434,6 +434,8 @@ use master go create login $SYBASE_USER with password $SYBASE_PASSWORD go +exec sp_configure 'enable xml', 1 +go exec sp_dboption $SYBASE_DB, 'abort tran on log full', true go exec sp_dboption $SYBASE_DB, 'allow nulls by default', true diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index ddffb6e0cc..793dd5095d 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -8,6 +8,7 @@ :array-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/array :json-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/json :xml-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/xml +:srf-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/srf :extrasdir: extras 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 |=== | Function | Purpose -| `array()` | Creates an array based on the passed arguments -| `array_list()` | Like `array`, but returns the result as `List` -| `array_agg()` | Aggregates row values into an array -| `array_position()` | Determines the position of an element in an array -| `array_positions()` | Determines all positions of an element in an array -| `array_positions_list()` | Like `array_positions`, but returns the result as `List` -| `array_length()` | Determines the length of an array -| `array_concat()` | Concatenates array with each other in order -| `array_prepend()` | Prepends element to array -| `array_append()` | Appends element to array -| `array_contains()` | Whether an array contains an element -| `array_contains_nullable()` | Whether an array contains an element, supporting `null` element -| `array_includes()` | Whether an array contains another array -| `array_includes_nullable()` | Whether an array contains another array, supporting `null` elements -| `array_intersects()` | Whether an array holds at least one element of another array -| `array_intersects_nullable()` | Whether an array holds at least one element of another array, supporting `null` elements -| `array_get()` | Accesses the element of an array by index -| `array_set()` | Creates array copy with given element at given index -| `array_remove()` | Creates array copy with given element removed -| `array_remove_index()` | Creates array copy with the element at the given index removed -| `array_slice()` | Creates a sub-array of the based on lower and upper index -| `array_replace()` | Creates array copy replacing a given element with another -| `array_trim()` | Creates array copy trimming the last _N_ elements -| `array_fill()` | Creates array filled with the same element _N_ times -| `array_fill_list()` | Like `array_fill`, but returns the result as `List` -| `array_to_string()` | String representation of array +| <> | Creates an array based on the passed arguments +| <> | Like `array`, but returns the result as `List` +| <> | Aggregates row values into an array +| <> | Determines the position of an element in an array +| <> | Determines all positions of an element in an array +| <> | Like `array_positions`, but returns the result as `List` +| <> | Determines the length of an array +| <> | Concatenates array with each other in order +| <> | Prepends element to array +| <> | Appends element to array +| <> | Whether an array contains an element +| <> | Whether an array contains an element, supporting `null` element +| <> | Whether an array contains another array +| <> | Whether an array contains another array, supporting `null` elements +| <> | Whether an array holds at least one element of another array +| <> | Whether an array holds at least one element of another array, supporting `null` elements +| <> | Accesses the element of an array by index +| <> | Creates array copy with given element at given index +| <> | Creates array copy with given element removed +| <> | Creates array copy with the element at the given index removed +| <> | Creates a sub-array of the based on lower and upper index +| <> | Creates array copy replacing a given element with another +| <> | Creates array copy trimming the last _N_ elements +| <> | Creates array filled with the same element _N_ times +| <> | Like `array_fill`, but returns the result as `List` +| <> | String representation of array +| <> | Turns an array into rows |=== [[hql-array-constructor-functions]] @@ -1637,6 +1639,32 @@ include::{array-example-dir-hql}/ArrayToStringTest.java[tags=hql-array-to-string ---- ==== +[[hql-array-unnest]] +===== `unnest()` + +A <>, which turns the single array argument into rows. +Returns no rows if the array argument is `null` or an empty array. +The `index()` function can be used to access the 1-based array index of an array element. + +[[hql-array-unnest-struct-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArrayUnnestStructTest.java[tags=hql-array-unnest-aggregate-with-ordinality-example] +---- +==== + +The `lateral` keyword is mandatory if the argument refers to a from node item of the same query level. +Basic plural attributes can also be joined directly, which is syntax sugar for `lateral unnest(...)`. + +[[hql-array-unnest-example]] +==== +[source, java, indent=0] +---- +include::{array-example-dir-hql}/ArrayUnnestTest.java[tags=hql-array-unnest-example] +---- +==== + [[hql-functions-json]] ==== Functions for dealing with JSON @@ -2916,6 +2944,48 @@ The CTE name can be used for a `from` clause root or a `join`, similar to entity Refer to the <> chapter for details about CTEs. +[[hql-from-set-returning-functions]] +==== Set-returning functions in `from` clause + +A set-returning function is a function that produces rows instead of a single scalar value +and is exclusively used in the `from` clause, either as root node or join target. + +The `index()` function can be used to access the 1-based index of a returned row. + +The following set-returning functions are available on many platforms: + +|=== +| Function | purpose + +| <> | Turns an array into rows +//| `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`: + +[[hql-from-set-returning-functions-contributor-example]] +==== +[source, java, indent=0] +---- +include::{srf-example-dir-hql}/CustomSetReturningFunctionTest.java[tags=hql-set-returning-function-contributor-example] +---- +==== + +After that, the function can be used in the `from` clause: + +[[hql-from-set-returning-functions-custom-example]] +==== +[source, java, indent=0] +---- +include::{srf-example-dir-hql}/CustomSetReturningFunctionTest.java[tags=hql-set-returning-function-custom-example] +---- +==== + +NOTE: The `index()` function represents the idea of the `with ordinality` SQL syntax, +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-join]] === Declaring joined entities @@ -3131,6 +3201,17 @@ Most databases support some flavor of `join lateral`, and Hibernate emulates the But emulation is neither very efficient, nor does it support all possible query shapes, so it's important to test on your target database. ==== +[[hql-join-set-returning-function]] +==== Set-returning functions in joins + +A `join` clause may contain a set-returning function, either: + +- an uncorrelated set-returning function, which is almost the same as a <>, except that it may have an `on` restriction, or +- a _lateral join_, which is a correlated set-returning function, and may refer to other roots declared earlier in the same `from` clause. + +The `lateral` keyword just distinguishes the two cases. +A lateral join may be an inner or left outer join, but not a right join, nor a full join. + [[hql-implicit-join]] ==== Implicit association joins (path expressions) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 6f7c766b46..7f1b206645 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -18,6 +18,7 @@ import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.PessimisticLockException; @@ -92,7 +93,6 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -263,11 +263,9 @@ public class CockroachLegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 20 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INET, "inet", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); } else { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); @@ -372,11 +370,11 @@ public class CockroachLegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 20, 0 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getInetJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); - jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonJdbcType( serviceRegistry ) ); - jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) ); } } else { @@ -384,11 +382,11 @@ public class CockroachLegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 20, 0 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSON_INSTANCE ); } } } @@ -398,11 +396,11 @@ public class CockroachLegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 20, 0 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSON_INSTANCE ); } } @@ -424,6 +422,7 @@ public class CockroachLegacyDialect extends Dialect { ) ); + // Replace the standard array constructor jdbcTypeRegistry.addTypeConstructor( PostgreSQLArrayJdbcTypeConstructor.INSTANCE ); } @@ -518,6 +517,8 @@ public class CockroachLegacyDialect extends Dialect { functionFactory.jsonArrayAppend_postgresql( false ); functionFactory.jsonArrayInsert_postgresql(); + functionFactory.unnest_postgresql(); + // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) .setExactArgumentCount( 2 ) @@ -534,6 +535,11 @@ public class CockroachLegacyDialect extends Dialect { functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); } + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "ordinality"; + } + @Override public TimeZoneSupport getTimeZoneSupport() { return TimeZoneSupport.NORMALIZE; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index 0cd3780273..e9787cf033 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -88,6 +88,7 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorDB import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.JavaObjectType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.java.JavaType; @@ -455,6 +456,14 @@ public class DB2LegacyDialect extends Dialect { functionFactory.xmlexists_db2_legacy(); } functionFactory.xmlagg(); + + functionFactory.unnest_emulated(); + } + + @Override + public int getPreferredSqlTypeCodeForArray() { + // Even if DB2 11 supports JSON functions, it's not possible to unnest a JSON array to rows, so stick to XML + return SqlTypes.XML_ARRAY; } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java index 07687aa07f..4e68ab5bfd 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java @@ -10,8 +10,11 @@ import java.util.function.Consumer; import org.hibernate.LockMode; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.Clause; @@ -19,6 +22,7 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; @@ -28,6 +32,8 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -253,6 +259,34 @@ public class DB2LegacySqlAstTranslator extends Abstract inLateral = oldLateral; } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "lateral (select t.*, row_number() over() " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from table(" ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( ") t)" ); + } + else { + appendSql( "table(" ); + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + append( ')' ); + } + } + @Override public void visitSelectStatement(SelectStatement statement) { if ( getQueryPartForRowNumbering() == statement.getQueryPart() && inLateral ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java index 83240b738a..54f7b33867 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacySqlAstTranslator.java @@ -4,17 +4,13 @@ */ package org.hibernate.community.dialect; -import org.hibernate.LockMode; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; -import org.hibernate.sql.ast.tree.from.FunctionTableReference; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -58,28 +54,10 @@ public class DB2zLegacySqlAstTranslator extends DB2Lega } @Override - protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { - if ( shouldInlineCte( tableGroup ) ) { - inlineCteTableGroup( tableGroup, lockMode ); - return false; - } - final TableReference tableReference = tableGroup.getPrimaryTableReference(); - if ( tableReference instanceof NamedTableReference ) { - return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); - } + public void visitQueryPartTableReference(QueryPartTableReference tableReference) { // DB2 z/OS we need the "table" qualifier for table valued functions or lateral sub-queries append( "table " ); - tableReference.accept( this ); - return false; - } - - @Override - public void visitFunctionTableReference(FunctionTableReference tableReference) { - // For the table qualifier we need parenthesis on DB2 z/OS - append( OPEN_PARENTHESIS ); - tableReference.getFunctionExpression().accept( this ); - append( CLOSE_PARENTHESIS ); - renderDerivedTableReference( tableReference ); + super.visitQueryPartTableReference( tableReference ); } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index e00ee7e4d7..4f418e13b4 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -14,6 +14,7 @@ import java.util.Date; import java.util.List; import java.util.TimeZone; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.PessimisticLockException; import org.hibernate.QueryTimeoutException; import org.hibernate.boot.model.FunctionContributions; @@ -97,7 +98,6 @@ import static org.hibernate.type.SqlTypes.FLOAT; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTERVAL_SECOND; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -265,7 +265,6 @@ public class H2LegacyDialect extends Dialect { } if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } } ddlTypeRegistry.addDescriptor( new NativeEnumDdlTypeImpl( this ) ); @@ -296,7 +295,8 @@ public class H2LegacyDialect extends Dialect { } if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonArrayJdbcType.INSTANCE ); + // Replace the standard array constructor + jdbcTypeRegistry.addTypeConstructor( H2JsonArrayJdbcTypeConstructor.INSTANCE ); } jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); @@ -427,6 +427,8 @@ public class H2LegacyDialect extends Dialect { else { functionFactory.listagg_groupConcat(); } + + functionFactory.unnest_h2( getMaximumArraySize() ); } /** @@ -439,6 +441,11 @@ public class H2LegacyDialect extends Dialect { return 1000; } + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "nord"; + } + @Override public void augmentPhysicalTableTypes(List tableTypesList) { if ( getVersion().isSameOrAfter( 2 ) ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java index 780a5c73e0..3dba675015 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacyDialect.java @@ -488,18 +488,28 @@ public class HANALegacyDialect extends Dialect { typeConfiguration ); - if ( getVersion().isSameOrAfter(2, 0, 20) ) { - // Introduced in 2.0 SPS 02 + if ( getVersion().isSameOrAfter( 2, 0 ) ) { + // Introduced in 2.0 SPS 00 functionFactory.jsonValue_no_passing(); functionFactory.jsonQuery_no_passing(); functionFactory.jsonExists_hana(); - if ( getVersion().isSameOrAfter(2, 0, 40) ) { - // Introduced in 2.0 SPS 04 - functionFactory.jsonObject_hana(); - functionFactory.jsonArray_hana(); - functionFactory.jsonArrayAgg_hana(); - functionFactory.jsonObjectAgg_hana(); + + functionFactory.unnest_hana(); +// functionFactory.json_table(); + + if ( getVersion().isSameOrAfter(2, 0, 20 ) ) { + if ( getVersion().isSameOrAfter( 2, 0, 40 ) ) { + // Introduced in 2.0 SPS 04 + functionFactory.jsonObject_hana(); + functionFactory.jsonArray_hana(); + functionFactory.jsonArrayAgg_hana(); + functionFactory.jsonObjectAgg_hana(); + } + +// functionFactory.xmltable(); } + +// functionFactory.xmlextract(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacySqlAstTranslator.java index 531a6ff8dc..025bf8f4e8 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HANALegacySqlAstTranslator.java @@ -9,17 +9,22 @@ import java.util.List; import org.hibernate.MappingException; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; @@ -34,6 +39,8 @@ import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.internal.TableInsertStandard; +import static org.hibernate.dialect.SybaseASESqlAstTranslator.isLob; + /** * An SQL AST translator for the Legacy HANA dialect. */ @@ -192,15 +199,35 @@ public class HANALegacySqlAstTranslator extends Abstrac } @Override - protected SqlAstNodeRenderingMode getParameterRenderingMode() { - // HANA does not support parameters in lateral subqueries for some reason, so inline all the parameters in this case - return inLateral ? SqlAstNodeRenderingMode.INLINE_ALL_PARAMETERS : super.getParameterRenderingMode(); + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } } @Override - public void visitFunctionTableReference(FunctionTableReference tableReference) { - tableReference.getFunctionExpression().accept( this ); - renderTableReferenceIdentificationVariable( tableReference ); + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "(select t.*, row_number() over() " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from " ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( " t)" ); + } + else { + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + } + } + + @Override + protected SqlAstNodeRenderingMode getParameterRenderingMode() { + // HANA does not support parameters in lateral subqueries for some reason, so inline all the parameters in this case + return inLateral ? SqlAstNodeRenderingMode.INLINE_ALL_PARAMETERS : super.getParameterRenderingMode(); } @Override @@ -212,7 +239,38 @@ public class HANALegacySqlAstTranslator extends Abstrac @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + // In SAP HANA, LOBs are not "comparable", so we have to use a like predicate for comparison + final boolean isLob = isLob( lhs.getExpressionType() ); if ( operator == ComparisonOperator.DISTINCT_FROM || operator == ComparisonOperator.NOT_DISTINCT_FROM ) { + if ( isLob ) { + switch ( operator ) { + case DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=1" ); + return; + case NOT_DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=0" ); + return; + default: + // Fall through + break; + } + } // HANA does not support plain parameters in the select clause of the intersect emulation withParameterRenderingMode( SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER, @@ -220,7 +278,24 @@ public class HANALegacySqlAstTranslator extends Abstrac ); } else { - renderComparisonEmulateIntersect( lhs, operator, rhs ); + if ( isLob ) { + switch ( operator ) { + case EQUAL: + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + return; + case NOT_EQUAL: + lhs.accept( this ); + appendSql( " not like " ); + rhs.accept( this ); + return; + default: + // Fall through + break; + } + } + renderComparisonStandard( lhs, operator, rhs ); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index 902ea452f4..ebd56ea696 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -9,6 +9,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.sql.Types; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.JDBCException; import org.hibernate.LockMode; import org.hibernate.StaleObjectStateException; @@ -277,6 +278,8 @@ public class HSQLLegacyDialect extends Dialect { functionFactory.jsonObjectAgg_h2(); } + functionFactory.unnest( "c1", "c2" ); + //trim() requires parameters to be cast when used as trim character functionContributions.getFunctionRegistry().register( "trim", new TrimFunction( this, @@ -285,6 +288,11 @@ public class HSQLLegacyDialect extends Dialect { ) ); } + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "c2"; + } + @Override public String currentTime() { return "localtime"; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java index 385555241a..2e8d776cd8 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java @@ -22,6 +22,8 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; @@ -72,6 +74,17 @@ public class HSQLLegacySqlAstTranslator extends Abstrac } } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + @Override protected void visitConflictClause(ConflictClause conflictClause) { if ( conflictClause != null ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java index 34ea646116..4cc8d2460b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java @@ -29,7 +29,7 @@ import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcTypeConstructor; import org.hibernate.type.descriptor.jdbc.JsonJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -96,7 +96,12 @@ public class MariaDBLegacyDialect extends MySQLLegacyDialect { commonFunctionFactory.jsonArrayAgg_mariadb(); commonFunctionFactory.jsonObjectAgg_mariadb(); commonFunctionFactory.jsonArrayAppend_mariadb(); + if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { + if ( getVersion().isSameOrAfter( 10, 6 ) ) { + commonFunctionFactory.unnest_emulated(); + } + commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) .setInvariantType( functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) ) @@ -145,7 +150,7 @@ public class MariaDBLegacyDialect extends MySQLLegacyDialect { final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); // Make sure we register the JSON type descriptor before calling super, because MariaDB does not need casting jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, JsonJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, JsonArrayJdbcType.INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( JsonArrayJdbcTypeConstructor.INSTANCE ); super.contributeTypes( typeContributions, serviceRegistry ); if ( getVersion().isSameOrAfter( 10, 7 ) ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java index ec288bf0f7..fb021d624f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java @@ -22,6 +22,7 @@ import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.insert.ConflictClause; @@ -277,6 +278,11 @@ public class MariaDBLegacySqlAstTranslator extends Abst emulateQueryPartTableReferenceColumnAliasing( tableReference ); } + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { + renderTableReferenceIdentificationVariable( tableReference ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { if ( !isRowNumberingCurrentQueryPart() ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 0d679e7e25..c8b5c7536b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -537,6 +537,11 @@ public class MySQLLegacyDialect extends Dialect { return Types.BIT; } + @Override + public int getPreferredSqlTypeCodeForArray() { + return getMySQLVersion().isSameOrAfter( 5, 7 ) ? SqlTypes.JSON_ARRAY : super.getPreferredSqlTypeCodeForArray(); + } + // @Override // public int getDefaultDecimalPrecision() { // //this is the maximum, but I guess it's too high @@ -667,6 +672,10 @@ public class MySQLLegacyDialect extends Dialect { functionFactory.jsonMergepatch_mysql(); functionFactory.jsonArrayAppend_mysql(); functionFactory.jsonArrayInsert_mysql(); + + if ( getMySQLVersion().isSameOrAfter( 8 ) ) { + functionFactory.unnest_emulated(); + } } } @@ -678,7 +687,7 @@ public class MySQLLegacyDialect extends Dialect { if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, MySQLCastingJsonJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, MySQLCastingJsonArrayJdbcType.INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( MySQLCastingJsonArrayJdbcTypeConstructor.INSTANCE ); } // MySQL requires a custom binder for binding untyped nulls with the NULL type diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java index 8d9d6a65d3..9d143da50a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java @@ -23,6 +23,8 @@ import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; @@ -269,6 +271,27 @@ public class MySQLLegacySqlAstTranslator extends Abstra } } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { + if ( getDialect().getVersion().isSameOrAfter( 8 ) ) { + super.renderDerivedTableReferenceIdentificationVariable( tableReference ); + } + else { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { if ( !isRowNumberingCurrentQueryPart() ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 4c9dc49918..b8b9772f96 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -91,7 +91,6 @@ import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.NullJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType; -import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType; import org.hibernate.type.descriptor.jdbc.SqlTypedJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; @@ -123,7 +122,6 @@ import static org.hibernate.type.SqlTypes.FLOAT; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.REAL; @@ -335,6 +333,8 @@ public class OracleLegacyDialect extends Dialect { functionFactory.xmlquery_oracle(); functionFactory.xmlexists(); functionFactory.xmlagg(); + + functionFactory.unnest_oracle(); } @Override @@ -745,11 +745,9 @@ public class OracleLegacyDialect extends Dialect { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "MDSYS.SDO_GEOMETRY", this ) ); if ( getVersion().isSameOrAfter( 21 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } else if ( getVersion().isSameOrAfter( 12 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "blob", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "blob", this ) ); } } @@ -919,11 +917,11 @@ public class OracleLegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 21 ) ) { typeContributions.contributeJdbcType( OracleJsonJdbcType.INSTANCE ); - typeContributions.contributeJdbcType( OracleJsonArrayJdbcType.INSTANCE ); + typeContributions.contributeJdbcTypeConstructor( OracleJsonArrayJdbcTypeConstructor.NATIVE_INSTANCE ); } else { typeContributions.contributeJdbcType( OracleJsonBlobJdbcType.INSTANCE ); - typeContributions.contributeJdbcType( OracleJsonArrayBlobJdbcType.INSTANCE ); + typeContributions.contributeJdbcTypeConstructor( OracleJsonArrayJdbcTypeConstructor.BLOB_INSTANCE ); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java index ac4442f508..5af48c85d5 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java @@ -10,18 +10,23 @@ import java.util.List; import org.hibernate.dialect.OracleArrayJdbcType; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.FrameExclusion; import org.hibernate.query.sqm.FrameKind; import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; @@ -33,6 +38,7 @@ import org.hibernate.sql.ast.tree.expression.Over; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.FromClause; import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; @@ -298,9 +304,42 @@ public class OracleLegacySqlAstTranslator extends Abstr @Override public void visitFunctionTableReference(FunctionTableReference tableReference) { - append( "table(" ); tableReference.getFunctionExpression().accept( this ); - append( CLOSE_PARENTHESIS ); + if ( !tableReference.rendersIdentifierVariable() ) { + renderDerivedTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "lateral (select t.*, rownum " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from table(" ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( ") t)" ); + } + else { + appendSql( "table(" ); + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + append( ')' ); + } + } + + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { renderTableReferenceIdentificationVariable( tableReference ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 00e183038d..655bd51af6 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.TimeZone; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Length; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -109,7 +110,6 @@ import static org.hibernate.type.SqlTypes.GEOGRAPHY; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -259,11 +259,9 @@ public class PostgreSQLLegacyDialect extends Dialect { // Prefer jsonb if possible if ( getVersion().isSameOrAfter( 9, 4 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); } else { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } } ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); @@ -706,6 +704,18 @@ public class PostgreSQLLegacyDialect extends Dialect { ); functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); functionFactory.dateTrunc(); + + if ( getVersion().isSameOrAfter( 17 ) ) { + functionFactory.unnest( null, "ordinality" ); + } + else { + functionFactory.unnest_postgresql(); + } + } + + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "ordinality"; } /** @@ -1460,21 +1470,21 @@ public class PostgreSQLLegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 9, 4 ) ) { if ( PgJdbcHelper.isUsable( serviceRegistry ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); - jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } } else { if ( PgJdbcHelper.isUsable( serviceRegistry ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonJdbcType( serviceRegistry ) ); - jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSON_INSTANCE ); } } } @@ -1490,11 +1500,11 @@ public class PostgreSQLLegacyDialect extends Dialect { if ( getVersion().isSameOrAfter( 9, 2 ) ) { if ( getVersion().isSameOrAfter( 9, 4 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSON_INSTANCE ); } } } @@ -1513,6 +1523,7 @@ public class PostgreSQLLegacyDialect extends Dialect { ) ); + // Replace the standard array constructor jdbcTypeRegistry.addTypeConstructor( PostgreSQLArrayJdbcTypeConstructor.INSTANCE ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index d95e8e468d..16ab474fb8 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -16,6 +16,8 @@ import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.Replacer; +import org.hibernate.dialect.SQLServerCastingXmlArrayJdbcTypeConstructor; +import org.hibernate.dialect.SQLServerCastingXmlJdbcType; import org.hibernate.dialect.TimeZoneSupport; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.CountFunction; @@ -74,7 +76,6 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType; import org.hibernate.type.descriptor.jdbc.TinyIntAsSmallIntJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; -import org.hibernate.type.descriptor.jdbc.XmlJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; @@ -244,6 +245,11 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( UUID, "uniqueidentifier", this ) ); } + @Override + public int getPreferredSqlTypeCodeForArray() { + return XML_ARRAY; + } + @Override public JdbcType resolveSqlTypeDescriptor( String columnTypeName, @@ -308,8 +314,9 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { Types.TINYINT, TinyIntAsSmallIntJdbcType.INSTANCE ); - typeContributions.contributeJdbcType( XmlJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( SQLServerCastingXmlJdbcType.INSTANCE ); typeContributions.contributeJdbcType( UUIDJdbcType.INSTANCE ); + typeContributions.contributeJdbcTypeConstructor( SQLServerCastingXmlArrayJdbcTypeConstructor.INSTANCE ); } @Override @@ -421,6 +428,9 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { functionFactory.xmlquery_sqlserver(); functionFactory.xmlexists_sqlserver(); functionFactory.xmlagg_sqlserver(); + + functionFactory.unnest_sqlserver(); + if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java index 7c2ba4ac75..37b7999a16 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacySqlAstTranslator.java @@ -11,14 +11,19 @@ import org.hibernate.LockOptions; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; @@ -26,10 +31,9 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; -import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.UnionTableReference; import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; @@ -180,17 +184,24 @@ public class SQLServerLegacySqlAstTranslator extends Ab } } - protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { - if ( shouldInlineCte( tableGroup ) ) { - inlineCteTableGroup( tableGroup, lockMode ); - return false; - } - final TableReference tableReference = tableGroup.getPrimaryTableReference(); - if ( tableReference instanceof NamedTableReference ) { - return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); - } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { tableReference.accept( this ); - return false; + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "(select t.*, row_number() over(order by (select 1)) " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from " ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( " t)" ); + } + else { + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + } } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java index 2118bfd1f3..d56db0a8e1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java @@ -12,10 +12,12 @@ import java.sql.Types; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.QueryTimeoutException; +import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.SybaseDriverKind; +import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.TopLimitHandler; import org.hibernate.engine.jdbc.Size; @@ -52,6 +54,7 @@ import static org.hibernate.type.SqlTypes.DATE; import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.XML_ARRAY; /** * A {@linkplain Dialect SQL dialect} for Sybase Adaptive Server Enterprise 11.9 and above. @@ -140,6 +143,11 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { } } + @Override + public int getPreferredSqlTypeCodeForArray() { + return XML_ARRAY; + } + @Override public int getMaxVarcharLength() { // the maximum length of a VARCHAR or VARBINARY @@ -151,6 +159,15 @@ public class SybaseASELegacyDialect extends SybaseLegacyDialect { return 16_384; } + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry( functionContributions ); + + CommonFunctionFactory functionFactory = new CommonFunctionFactory( functionContributions); + + functionFactory.unnest_sybasease(); + } + private static boolean isAnsiNull(DatabaseMetaData databaseMetaData) { if ( databaseMetaData != null ) { try (java.sql.Statement s = databaseMetaData.getConnection().createStatement() ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java index b6f594e417..3dcf6a6eda 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java @@ -48,6 +48,8 @@ import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; +import static org.hibernate.dialect.SybaseASESqlAstTranslator.isLob; + /** * A SQL AST translator for Sybase ASE. * @@ -336,7 +338,7 @@ public class SybaseASELegacySqlAstTranslator extends Ab append( '(' ); visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); append( ')' ); - renderDerivedTableReference( tableReference ); + renderDerivedTableReferenceIdentificationVariable( tableReference ); } @Override @@ -371,8 +373,56 @@ public class SybaseASELegacySqlAstTranslator extends Ab @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + // In Sybase ASE, XMLTYPE is not "comparable", so we have to cast the two parts to varchar for this purpose + final boolean isLob = isLob( lhs.getExpressionType() ); + if ( isLob ) { + switch ( operator ) { + case EQUAL: + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + return; + case NOT_EQUAL: + lhs.accept( this ); + appendSql( " not like " ); + rhs.accept( this ); + return; + default: + // Fall through + break; + } + } // I think intersect is only supported in 16.0 SP3 if ( getDialect().isAnsiNullOn() ) { + if ( isLob ) { + switch ( operator ) { + case DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=1" ); + return; + case NOT_DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=0" ); + return; + default: + // Fall through + break; + } + } if ( supportsDistinctFromPredicate() ) { renderComparisonEmulateIntersect( lhs, operator, rhs ); } @@ -393,10 +443,20 @@ public class SybaseASELegacySqlAstTranslator extends Ab lhs.accept( this ); switch ( operator ) { case DISTINCT_FROM: - appendSql( "<>" ); + if ( isLob ) { + appendSql( " not like " ); + } + else { + appendSql( "<>" ); + } break; case NOT_DISTINCT_FROM: - appendSql( '=' ); + if ( isLob ) { + appendSql( " like " ); + } + else { + appendSql( '=' ); + } break; case LESS_THAN: case GREATER_THAN: diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java index dfc96868a3..0bf55746fb 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseLegacySqlAstTranslator.java @@ -204,7 +204,7 @@ public class SybaseLegacySqlAstTranslator extends Abstr append( '(' ); visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); append( ')' ); - renderDerivedTableReference( tableReference ); + renderDerivedTableReferenceIdentificationVariable( tableReference ); } @Override diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index a4e867ed2c..77ea591d91 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -225,6 +225,7 @@ entityWithJoins fromRoot : entityName variable? # RootEntity | LEFT_PAREN subquery RIGHT_PAREN variable? # RootSubquery + | setReturningFunction variable? # RootFunction ; /** @@ -275,8 +276,9 @@ joinType * The joined path, with an optional identification variable */ joinTarget - : path variable? #JoinPath - | LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable? #JoinSubquery + : path variable? # JoinPath + | LATERAL? LEFT_PAREN subquery RIGHT_PAREN variable? # JoinSubquery + | LATERAL? setReturningFunction variable? # JoinFunction ; /** @@ -1114,6 +1116,17 @@ function | genericFunction ; +setReturningFunction + : simpleSetReturningFunction + ; + +/** + * A simple set returning function invocation without special syntax. + */ +simpleSetReturningFunction + : identifier LEFT_PAREN genericFunctionArguments? RIGHT_PAREN + ; + /** * A syntax for calling user-defined or native database functions, required by JPQL */ diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java index 9cd882fea2..80e8db5f04 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java @@ -85,8 +85,11 @@ import org.hibernate.type.descriptor.java.ByteArrayJavaType; import org.hibernate.type.descriptor.java.CharacterArrayJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.JsonArrayAsStringJdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcTypeConstructor; +import org.hibernate.type.descriptor.jdbc.JsonAsStringArrayJdbcTypeConstructor; import org.hibernate.type.descriptor.jdbc.JsonAsStringJdbcType; +import org.hibernate.type.descriptor.jdbc.XmlArrayJdbcTypeConstructor; +import org.hibernate.type.descriptor.jdbc.XmlAsStringArrayJdbcTypeConstructor; import org.hibernate.type.descriptor.jdbc.XmlAsStringJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.DdlType; @@ -103,7 +106,6 @@ import org.jboss.jandex.Indexer; import jakarta.persistence.AttributeConverter; import static org.hibernate.internal.util.collections.CollectionHelper.mutableJoin; -import static org.hibernate.internal.util.config.ConfigurationHelper.getPreferredSqlTypeCodeForArray; import static org.hibernate.internal.util.config.ConfigurationHelper.getPreferredSqlTypeCodeForDuration; import static org.hibernate.internal.util.config.ConfigurationHelper.getPreferredSqlTypeCodeForInstant; import static org.hibernate.internal.util.config.ConfigurationHelper.getPreferredSqlTypeCodeForUuid; @@ -763,9 +765,6 @@ public class MetadataBuildingProcess { // add Dialect contributed types final Dialect dialect = options.getServiceRegistry().requireService( JdbcServices.class ).getDialect(); dialect.contribute( typeContributions, options.getServiceRegistry() ); - // Capture the dialect configured JdbcTypes so that we can detect if a TypeContributor overwrote them, - // which has precedence over the fallback and preferred type registrations - final JdbcType dialectArrayDescriptor = jdbcTypeRegistry.findDescriptor( SqlTypes.ARRAY ); // add TypeContributor contributed types. for ( TypeContributor contributor : classLoaderService.loadJavaServices( TypeContributor.class ) ) { @@ -790,17 +789,23 @@ public class MetadataBuildingProcess { addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.UUID, SqlTypes.BINARY ); } - final int preferredSqlTypeCodeForArray = getPreferredSqlTypeCodeForArray( serviceRegistry ); - if ( preferredSqlTypeCodeForArray != SqlTypes.ARRAY ) { - adaptToPreferredSqlTypeCode( - jdbcTypeRegistry, - dialectArrayDescriptor, - SqlTypes.ARRAY, - preferredSqlTypeCodeForArray - ); + jdbcTypeRegistry.addDescriptorIfAbsent( JsonAsStringJdbcType.VARCHAR_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( XmlAsStringJdbcType.VARCHAR_INSTANCE ); + if ( jdbcTypeRegistry.getConstructor( SqlTypes.JSON_ARRAY ) == null ) { + if ( jdbcTypeRegistry.getDescriptor( SqlTypes.JSON ).getDdlTypeCode() == SqlTypes.JSON ) { + jdbcTypeRegistry.addTypeConstructor( JsonArrayJdbcTypeConstructor.INSTANCE ); + } + else { + jdbcTypeRegistry.addTypeConstructor( JsonAsStringArrayJdbcTypeConstructor.INSTANCE ); + } } - else { - addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.ARRAY, SqlTypes.VARBINARY ); + if ( jdbcTypeRegistry.getConstructor( SqlTypes.XML_ARRAY ) == null ) { + if ( jdbcTypeRegistry.getDescriptor( SqlTypes.SQLXML ).getDdlTypeCode() == SqlTypes.SQLXML ) { + jdbcTypeRegistry.addTypeConstructor( XmlArrayJdbcTypeConstructor.INSTANCE ); + } + else { + jdbcTypeRegistry.addTypeConstructor( XmlAsStringArrayJdbcTypeConstructor.INSTANCE ); + } } final int preferredSqlTypeCodeForDuration = getPreferredSqlTypeCodeForDuration( serviceRegistry ); @@ -823,10 +828,6 @@ public class MetadataBuildingProcess { addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.POINT, SqlTypes.VARBINARY ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.GEOGRAPHY, SqlTypes.GEOMETRY ); - jdbcTypeRegistry.addDescriptorIfAbsent( JsonAsStringJdbcType.VARCHAR_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( JsonArrayAsStringJdbcType.VARCHAR_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( XmlAsStringJdbcType.VARCHAR_INSTANCE ); - addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.MATERIALIZED_BLOB, SqlTypes.BLOB ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.MATERIALIZED_CLOB, SqlTypes.CLOB ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.MATERIALIZED_NCLOB, SqlTypes.NCLOB ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index cc5e836d89..fef9b37c56 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -19,6 +19,7 @@ import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.PessimisticLockException; @@ -95,7 +96,6 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -260,7 +260,6 @@ public class CockroachDialect extends Dialect { // Prefer jsonb if possible ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INET, "inet", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); @@ -356,13 +355,13 @@ public class CockroachDialect extends Dialect { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getIntervalJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getInetJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); - jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } } else { @@ -370,7 +369,7 @@ public class CockroachDialect extends Dialect { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } // Force Blob binding to byte[] for CockroachDB @@ -391,6 +390,7 @@ public class CockroachDialect extends Dialect { ) ); + // Replace the standard array constructor jdbcTypeRegistry.addTypeConstructor( PostgreSQLArrayJdbcTypeConstructor.INSTANCE ); } @@ -485,6 +485,8 @@ public class CockroachDialect extends Dialect { functionFactory.jsonArrayAppend_postgresql( false ); functionFactory.jsonArrayInsert_postgresql(); + functionFactory.unnest_postgresql(); + // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) .setExactArgumentCount( 2 ) @@ -498,6 +500,11 @@ public class CockroachDialect extends Dialect { functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); } + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "ordinality"; + } + @Override public TimeZoneSupport getTimeZoneSupport() { return TimeZoneSupport.NORMALIZE; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index 1cab2faf38..e8e01dedf1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -78,6 +78,7 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorDB import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.JavaObjectType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.java.JavaType; @@ -440,6 +441,14 @@ public class DB2Dialect extends Dialect { functionFactory.xmlexists_db2_legacy(); } functionFactory.xmlagg(); + + functionFactory.unnest_emulated(); + } + + @Override + public int getPreferredSqlTypeCodeForArray() { + // Even if DB2 11 supports JSON functions, it's not possible to unnest a JSON array to rows, so stick to XML + return SqlTypes.XML_ARRAY; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java index d3d328ab7b..7159024ee8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java @@ -9,8 +9,11 @@ import java.util.function.Consumer; import org.hibernate.LockMode; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.Clause; @@ -18,6 +21,7 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; @@ -26,6 +30,8 @@ import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -252,6 +258,34 @@ public class DB2SqlAstTranslator extends AbstractSqlAst inLateral = oldLateral; } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "lateral (select t.*, row_number() over() " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from table(" ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( ") t)" ); + } + else { + appendSql( "table(" ); + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + append( ')' ); + } + } + @Override public void visitSelectStatement(SelectStatement statement) { if ( getQueryPartForRowNumbering() == statement.getQueryPart() && inLateral ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2StructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2StructJdbcType.java index 230654a15c..868f3fe451 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2StructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2StructJdbcType.java @@ -63,6 +63,11 @@ public class DB2StructJdbcType implements StructJdbcType { return SqlTypes.SQLXML; } + @Override + public int getDdlTypeCode() { + return SqlTypes.SQLXML; + } + @Override public int getDefaultSqlTypeCode() { return SqlTypes.STRUCT; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java index 85a2a32810..d037834215 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java @@ -4,15 +4,11 @@ */ package org.hibernate.dialect; -import org.hibernate.LockMode; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.from.FunctionTableReference; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -51,28 +47,10 @@ public class DB2zSqlAstTranslator extends DB2SqlAstTran } @Override - protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { - if ( shouldInlineCte( tableGroup ) ) { - inlineCteTableGroup( tableGroup, lockMode ); - return false; - } - final TableReference tableReference = tableGroup.getPrimaryTableReference(); - if ( tableReference instanceof NamedTableReference ) { - return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); - } + public void visitQueryPartTableReference(QueryPartTableReference tableReference) { // DB2 z/OS we need the "table" qualifier for table valued functions or lateral sub-queries append( "table " ); - tableReference.accept( this ); - return false; - } - - @Override - public void visitFunctionTableReference(FunctionTableReference tableReference) { - // For the table qualifier we need parenthesis on DB2 z/OS - append( OPEN_PARENTHESIS ); - tableReference.getFunctionExpression().accept( this ); - append( CLOSE_PARENTHESIS ); - renderDerivedTableReference( tableReference ); + super.visitQueryPartTableReference( tableReference ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index af7fb977d3..b484247fc0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -1871,7 +1871,7 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun } if ( supportsStandardArrays() ) { - jdbcTypeRegistry.addTypeConstructor( ArrayJdbcTypeConstructor.INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( ArrayJdbcTypeConstructor.INSTANCE ); } if ( supportsMaterializedLobAccess() ) { jdbcTypeRegistry.addDescriptor( SqlTypes.MATERIALIZED_BLOB, BlobJdbcType.MATERIALIZED ); @@ -5290,6 +5290,14 @@ public abstract class Dialect implements ConversionContext, TypeContributor, Fun return null; } + /** + * Returns the default name of the ordinality column for a set-returning function + * if it supports that, otherwise returns {@code null}. + */ + public @Nullable String getDefaultOrdinalityColumnName() { + return null; + } + /** * Pluggable strategy for determining the {@link Size} to use for * columns of a given SQL type. diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index cf7ffe2eb8..7de1b3d6ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -14,6 +14,7 @@ import java.util.Date; import java.util.List; import java.util.TimeZone; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.PessimisticLockException; import org.hibernate.QueryTimeoutException; import org.hibernate.boot.model.FunctionContributions; @@ -89,7 +90,6 @@ import static org.hibernate.type.SqlTypes.FLOAT; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTERVAL_SECOND; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -229,7 +229,6 @@ public class H2Dialect extends Dialect { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "geometry", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INTERVAL_SECOND, "interval second($p,$s)", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); ddlTypeRegistry.addDescriptor( new NativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NativeOrdinalEnumDdlTypeImpl( this ) ); } @@ -246,7 +245,8 @@ public class H2Dialect extends Dialect { jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonArrayJdbcType.INSTANCE ); + // Replace the standard array constructor + jdbcTypeRegistry.addTypeConstructor( H2JsonArrayJdbcTypeConstructor.INSTANCE ); jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); } @@ -358,6 +358,8 @@ public class H2Dialect extends Dialect { functionFactory.xmlforest_h2(); functionFactory.xmlconcat_h2(); functionFactory.xmlpi_h2(); + + functionFactory.unnest_h2( getMaximumArraySize() ); } /** @@ -370,6 +372,11 @@ public class H2Dialect extends Dialect { return 1000; } + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "nord"; + } + @Override public void augmentPhysicalTableTypes(List tableTypesList) { tableTypesList.add( "BASE TABLE" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java index 8406dfbc60..8fa9a50b6b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java @@ -13,18 +13,16 @@ import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; /** * H2 requires binding JSON via {@code setBytes} methods. */ public class H2JsonArrayJdbcType extends JsonArrayJdbcType { - /** - * Singleton access - */ - public static final H2JsonArrayJdbcType INSTANCE = new H2JsonArrayJdbcType(); - protected H2JsonArrayJdbcType() { + public H2JsonArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..c51c475240 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcTypeConstructor.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link H2JsonArrayJdbcType}. + */ +public class H2JsonArrayJdbcTypeConstructor implements JdbcTypeConstructor { + + public static final H2JsonArrayJdbcTypeConstructor INSTANCE = new H2JsonArrayJdbcTypeConstructor(); + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new H2JsonArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java index 80a329ed6d..d78d98c926 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -66,6 +66,7 @@ import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorHA import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.internal.StandardTableExporter; import org.hibernate.tool.schema.spi.Exporter; +import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; @@ -413,6 +414,12 @@ public class HANADialect extends Dialect { return 5000; } + @Override + public int getPreferredSqlTypeCodeForArray() { + // Prefer XML since JSON was only added later + return getVersion().isSameOrAfter( 2 ) ? SqlTypes.XML_ARRAY : super.getPreferredSqlTypeCodeForArray(); + } + @Override public void initializeFunctionRegistry(FunctionContributions functionContributions) { super.initializeFunctionRegistry(functionContributions); @@ -490,15 +497,23 @@ public class HANADialect extends Dialect { typeConfiguration ); - // Introduced in 2.0 SPS 02 - functionFactory.jsonValue_no_passing(); - functionFactory.jsonQuery_no_passing(); - functionFactory.jsonExists_hana(); - // Introduced in 2.0 SPS 04 - functionFactory.jsonObject_hana(); - functionFactory.jsonArray_hana(); - functionFactory.jsonArrayAgg_hana(); - functionFactory.jsonObjectAgg_hana(); + // Introduced in 2.0 SPS 00 + functionFactory.jsonValue_no_passing(); + functionFactory.jsonQuery_no_passing(); + functionFactory.jsonExists_hana(); + + functionFactory.unnest_hana(); +// functionFactory.json_table(); + + // Introduced in 2.0 SPS 04 + functionFactory.jsonObject_hana(); + functionFactory.jsonArray_hana(); + functionFactory.jsonArrayAgg_hana(); + functionFactory.jsonObjectAgg_hana(); + +// functionFactory.xmltable(); + +// functionFactory.xmlextract(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java index a75f053baf..5a935a2513 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java @@ -9,17 +9,22 @@ import java.util.List; import org.hibernate.MappingException; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; @@ -34,6 +39,8 @@ import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.model.internal.TableInsertStandard; +import static org.hibernate.dialect.SybaseASESqlAstTranslator.isLob; + /** * An SQL AST translator for HANA. * @@ -193,6 +200,32 @@ public class HANASqlAstTranslator extends AbstractSqlAs } } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "(select t.*, row_number() over() " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from " ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( " t)" ); + } + else { + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + } + } + @Override protected SqlAstNodeRenderingMode getParameterRenderingMode() { // HANA does not support parameters in lateral subqueries for some reason, so inline all the parameters in this case @@ -200,8 +233,7 @@ public class HANASqlAstTranslator extends AbstractSqlAs } @Override - public void visitFunctionTableReference(FunctionTableReference tableReference) { - tableReference.getFunctionExpression().accept( this ); + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { renderTableReferenceIdentificationVariable( tableReference ); } @@ -214,7 +246,38 @@ public class HANASqlAstTranslator extends AbstractSqlAs @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + // In SAP HANA, LOBs are not "comparable", so we have to use a like predicate for comparison + final boolean isLob = isLob( lhs.getExpressionType() ); if ( operator == ComparisonOperator.DISTINCT_FROM || operator == ComparisonOperator.NOT_DISTINCT_FROM ) { + if ( isLob ) { + switch ( operator ) { + case DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=1" ); + return; + case NOT_DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=0" ); + return; + default: + // Fall through + break; + } + } // HANA does not support plain parameters in the select clause of the intersect emulation withParameterRenderingMode( SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER, @@ -222,7 +285,24 @@ public class HANASqlAstTranslator extends AbstractSqlAs ); } else { - renderComparisonEmulateIntersect( lhs, operator, rhs ); + if ( isLob ) { + switch ( operator ) { + case EQUAL: + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + return; + case NOT_EQUAL: + lhs.accept( this ); + appendSql( " not like " ); + rhs.accept( this ); + return; + default: + // Fall through + break; + } + } + renderComparisonStandard( lhs, operator, rhs ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index fe5ae3fa3b..93dddfb8b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -8,6 +8,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.sql.Types; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.TrimFunction; @@ -212,6 +213,8 @@ public class HSQLDialect extends Dialect { functionFactory.jsonObjectAgg_h2(); } + functionFactory.unnest( "c1", "c2" ); + //trim() requires parameters to be cast when used as trim character functionContributions.getFunctionRegistry().register( "trim", new TrimFunction( this, @@ -220,6 +223,11 @@ public class HSQLDialect extends Dialect { ) ); } + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "c2"; + } + @Override public String currentTime() { return "localtime"; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java index 63546f2528..256a343aa4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java @@ -22,6 +22,8 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; @@ -72,6 +74,17 @@ public class HSQLSqlAstTranslator extends AbstractSqlAs } } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + @Override protected void visitConflictClause(ConflictClause conflictClause) { if ( conflictClause != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java index 578d56fb40..3685fba179 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/JsonHelper.java @@ -31,6 +31,8 @@ import org.hibernate.type.BasicPluralType; import org.hibernate.type.BasicType; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; +import org.hibernate.type.descriptor.java.EnumJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JdbcDateJavaType; import org.hibernate.type.descriptor.java.JdbcTimeJavaType; @@ -38,6 +40,9 @@ import org.hibernate.type.descriptor.java.JdbcTimestampJavaType; import org.hibernate.type.descriptor.java.OffsetDateTimeJavaType; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; import static org.hibernate.dialect.StructHelper.getEmbeddedPart; import static org.hibernate.dialect.StructHelper.instantiate; @@ -57,6 +62,43 @@ public class JsonHelper { return sb.toString(); } + public static String arrayToString(MappingType elementMappingType, Object[] values, WrapperOptions options) { + if ( values.length == 0 ) { + return "[]"; + } + final StringBuilder sb = new StringBuilder(); + final JsonAppender jsonAppender = new JsonAppender( sb ); + char separator = '['; + for ( Object value : values ) { + sb.append( separator ); + toString( elementMappingType, value, options, jsonAppender ); + separator = ','; + } + sb.append( ']' ); + return sb.toString(); + } + + public static String arrayToString( + JavaType elementJavaType, + JdbcType elementJdbcType, + Object[] values, + WrapperOptions options) { + if ( values.length == 0 ) { + return "[]"; + } + final StringBuilder sb = new StringBuilder(); + final JsonAppender jsonAppender = new JsonAppender( sb ); + char separator = '['; + for ( Object value : values ) { + sb.append( separator ); + //noinspection unchecked + convertedValueToString( (JavaType) elementJavaType, elementJdbcType, value, options, jsonAppender ); + separator = ','; + } + sb.append( ']' ); + return sb.toString(); + } + private static void toString(EmbeddableMappingType embeddableMappingType, Object value, WrapperOptions options, JsonAppender appender) { toString( embeddableMappingType, options, appender, value, '{' ); appender.append( '}' ); @@ -130,34 +172,45 @@ public class JsonHelper { } private static void convertedValueToString( - MappingType mappedType, + JavaType javaType, + JdbcType jdbcType, Object value, WrapperOptions options, JsonAppender appender) { if ( value == null ) { appender.append( "null" ); } - else if ( mappedType instanceof EmbeddableMappingType ) { - toString( (EmbeddableMappingType) mappedType, value, options, appender ); - } - else if ( mappedType instanceof BasicType ) { - //noinspection unchecked - final BasicType basicType = (BasicType) mappedType; - convertedBasicValueToString( value, options, appender, basicType ); + else if ( jdbcType instanceof AggregateJdbcType aggregateJdbcType ) { + toString( aggregateJdbcType.getEmbeddableMappingType(), value, options, appender ); } else { - throw new UnsupportedOperationException( "Support for mapping type not yet implemented: " + mappedType.getClass().getName() ); + convertedBasicValueToString( value, options, appender, javaType, jdbcType ); } } + private static void convertedBasicValueToString( Object value, WrapperOptions options, JsonAppender appender, BasicType basicType) { //noinspection unchecked - final JavaType javaType = (JavaType) basicType.getJdbcJavaType(); - switch ( basicType.getJdbcType().getDefaultSqlTypeCode() ) { + convertedBasicValueToString( + value, + options, + appender, + (JavaType) basicType.getJdbcJavaType(), + basicType.getJdbcType() + ); + } + + private static void convertedBasicValueToString( + Object value, + WrapperOptions options, + JsonAppender appender, + JavaType javaType, + JdbcType jdbcType) { + switch ( jdbcType.getDefaultSqlTypeCode() ) { case SqlTypes.TINYINT: case SqlTypes.SMALLINT: case SqlTypes.INTEGER: @@ -272,19 +325,21 @@ public class JsonHelper { final int length = Array.getLength( value ); appender.append( '[' ); if ( length != 0 ) { - final BasicType elementType = ( (BasicPluralType) basicType ).getElementType(); + //noinspection unchecked + final JavaType elementJavaType = ( (BasicPluralJavaType) javaType ).getElementJavaType(); + final JdbcType elementJdbcType = ( (ArrayJdbcType) jdbcType ).getElementJdbcType(); Object arrayElement = Array.get( value, 0 ); - convertedValueToString( elementType, arrayElement, options, appender ); + convertedValueToString( elementJavaType, elementJdbcType, arrayElement, options, appender ); for ( int i = 1; i < length; i++ ) { arrayElement = Array.get( value, i ); appender.append( ',' ); - convertedValueToString( elementType, arrayElement, options, appender ); + convertedValueToString( elementJavaType, elementJdbcType, arrayElement, options, appender ); } } appender.append( ']' ); break; default: - throw new UnsupportedOperationException( "Unsupported JdbcType nested in JSON: " + basicType.getJdbcType() ); + throw new UnsupportedOperationException( "Unsupported JdbcType nested in JSON: " + jdbcType ); } } @@ -314,6 +369,39 @@ public class JsonHelper { return (X) values; } + public static X arrayFromString( + JavaType javaType, + JsonArrayJdbcType jsonArrayJdbcType, + String string, + WrapperOptions options) throws SQLException { + if ( string == null ) { + return null; + } + final JavaType elementJavaType = ((BasicPluralJavaType) javaType).getElementJavaType(); + final Class preferredJavaTypeClass = jsonArrayJdbcType.getElementJdbcType().getPreferredJavaTypeClass( options ); + final JavaType jdbcJavaType; + if ( preferredJavaTypeClass == null || preferredJavaTypeClass == elementJavaType.getJavaTypeClass() ) { + jdbcJavaType = elementJavaType; + } + else { + jdbcJavaType = options.getSessionFactory().getTypeConfiguration().getJavaTypeRegistry() + .resolveDescriptor( preferredJavaTypeClass ); + } + final CustomArrayList arrayList = new CustomArrayList(); + final int i = fromArrayString( + string, + false, + options, + 0, + arrayList, + elementJavaType, + jdbcJavaType, + jsonArrayJdbcType.getElementJdbcType() + ); + assert string.charAt( i - 1 ) == ']'; + return javaType.wrap( arrayList, options ); + } + private static int fromString( EmbeddableMappingType embeddableMappingType, String string, @@ -559,7 +647,30 @@ public class JsonHelper { int begin, CustomArrayList arrayList, BasicType elementType) throws SQLException { + return fromArrayString( + string, + returnEmbeddable, + options, + begin, + arrayList, + elementType.getMappedJavaType(), + elementType.getJdbcJavaType(), + elementType.getJdbcType() + ); + } + private static int fromArrayString( + String string, + boolean returnEmbeddable, + WrapperOptions options, + int begin, + CustomArrayList arrayList, + JavaType javaType, + JavaType jdbcJavaType, + JdbcType jdbcType) throws SQLException { + if ( string.length() == begin + 2 ) { + return begin + 2; + } boolean hasEscape = false; assert string.charAt( begin ) == '['; int start = begin + 1; @@ -586,7 +697,9 @@ public class JsonHelper { s = State.VALUE_END; arrayList.add( fromString( - elementType, + javaType, + jdbcJavaType, + jdbcType, string, start, i, @@ -693,7 +806,9 @@ public class JsonHelper { string, i, arrayList.getUnderlyingArray(), - elementType, + javaType, + jdbcJavaType, + jdbcType, elementIndex, returnEmbeddable, options @@ -728,6 +843,29 @@ public class JsonHelper { int selectableIndex, boolean returnEmbeddable, WrapperOptions options) throws SQLException { + return consumeLiteral( + string, + start, + values, + jdbcMapping.getMappedJavaType(), + jdbcMapping.getJdbcJavaType(), + jdbcMapping.getJdbcType(), + selectableIndex, + returnEmbeddable, + options + ); + } + + private static int consumeLiteral( + String string, + int start, + Object[] values, + JavaType javaType, + JavaType jdbcJavaType, + JdbcType jdbcType, + int selectableIndex, + boolean returnEmbeddable, + WrapperOptions options) throws SQLException { final char c = string.charAt( start ); switch ( c ) { case 'n': @@ -750,7 +888,9 @@ public class JsonHelper { start, start + 1, values, - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, selectableIndex, returnEmbeddable, options @@ -762,14 +902,18 @@ public class JsonHelper { start, start + 1, values, - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, selectableIndex, returnEmbeddable, options ); } values[selectableIndex] = fromString( - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, string, start, start + 1, @@ -806,7 +950,9 @@ public class JsonHelper { start, i, values, - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, selectableIndex, returnEmbeddable, options @@ -818,7 +964,9 @@ public class JsonHelper { start, i, values, - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, selectableIndex, returnEmbeddable, options @@ -836,7 +984,9 @@ public class JsonHelper { break; default: values[selectableIndex] = fromString( - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, string, start, i, @@ -856,7 +1006,9 @@ public class JsonHelper { int start, int dotIndex, Object[] values, - JdbcMapping jdbcMapping, + JavaType javaType, + JavaType jdbcJavaType, + JdbcType jdbcType, int selectableIndex, boolean returnEmbeddable, WrapperOptions options) throws SQLException { @@ -870,7 +1022,9 @@ public class JsonHelper { start, i, values, - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, selectableIndex, returnEmbeddable, options @@ -888,7 +1042,9 @@ public class JsonHelper { break; default: values[selectableIndex] = fromString( - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, string, start, i, @@ -906,7 +1062,9 @@ public class JsonHelper { int start, int eIndex, Object[] values, - JdbcMapping jdbcMapping, + JavaType javaType, + JavaType jdbcJavaType, + JdbcType jdbcType, int selectableIndex, boolean returnEmbeddable, WrapperOptions options) throws SQLException { @@ -933,7 +1091,9 @@ public class JsonHelper { break; default: values[selectableIndex] = fromString( - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, string, start, i, @@ -1001,10 +1161,35 @@ public class JsonHelper { boolean hasEscape, boolean returnEmbeddable, WrapperOptions options) throws SQLException { + return fromString( + jdbcMapping.getMappedJavaType(), + jdbcMapping.getJdbcJavaType(), + jdbcMapping.getJdbcType(), + string, + start, + end, + hasEscape, + returnEmbeddable, + options + ); + } + + private static Object fromString( + JavaType javaType, + JavaType jdbcJavaType, + JdbcType jdbcType, + String string, + int start, + int end, + boolean hasEscape, + boolean returnEmbeddable, + WrapperOptions options) throws SQLException { if ( hasEscape ) { final String unescaped = unescape( string, start, end ); return fromString( - jdbcMapping, + javaType, + jdbcJavaType, + jdbcType, unescaped, 0, unescaped.length(), @@ -1012,22 +1197,33 @@ public class JsonHelper { options ); } - return fromString( jdbcMapping, string, start, end, returnEmbeddable, options ); + return fromString( + javaType, + jdbcJavaType, + jdbcType, + string, + start, + end, + returnEmbeddable, + options + ); } private static Object fromString( - JdbcMapping jdbcMapping, + JavaType javaType, + JavaType jdbcJavaType, + JdbcType jdbcType, String string, int start, int end, boolean returnEmbeddable, WrapperOptions options) throws SQLException { - switch ( jdbcMapping.getJdbcType().getDefaultSqlTypeCode() ) { + switch ( jdbcType.getDefaultSqlTypeCode() ) { case SqlTypes.BINARY: case SqlTypes.VARBINARY: case SqlTypes.LONGVARBINARY: case SqlTypes.LONG32VARBINARY: - return jdbcMapping.getJdbcJavaType().wrap( + return jdbcJavaType.wrap( PrimitiveByteArrayJavaType.INSTANCE.fromEncodedString( string, start, @@ -1036,7 +1232,7 @@ public class JsonHelper { options ); case SqlTypes.DATE: - return jdbcMapping.getJdbcJavaType().wrap( + return jdbcJavaType.wrap( JdbcDateJavaType.INSTANCE.fromEncodedString( string, start, @@ -1047,7 +1243,7 @@ public class JsonHelper { case SqlTypes.TIME: case SqlTypes.TIME_WITH_TIMEZONE: case SqlTypes.TIME_UTC: - return jdbcMapping.getJdbcJavaType().wrap( + return jdbcJavaType.wrap( JdbcTimeJavaType.INSTANCE.fromEncodedString( string, start, @@ -1056,7 +1252,7 @@ public class JsonHelper { options ); case SqlTypes.TIMESTAMP: - return jdbcMapping.getJdbcJavaType().wrap( + return jdbcJavaType.wrap( JdbcTimestampJavaType.INSTANCE.fromEncodedString( string, start, @@ -1066,7 +1262,7 @@ public class JsonHelper { ); case SqlTypes.TIMESTAMP_WITH_TIMEZONE: case SqlTypes.TIMESTAMP_UTC: - return jdbcMapping.getJdbcJavaType().wrap( + return jdbcJavaType.wrap( OffsetDateTimeJavaType.INSTANCE.fromEncodedString( string, start, @@ -1077,28 +1273,21 @@ public class JsonHelper { case SqlTypes.TINYINT: case SqlTypes.SMALLINT: case SqlTypes.INTEGER: - if ( jdbcMapping.getValueConverter() == null ) { - Class javaTypeClass = jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass(); - if ( javaTypeClass == Boolean.class ) { - // BooleanJavaType has this as an implicit conversion - return Integer.parseInt( string, start, end, 10 ) == 1; - } - if ( javaTypeClass.isEnum() ) { - return javaTypeClass.getEnumConstants()[Integer.parseInt( string, start, end, 10 )]; - } + if ( jdbcJavaType.getJavaTypeClass() == Boolean.class ) { + return jdbcJavaType.wrap( Integer.parseInt( string, start, end, 10 ), options ); + } + else if ( jdbcJavaType instanceof EnumJavaType ) { + return jdbcJavaType.wrap( Integer.parseInt( string, start, end, 10 ), options ); } case SqlTypes.CHAR: case SqlTypes.NCHAR: case SqlTypes.VARCHAR: case SqlTypes.NVARCHAR: - if ( jdbcMapping.getValueConverter() == null - && jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass() == Boolean.class ) { - // BooleanJavaType has this as an implicit conversion - return end == start + 1 && string.charAt( start ) == 'Y'; + if ( jdbcJavaType.getJavaTypeClass() == Boolean.class && end == start + 1 ) { + return jdbcJavaType.wrap( string.charAt( start ), options ); } default: - if ( jdbcMapping.getJdbcType() instanceof AggregateJdbcType ) { - final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) jdbcMapping.getJdbcType(); + if ( jdbcType instanceof AggregateJdbcType aggregateJdbcType ) { final Object[] subValues = aggregateJdbcType.extractJdbcValues( CharSequenceHelper.subSequence( string, @@ -1119,7 +1308,7 @@ public class JsonHelper { return subValues; } - return jdbcMapping.getJdbcJavaType().fromEncodedString( + return jdbcJavaType.fromEncodedString( string, start, end diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index 6d2269e794..70d7d40e9e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -30,7 +30,7 @@ import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcTypeConstructor; import org.hibernate.type.descriptor.jdbc.JsonJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -99,6 +99,11 @@ public class MariaDBDialect extends MySQLDialect { commonFunctionFactory.jsonArrayAgg_mariadb(); commonFunctionFactory.jsonObjectAgg_mariadb(); commonFunctionFactory.jsonArrayAppend_mariadb(); + + if ( getVersion().isSameOrAfter( 10, 6 ) ) { + commonFunctionFactory.unnest_emulated(); + } + commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) .setInvariantType( functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) ) @@ -152,7 +157,7 @@ public class MariaDBDialect extends MySQLDialect { final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); // Make sure we register the JSON type descriptor before calling super, because MariaDB does not need casting jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, JsonJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, JsonArrayJdbcType.INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( JsonArrayJdbcTypeConstructor.INSTANCE ); super.contributeTypes( typeContributions, serviceRegistry ); if ( getVersion().isSameOrAfter( 10, 7 ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java index 1f769c64fd..f06bf0b489 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java @@ -21,6 +21,7 @@ import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.insert.ConflictClause; @@ -280,6 +281,11 @@ public class MariaDBSqlAstTranslator extends AbstractSq emulateQueryPartTableReferenceColumnAliasing( tableReference ); } + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { + renderTableReferenceIdentificationVariable( tableReference ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { if ( !isRowNumberingCurrentQueryPart() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java index 9a77485407..27a752e279 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java @@ -5,16 +5,17 @@ package org.hibernate.dialect; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; /** * @author Christian Beikov */ public class MySQLCastingJsonArrayJdbcType extends JsonArrayJdbcType { - /** - * Singleton access - */ - public static final JsonArrayJdbcType INSTANCE = new MySQLCastingJsonArrayJdbcType(); + + public MySQLCastingJsonArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); + } @Override public void appendWriteExpression( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..39bf0a5128 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcTypeConstructor.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link MySQLCastingJsonArrayJdbcType}. + */ +public class MySQLCastingJsonArrayJdbcTypeConstructor implements JdbcTypeConstructor { + + public static final MySQLCastingJsonArrayJdbcTypeConstructor INSTANCE = new MySQLCastingJsonArrayJdbcTypeConstructor(); + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new MySQLCastingJsonArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 06a3190bd4..c157d979d3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -537,6 +537,11 @@ public class MySQLDialect extends Dialect { return Types.BIT; } + @Override + public int getPreferredSqlTypeCodeForArray() { + return SqlTypes.JSON_ARRAY; + } + // @Override // public int getDefaultDecimalPrecision() { // //this is the maximum, but I guess it's too high @@ -652,6 +657,10 @@ public class MySQLDialect extends Dialect { functionFactory.jsonMergepatch_mysql(); functionFactory.jsonArrayAppend_mysql(); functionFactory.jsonArrayInsert_mysql(); + + if ( getMySQLVersion().isSameOrAfter( 8 ) ) { + functionFactory.unnest_emulated(); + } } @Override @@ -661,7 +670,7 @@ public class MySQLDialect extends Dialect { final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, MySQLCastingJsonJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, MySQLCastingJsonArrayJdbcType.INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( MySQLCastingJsonArrayJdbcTypeConstructor.INSTANCE ); // MySQL requires a custom binder for binding untyped nulls with the NULL type typeContributions.contributeJdbcType( NullJdbcType.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java index 542d76dd39..16bea157e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java @@ -23,6 +23,8 @@ import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; +import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; @@ -315,6 +317,17 @@ public class MySQLSqlAstTranslator extends AbstractSqlA emulateValuesTableReferenceColumnAliasing( tableReference ); } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { if ( !isRowNumberingCurrentQueryPart() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index b32ae9bf7b..1d1a0d7819 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -83,13 +83,10 @@ import org.hibernate.type.JavaObjectType; import org.hibernate.type.NullType; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; -import org.hibernate.type.descriptor.jdbc.ArrayJdbcType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.NullJdbcType; -import org.hibernate.type.descriptor.jdbc.ObjectJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType; -import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType; import org.hibernate.type.descriptor.jdbc.SqlTypedJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; @@ -134,7 +131,6 @@ import static org.hibernate.type.SqlTypes.FLOAT; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.REAL; @@ -425,6 +421,8 @@ public class OracleDialect extends Dialect { functionFactory.xmlquery_oracle(); functionFactory.xmlexists(); functionFactory.xmlagg(); + + functionFactory.unnest_oracle(); } @Override @@ -821,11 +819,9 @@ public class OracleDialect extends Dialect { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "MDSYS.SDO_GEOMETRY", this ) ); if ( getVersion().isSameOrAfter( 21 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } else { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "blob", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "blob", this ) ); } ddlTypeRegistry.addDescriptor( new ArrayDdlTypeImpl( this, false ) ); @@ -995,16 +991,16 @@ public class OracleDialect extends Dialect { if ( getVersion().isSameOrAfter( 21 ) ) { typeContributions.contributeJdbcType( OracleJsonJdbcType.INSTANCE ); - typeContributions.contributeJdbcType( OracleJsonArrayJdbcType.INSTANCE ); + typeContributions.contributeJdbcTypeConstructor( OracleJsonArrayJdbcTypeConstructor.NATIVE_INSTANCE ); } else { typeContributions.contributeJdbcType( OracleJsonBlobJdbcType.INSTANCE ); - typeContributions.contributeJdbcType( OracleJsonArrayBlobJdbcType.INSTANCE ); + typeContributions.contributeJdbcTypeConstructor( OracleJsonArrayJdbcTypeConstructor.BLOB_INSTANCE ); } if ( OracleJdbcHelper.isUsable( serviceRegistry ) ) { // Register a JdbcType to allow reading from native queries - typeContributions.contributeJdbcType( new ArrayJdbcType( ObjectJdbcType.INSTANCE ) ); +// typeContributions.contributeJdbcType( new ArrayJdbcType( ObjectJdbcType.INSTANCE ) ); typeContributions.contributeJdbcTypeConstructor( getArrayJdbcTypeConstructor( serviceRegistry ) ); typeContributions.contributeJdbcTypeConstructor( getNestedTableJdbcTypeConstructor( serviceRegistry ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java index 5def46e4ed..aac07cb6fa 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java @@ -6,6 +6,7 @@ package org.hibernate.dialect; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; /** @@ -14,12 +15,9 @@ import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; * @author Christian Beikov */ public class OracleJsonArrayJdbcType extends OracleJsonArrayBlobJdbcType { - /** - * Singleton access - */ - public static final OracleJsonArrayJdbcType INSTANCE = new OracleJsonArrayJdbcType(); - private OracleJsonArrayJdbcType() { + public OracleJsonArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..bb245e4c55 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcTypeConstructor.java @@ -0,0 +1,51 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link OracleJsonArrayJdbcType} and {@link OracleJsonArrayBlobJdbcType}. + */ +public class OracleJsonArrayJdbcTypeConstructor implements JdbcTypeConstructor { + + public static final OracleJsonArrayJdbcTypeConstructor NATIVE_INSTANCE = new OracleJsonArrayJdbcTypeConstructor( true ); + public static final OracleJsonArrayJdbcTypeConstructor BLOB_INSTANCE = new OracleJsonArrayJdbcTypeConstructor( false ); + + private final boolean nativeJson; + + public OracleJsonArrayJdbcTypeConstructor(boolean nativeJson) { + this.nativeJson = nativeJson; + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return nativeJson ? new OracleJsonArrayJdbcType( elementType ) : new OracleJsonArrayBlobJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java index 80d28ff41f..e10d6cfb90 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java @@ -8,17 +8,22 @@ import java.util.ArrayList; import java.util.List; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.FrameExclusion; import org.hibernate.query.sqm.FrameKind; import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.cte.CteMaterialization; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; @@ -30,6 +35,7 @@ import org.hibernate.sql.ast.tree.expression.Over; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.FromClause; import org.hibernate.sql.ast.tree.from.FunctionTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; @@ -254,9 +260,42 @@ public class OracleSqlAstTranslator extends SqlAstTrans @Override public void visitFunctionTableReference(FunctionTableReference tableReference) { - append( "table(" ); tableReference.getFunctionExpression().accept( this ); - append( CLOSE_PARENTHESIS ); + if ( !tableReference.rendersIdentifierVariable() ) { + renderDerivedTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "lateral (select t.*, rownum " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from table(" ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( ") t)" ); + } + else { + appendSql( "table(" ); + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + append( ')' ); + } + } + + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference instanceof FunctionTableReference && tableReference.isLateral() ) { + // No need for a lateral keyword for functions + tableReference.accept( this ); + } + else { + super.renderDerivedTableReference( tableReference ); + } + } + + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { renderTableReferenceIdentificationVariable( tableReference ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java index 20f8aa88bd..89c4db2cd1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java @@ -12,6 +12,7 @@ import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; /** * The following class provides some convenience methods for accessing JdbcType instance, @@ -52,12 +53,12 @@ public final class PgJdbcHelper { return createJdbcType( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonPGObjectJsonbType" ); } - public static JdbcType getJsonArrayJdbcType(ServiceRegistry serviceRegistry) { - return createJdbcType( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonArrayPGObjectJsonType" ); + public static JdbcTypeConstructor getJsonArrayJdbcType(ServiceRegistry serviceRegistry) { + return createJdbcTypeConstructor( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonArrayPGObjectJsonJdbcTypeConstructor" ); } - public static JdbcType getJsonbArrayJdbcType(ServiceRegistry serviceRegistry) { - return createJdbcType( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonArrayPGObjectJsonbType" ); + public static JdbcTypeConstructor getJsonbArrayJdbcType(ServiceRegistry serviceRegistry) { + return createJdbcTypeConstructor( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonArrayPGObjectJsonbJdbcTypeConstructor" ); } public static JdbcType createJdbcType(ServiceRegistry serviceRegistry, String className) { @@ -74,4 +75,19 @@ public final class PgJdbcHelper { throw new HibernateError( "Could not construct JdbcType", e ); } } + + public static JdbcTypeConstructor createJdbcTypeConstructor(ServiceRegistry serviceRegistry, String className) { + final ClassLoaderService classLoaderService = serviceRegistry.requireService( ClassLoaderService.class ); + try { + final Class clazz = classLoaderService.classForName( className ); + final Constructor constructor = clazz.getConstructor(); + return (JdbcTypeConstructor) constructor.newInstance(); + } + catch (NoSuchMethodException e) { + throw new HibernateError( "Class does not have an empty constructor", e ); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new HibernateError( "Could not construct JdbcTypeConstructor", e ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLArrayJdbcType.java index d9845d7e13..695a0631e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLArrayJdbcType.java @@ -73,7 +73,9 @@ public class PostgreSQLArrayJdbcType extends ArrayJdbcType { ); objects = new Object[domainObjects.length]; for ( int i = 0; i < domainObjects.length; i++ ) { - objects[i] = aggregateJdbcType.createJdbcValue( domainObjects[i], options ); + if ( domainObjects[i] != null ) { + objects[i] = aggregateJdbcType.createJdbcValue( domainObjects[i], options ); + } } } else { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java index 5d06d7140f..9b12827385 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java @@ -5,6 +5,7 @@ package org.hibernate.dialect; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; /** @@ -12,12 +13,10 @@ import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; */ public class PostgreSQLCastingJsonArrayJdbcType extends JsonArrayJdbcType { - public static final PostgreSQLCastingJsonArrayJdbcType JSON_INSTANCE = new PostgreSQLCastingJsonArrayJdbcType( false ); - public static final PostgreSQLCastingJsonArrayJdbcType JSONB_INSTANCE = new PostgreSQLCastingJsonArrayJdbcType( true ); - private final boolean jsonb; - public PostgreSQLCastingJsonArrayJdbcType(boolean jsonb) { + public PostgreSQLCastingJsonArrayJdbcType(JdbcType elementJdbcType, boolean jsonb) { + super( elementJdbcType ); this.jsonb = jsonb; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..3484bc2520 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcTypeConstructor.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link PostgreSQLCastingJsonArrayJdbcType}. + */ +public class PostgreSQLCastingJsonArrayJdbcTypeConstructor implements JdbcTypeConstructor { + + public static final PostgreSQLCastingJsonArrayJdbcTypeConstructor JSONB_INSTANCE = new PostgreSQLCastingJsonArrayJdbcTypeConstructor( true ); + public static final PostgreSQLCastingJsonArrayJdbcTypeConstructor JSON_INSTANCE = new PostgreSQLCastingJsonArrayJdbcTypeConstructor( false ); + + private final boolean jsonb; + + public PostgreSQLCastingJsonArrayJdbcTypeConstructor(boolean jsonb) { + this.jsonb = jsonb; + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new PostgreSQLCastingJsonArrayJdbcType( elementType, jsonb ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index b8b6368467..27ebf068ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.TimeZone; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Length; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -111,7 +112,6 @@ import static org.hibernate.type.SqlTypes.GEOGRAPHY; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.JSON; -import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -261,7 +261,6 @@ public class PostgreSQLDialect extends Dialect { // Prefer jsonb if possible ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); - ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); @@ -665,6 +664,18 @@ public class PostgreSQLDialect extends Dialect { ); functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); functionFactory.dateTrunc(); + + if ( getVersion().isSameOrAfter( 17 ) ) { + functionFactory.unnest( null, "ordinality" ); + } + else { + functionFactory.unnest_postgresql(); + } + } + + @Override + public @Nullable String getDefaultOrdinalityColumnName() { + return "ordinality"; } /** @@ -1415,14 +1426,14 @@ public class PostgreSQLDialect extends Dialect { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getIntervalJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getStructJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); - jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLStructCastingJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } } else { @@ -1430,7 +1441,7 @@ public class PostgreSQLDialect extends Dialect { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLStructCastingJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); - jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addTypeConstructorIfAbsent( PostgreSQLCastingJsonArrayJdbcTypeConstructor.JSONB_INSTANCE ); } // PostgreSQL requires a custom binder for binding untyped nulls as VARBINARY @@ -1450,6 +1461,7 @@ public class PostgreSQLDialect extends Dialect { jdbcTypeRegistry.addDescriptor( PostgreSQLOrdinalEnumJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( PostgreSQLUUIDJdbcType.INSTANCE ); + // Replace the standard array constructor jdbcTypeRegistry.addTypeConstructor( PostgreSQLArrayJdbcTypeConstructor.INSTANCE ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonJdbcTypeConstructor.java new file mode 100644 index 0000000000..191e6638f2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonJdbcTypeConstructor.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link PostgreSQLCastingJsonArrayJdbcType}. + */ +public class PostgreSQLJsonArrayPGObjectJsonJdbcTypeConstructor implements JdbcTypeConstructor { + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new PostgreSQLJsonArrayPGObjectType( elementType, false ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java deleted file mode 100644 index 43daecfe84..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-License-Identifier: LGPL-2.1-or-later - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.dialect; - -/** - * @author Christian Beikov - */ -public class PostgreSQLJsonArrayPGObjectJsonType extends AbstractPostgreSQLJsonArrayPGObjectType { - public PostgreSQLJsonArrayPGObjectJsonType() { - super( false ); - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbJdbcTypeConstructor.java new file mode 100644 index 0000000000..13547a390d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbJdbcTypeConstructor.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link PostgreSQLCastingJsonArrayJdbcType}. + */ +public class PostgreSQLJsonArrayPGObjectJsonbJdbcTypeConstructor implements JdbcTypeConstructor { + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new PostgreSQLJsonArrayPGObjectType( elementType, true ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java deleted file mode 100644 index c3245958ff..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-License-Identifier: LGPL-2.1-or-later - * Copyright Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.dialect; - -/** - * @author Christian Beikov - */ -public class PostgreSQLJsonArrayPGObjectJsonbType extends AbstractPostgreSQLJsonArrayPGObjectType { - public PostgreSQLJsonArrayPGObjectJsonbType() { - super( true ); - } -} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectType.java similarity index 84% rename from hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java rename to hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectType.java index 30a5daf6f4..0542e31536 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectType.java @@ -16,6 +16,7 @@ import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.BasicBinder; import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; import org.postgresql.util.PGobject; @@ -23,10 +24,12 @@ import org.postgresql.util.PGobject; /** * @author Christian Beikov */ -public abstract class AbstractPostgreSQLJsonArrayPGObjectType extends JsonArrayJdbcType { +public class PostgreSQLJsonArrayPGObjectType extends JsonArrayJdbcType { private final boolean jsonb; - protected AbstractPostgreSQLJsonArrayPGObjectType(boolean jsonb) { + + public PostgreSQLJsonArrayPGObjectType(JdbcType elementJdbcType, boolean jsonb) { + super( elementJdbcType ); this.jsonb = jsonb; } @@ -41,7 +44,7 @@ public abstract class AbstractPostgreSQLJsonArrayPGObjectType extends JsonArrayJ @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - final String stringValue = ( (AbstractPostgreSQLJsonArrayPGObjectType) getJdbcType() ).toString( + final String stringValue = ( (PostgreSQLJsonArrayPGObjectType) getJdbcType() ).toString( value, getJavaType(), options @@ -55,7 +58,7 @@ public abstract class AbstractPostgreSQLJsonArrayPGObjectType extends JsonArrayJ @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { - final String stringValue = ( (AbstractPostgreSQLJsonArrayPGObjectType) getJdbcType() ).toString( + final String stringValue = ( (PostgreSQLJsonArrayPGObjectType) getJdbcType() ).toString( value, getJavaType(), options @@ -91,7 +94,7 @@ public abstract class AbstractPostgreSQLJsonArrayPGObjectType extends JsonArrayJ if ( object == null ) { return null; } - return ( (AbstractPostgreSQLJsonArrayPGObjectType) getJdbcType() ).fromString( + return ( (PostgreSQLJsonArrayPGObjectType) getJdbcType() ).fromString( object.toString(), getJavaType(), options diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcType.java new file mode 100644 index 0000000000..646041b248 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcType.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.XmlArrayJdbcType; + +/** + * @author Christian Beikov + */ +public class SQLServerCastingXmlArrayJdbcType extends XmlArrayJdbcType { + + public SQLServerCastingXmlArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); + } + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as xml)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..35e2c63fb8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlArrayJdbcTypeConstructor.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link SQLServerCastingXmlArrayJdbcType}. + */ +public class SQLServerCastingXmlArrayJdbcTypeConstructor implements JdbcTypeConstructor { + + public static final SQLServerCastingXmlArrayJdbcTypeConstructor INSTANCE = new SQLServerCastingXmlArrayJdbcTypeConstructor(); + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + @Override + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new SQLServerCastingXmlArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.XML_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlJdbcType.java new file mode 100644 index 0000000000..38608eefa2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerCastingXmlJdbcType.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.jdbc.XmlJdbcType; + +/** + * @author Christian Beikov + */ +public class SQLServerCastingXmlJdbcType extends XmlJdbcType { + /** + * Singleton access + */ + public static final XmlJdbcType INSTANCE = new SQLServerCastingXmlJdbcType( null ); + + public SQLServerCastingXmlJdbcType(EmbeddableMappingType embeddableMappingType) { + super( embeddableMappingType ); + } + + @Override + public AggregateJdbcType resolveAggregateJdbcType( + EmbeddableMappingType mappingType, + String sqlType, + RuntimeModelCreationContext creationContext) { + return new SQLServerCastingXmlJdbcType( mappingType ); + } + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as xml)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index d00622b886..74714a22c7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -82,7 +82,6 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.TimestampUtcAsJdbcTimestampJdbcType; import org.hibernate.type.descriptor.jdbc.TinyIntAsSmallIntJdbcType; import org.hibernate.type.descriptor.jdbc.UUIDJdbcType; -import org.hibernate.type.descriptor.jdbc.XmlJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; @@ -116,6 +115,7 @@ import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.SqlTypes.XML_ARRAY; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; @@ -267,6 +267,11 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( UUID, "uniqueidentifier", this ) ); } + @Override + public int getPreferredSqlTypeCodeForArray() { + return XML_ARRAY; + } + @Override public JdbcType resolveSqlTypeDescriptor( String columnTypeName, @@ -329,8 +334,9 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { Types.TINYINT, TinyIntAsSmallIntJdbcType.INSTANCE ); - typeContributions.contributeJdbcType( XmlJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( SQLServerCastingXmlJdbcType.INSTANCE ); typeContributions.contributeJdbcType( UUIDJdbcType.INSTANCE ); + typeContributions.contributeJdbcTypeConstructor( SQLServerCastingXmlArrayJdbcTypeConstructor.INSTANCE ); } @Override @@ -439,6 +445,9 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { functionFactory.xmlquery_sqlserver(); functionFactory.xmlexists_sqlserver(); functionFactory.xmlagg_sqlserver(); + + functionFactory.unnest_sqlserver(); + if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java index 9865b7c1cf..e5955ad50d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerSqlAstTranslator.java @@ -10,13 +10,18 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.query.IllegalQueryOperationException; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.ast.tree.delete.DeleteStatement; import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; @@ -24,10 +29,9 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; -import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.from.UnionTableReference; import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; @@ -180,17 +184,24 @@ public class SQLServerSqlAstTranslator extends SqlAstTr } } - protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { - if ( shouldInlineCte( tableGroup ) ) { - inlineCteTableGroup( tableGroup, lockMode ); - return false; - } - final TableReference tableReference = tableGroup.getPrimaryTableReference(); - if ( tableReference instanceof NamedTableReference ) { - return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); - } + @Override + protected void renderDerivedTableReference(DerivedTableReference tableReference) { tableReference.accept( this ); - return false; + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalitySubPart != null ) { + appendSql( "(select t.*, row_number() over(order by (select 1)) " ); + appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + appendSql( " from " ); + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + append( " t)" ); + } + else { + super.renderNamedSetReturningFunction( functionName, sqlAstArguments, tupleType, tableIdentifierVariable, argumentRenderingMode ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java index 298cdd2b51..7abc44fca4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SpannerSqlAstTranslator.java @@ -6,7 +6,6 @@ package org.hibernate.dialect; import java.util.List; -import org.hibernate.LockMode; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; @@ -18,9 +17,6 @@ import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; import org.hibernate.sql.ast.tree.expression.Summarization; import org.hibernate.sql.ast.tree.from.DerivedTableReference; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; @@ -107,17 +103,8 @@ public class SpannerSqlAstTranslator extends AbstractSq } @Override - protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lockMode) { - if ( shouldInlineCte( tableGroup ) ) { - inlineCteTableGroup( tableGroup, lockMode ); - return false; - } - final TableReference tableReference = tableGroup.getPrimaryTableReference(); - if ( tableReference instanceof NamedTableReference ) { - return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); - } - final DerivedTableReference derivedTableReference = (DerivedTableReference) tableReference; - final boolean correlated = derivedTableReference.isLateral(); + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + final boolean correlated = tableReference.isLateral(); final boolean oldCorrelated = this.correlated; if ( correlated ) { this.correlated = true; @@ -128,7 +115,6 @@ public class SpannerSqlAstTranslator extends AbstractSq this.correlated = oldCorrelated; appendSql( CLOSE_PARENTHESIS ); } - return false; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java index 59f87a1143..3248b0a5d4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -13,7 +13,9 @@ import org.hibernate.Length; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.QueryTimeoutException; +import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.TypeContributions; +import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.TopLimitHandler; import org.hibernate.engine.jdbc.Size; @@ -52,6 +54,7 @@ import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.TIME; import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.XML_ARRAY; /** * A {@linkplain Dialect SQL dialect} for Sybase Adaptive Server Enterprise 16 and above. @@ -157,6 +160,11 @@ public class SybaseASEDialect extends SybaseDialect { } } + @Override + public int getPreferredSqlTypeCodeForArray() { + return XML_ARRAY; + } + @Override public int getMaxVarcharLength() { // the maximum length of a VARCHAR or VARBINARY @@ -168,6 +176,15 @@ public class SybaseASEDialect extends SybaseDialect { return 16_384; } + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry( functionContributions ); + + CommonFunctionFactory functionFactory = new CommonFunctionFactory( functionContributions); + + functionFactory.unnest_sybasease(); + } + @Override public long getDefaultLobLength() { return Length.LONG32; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java index a6d4eaf5c8..e404008198 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java @@ -11,6 +11,7 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; @@ -46,6 +47,7 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.type.SqlTypes; /** * A SQL AST translator for Sybase ASE. @@ -320,7 +322,7 @@ public class SybaseASESqlAstTranslator extends Abstract append( '(' ); visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); append( ')' ); - renderDerivedTableReference( tableReference ); + renderDerivedTableReferenceIdentificationVariable( tableReference ); } @Override @@ -355,8 +357,56 @@ public class SybaseASESqlAstTranslator extends Abstract @Override protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + // In Sybase ASE, XMLTYPE is not "comparable", so we have to cast the two parts to varchar for this purpose + final boolean isLob = isLob( lhs.getExpressionType() ); + if ( isLob ) { + switch ( operator ) { + case EQUAL: + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + return; + case NOT_EQUAL: + lhs.accept( this ); + appendSql( " not like " ); + rhs.accept( this ); + return; + default: + // Fall through + break; + } + } // I think intersect is only supported in 16.0 SP3 if ( getDialect().isAnsiNullOn() ) { + if ( isLob ) { + switch ( operator ) { + case DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=1" ); + return; + case NOT_DISTINCT_FROM: + appendSql( "case when " ); + lhs.accept( this ); + appendSql( " like " ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null and " ); + rhs.accept( this ); + appendSql( " is null then 0 else 1 end=0" ); + return; + default: + // Fall through + break; + } + } if ( supportsDistinctFromPredicate() ) { renderComparisonEmulateIntersect( lhs, operator, rhs ); } @@ -377,10 +427,20 @@ public class SybaseASESqlAstTranslator extends Abstract lhs.accept( this ); switch ( operator ) { case DISTINCT_FROM: - appendSql( "<>" ); + if ( isLob ) { + appendSql( " not like " ); + } + else { + appendSql( "<>" ); + } break; case NOT_DISTINCT_FROM: - appendSql( '=' ); + if ( isLob ) { + appendSql( " like " ); + } + else { + appendSql( '=' ); + } break; case LESS_THAN: case GREATER_THAN: @@ -416,6 +476,21 @@ public class SybaseASESqlAstTranslator extends Abstract } } + public static boolean isLob(JdbcMappingContainer expressionType) { + return expressionType != null && expressionType.getJdbcTypeCount() == 1 && switch ( expressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() ) { + case SqlTypes.LONG32NVARCHAR, + SqlTypes.LONG32VARCHAR, + SqlTypes.LONGNVARCHAR, + SqlTypes.LONGVARCHAR, + SqlTypes.LONG32VARBINARY, + SqlTypes.LONGVARBINARY, + SqlTypes.CLOB, + SqlTypes.NCLOB, + SqlTypes.BLOB -> true; + default -> false; + }; + } + @Override protected boolean supportsIntersect() { // At least the version that diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java index 46cdcf5a6a..b1fd16ce75 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseSqlAstTranslator.java @@ -204,7 +204,7 @@ public class SybaseSqlAstTranslator extends AbstractSql append( '(' ); visitValuesListEmulateSelectUnion( tableReference.getValuesList() ); append( ')' ); - renderDerivedTableReference( tableReference ); + renderDerivedTableReferenceIdentificationVariable( tableReference ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java index ade81bf7d9..cea64630f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java @@ -22,6 +22,7 @@ import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.DerivedTableReference; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.QueryPartTableReference; import org.hibernate.sql.ast.tree.from.ValuesTableReference; @@ -249,6 +250,11 @@ public class TiDBSqlAstTranslator extends AbstractSqlAs emulateQueryPartTableReferenceColumnAliasing( tableReference ); } + @Override + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { + renderTableReferenceIdentificationVariable( tableReference ); + } + @Override public void visitOffsetFetchClause(QueryPart queryPart) { if ( !isRowNumberingCurrentQueryPart() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java index 1f005b7df1..34825926c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java @@ -15,6 +15,8 @@ import java.util.Base64; import java.util.List; import org.hibernate.Internal; +import org.hibernate.engine.spi.LazySessionWrapperOptions; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.CharSequenceHelper; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; @@ -24,6 +26,7 @@ import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.IntegerJavaType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JdbcDateJavaType; @@ -32,6 +35,8 @@ import org.hibernate.type.descriptor.java.JdbcTimestampJavaType; import org.hibernate.type.descriptor.java.OffsetDateTimeJavaType; import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import static java.lang.Character.isLetter; +import static java.lang.Character.isLetterOrDigit; import static org.hibernate.dialect.StructHelper.getEmbeddedPart; import static org.hibernate.dialect.StructHelper.instantiate; @@ -486,7 +491,6 @@ public class XmlHelper { WrapperOptions options, XMLAppender sb) { final Object[] array = embeddableMappingType.getValues( value ); - final int numberOfAttributes = embeddableMappingType.getNumberOfAttributeMappings(); for ( int i = 0; i < array.length; i++ ) { if ( array[i] == null ) { continue; @@ -646,6 +650,28 @@ public class XmlHelper { return selectableIndex; } + public static boolean isValidXmlName(String name) { + if ( name.isEmpty() + || !isValidXmlNameStart( name.charAt( 0 ) ) + || name.regionMatches( true, 0, "xml", 0, 3 ) ) { + return false; + } + for ( int i = 1; i < name.length(); i++ ) { + if ( !isValidXmlNameChar( name.charAt( i ) ) ) { + return false; + } + } + return true; + } + + public static boolean isValidXmlNameStart(char c) { + return isLetter( c ) || c == '_' || c == ':'; + } + + public static boolean isValidXmlNameChar(char c) { + return isLetterOrDigit( c ) || c == '_' || c == ':' || c == '-' || c == '.'; + } + private static class XMLAppender extends OutputStream implements SqlAppender { private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); @@ -781,4 +807,47 @@ public class XmlHelper { } } + public static CollectionTags determineCollectionTags(BasicPluralJavaType pluralJavaType, SessionFactoryImplementor sessionFactory) { + //noinspection unchecked + final JavaType javaType = (JavaType) pluralJavaType; + final LazySessionWrapperOptions lazySessionWrapperOptions = new LazySessionWrapperOptions( sessionFactory ); + // Produce the XML string for a collection with a null element to find out the root and element tag names + final String nullElementXml; + try { + nullElementXml = sessionFactory.getSessionFactoryOptions().getXmlFormatMapper().toString( + javaType.fromString( "{null}" ), + javaType, + lazySessionWrapperOptions + ); + } + finally { + lazySessionWrapperOptions.cleanup(); + } + + // There must be an end tag for the root, so find that first + final int rootCloseTagPosition = nullElementXml.lastIndexOf( '<' ); + assert nullElementXml.charAt( rootCloseTagPosition + 1 ) == '/'; + final int rootNameStart = rootCloseTagPosition + 2; + final int rootCloseTagEnd = nullElementXml.indexOf( '>', rootCloseTagPosition ); + final String rootTag = nullElementXml.substring( rootNameStart, rootCloseTagEnd ); + + // Then search for the open tag of the root and determine the start of the first item + final int itemTagStart = nullElementXml.indexOf( + '<', + nullElementXml.indexOf( "<" + rootTag + ">" ) + rootTag.length() + 2 + ); + final int itemNameStart = itemTagStart + 1; + int itemNameEnd = itemNameStart; + for ( int i = itemNameStart + 1; i < nullElementXml.length(); i++ ) { + if ( !isValidXmlNameChar( nullElementXml.charAt( i ) ) ) { + itemNameEnd = i; + break; + } + } + final String elementNodeName = nullElementXml.substring( itemNameStart, itemNameEnd ); + return new CollectionTags( rootTag, elementNodeName ); + } + + public record CollectionTags(String rootName, String elementName) {} + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java index 6629a6ce17..69a87ae3f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupport.java @@ -13,6 +13,8 @@ import org.hibernate.dialect.Dialect; import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.Column; import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.expression.Expression; @@ -39,13 +41,51 @@ public interface AggregateSupport { * @param aggregateColumn The type information for the aggregate column * @param column The column within the aggregate type, for which to return the read expression */ - String aggregateComponentCustomReadExpression( + default String aggregateComponentCustomReadExpression( String template, String placeholder, String aggregateParentReadExpression, String columnExpression, AggregateColumn aggregateColumn, - Column column); + Column column) { + return aggregateComponentCustomReadExpression( + template, + placeholder, + aggregateParentReadExpression, + columnExpression, + aggregateColumn.getTypeCode(), + new SqlTypedMappingImpl( + column.getTypeName(), + column.getLength(), + column.getPrecision(), + column.getScale(), + column.getTemporalPrecision(), + column.getType() + ) + ); + } + + /** + * Returns the custom read expression to use for {@code column}. + * Replaces the given {@code placeholder} in the given {@code template} + * by the custom read expression to use for {@code column}. + * + * @param template The custom read expression template of the column + * @param placeholder The placeholder to replace with the actual read expression + * @param aggregateParentReadExpression The expression to the aggregate column, which contains the column + * @param columnExpression The column within the aggregate type, for which to return the read expression + * @param aggregateColumnTypeCode The SQL type code of the aggregate column + * @param column The column within the aggregate type, for which to return the read expression + * + * @since 7.0 + */ + String aggregateComponentCustomReadExpression( + String template, + String placeholder, + String aggregateParentReadExpression, + String columnExpression, + int aggregateColumnTypeCode, + SqlTypedMapping column); /** * Returns the assignment expression to use for {@code column}, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupportImpl.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupportImpl.java index d76991acef..cf19aba14d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupportImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/AggregateSupportImpl.java @@ -12,6 +12,7 @@ import org.hibernate.boot.model.relational.Namespace; import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.Column; import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.type.spi.TypeConfiguration; public class AggregateSupportImpl implements AggregateSupport { @@ -19,13 +20,7 @@ public class AggregateSupportImpl implements AggregateSupport { public static final AggregateSupport INSTANCE = new AggregateSupportImpl(); @Override - public String aggregateComponentCustomReadExpression( - String template, - String placeholder, - String aggregateParentReadExpression, - String columnExpression, - AggregateColumn aggregateColumn, - Column column) { + public String aggregateComponentCustomReadExpression(String template, String placeholder, String aggregateParentReadExpression, String columnExpression, int aggregateColumnTypeCode, SqlTypedMapping column) { throw new UnsupportedOperationException( "Dialect does not support aggregateComponentCustomReadExpression: " + getClass().getName() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java index 27c6b9f593..6dca601d4c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/DB2AggregateSupport.java @@ -26,6 +26,7 @@ import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.mapping.SqlExpressible; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; @@ -47,13 +48,13 @@ public class DB2AggregateSupport extends AggregateSupportImpl { String placeholder, String aggregateParentReadExpression, String columnExpression, - AggregateColumn aggregateColumn, - Column column) { - switch ( aggregateColumn.getTypeCode() ) { + int aggregateColumnTypeCode, + SqlTypedMapping column) { + switch ( aggregateColumnTypeCode ) { case STRUCT: return template.replace( placeholder, aggregateParentReadExpression + ".." + columnExpression ); } - throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumn.getTypeCode() ); + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java index 78c6a6f319..247c28357a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java @@ -24,6 +24,7 @@ import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; @@ -113,9 +114,9 @@ public class OracleAggregateSupport extends AggregateSupportImpl { String placeholder, String aggregateParentReadExpression, String columnExpression, - AggregateColumn aggregateColumn, - Column column) { - switch ( aggregateColumn.getTypeCode() ) { + int aggregateColumnTypeCode, + SqlTypedMapping column) { + switch ( aggregateColumnTypeCode ) { case JSON: String jsonTypeName = "json"; switch ( jsonSupport ) { @@ -132,14 +133,14 @@ public class OracleAggregateSupport extends AggregateSupportImpl { else { parentPartExpression = aggregateParentReadExpression + ",'$."; } - switch ( column.getTypeCode() ) { + switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { case BIT: return template.replace( placeholder, "decode(json_value(" + parentPartExpression + columnExpression + "'),'true',1,'false',0,null)" ); case BOOLEAN: - if ( column.getTypeName().toLowerCase( Locale.ROOT ).trim().startsWith( "number" ) ) { + if ( column.getColumnDefinition().toLowerCase( Locale.ROOT ).trim().startsWith( "number" ) ) { return template.replace( placeholder, "decode(json_value(" + parentPartExpression + columnExpression + "'),'true',1,'false',0,null)" @@ -152,7 +153,7 @@ public class OracleAggregateSupport extends AggregateSupportImpl { case BIGINT: return template.replace( placeholder, - "json_value(" + parentPartExpression + columnExpression + "' returning " + column.getTypeName() + ')' + "json_value(" + parentPartExpression + columnExpression + "' returning " + column.getColumnDefinition() + ')' ); case DATE: return template.replace( @@ -189,10 +190,10 @@ public class OracleAggregateSupport extends AggregateSupportImpl { // We encode binary data as hex, so we have to decode here return template.replace( placeholder, - "(select * from json_table(" + aggregateParentReadExpression + ",'$' columns (" + columnExpression + " " + column.getTypeName() + " path '$." + columnExpression + "')))" + "(select * from json_table(" + aggregateParentReadExpression + ",'$' columns (" + columnExpression + " " + column.getColumnDefinition() + " path '$." + columnExpression + "')))" ); case ARRAY: - final BasicPluralType pluralType = (BasicPluralType) column.getValue().getType(); + final BasicPluralType pluralType = (BasicPluralType) column.getJdbcMapping(); final OracleArrayJdbcType jdbcType = (OracleArrayJdbcType) pluralType.getJdbcType(); switch ( jdbcType.getElementJdbcType().getDefaultSqlTypeCode() ) { case BOOLEAN: @@ -211,7 +212,7 @@ public class OracleAggregateSupport extends AggregateSupportImpl { default: return template.replace( placeholder, - "json_value(" + parentPartExpression + columnExpression + "' returning " + column.getTypeName() + ')' + "json_value(" + parentPartExpression + columnExpression + "' returning " + column.getColumnDefinition() + ')' ); } case JSON: @@ -222,7 +223,7 @@ public class OracleAggregateSupport extends AggregateSupportImpl { default: return template.replace( placeholder, - "cast(json_value(" + parentPartExpression + columnExpression + "') as " + column.getTypeName() + ')' + "cast(json_value(" + parentPartExpression + columnExpression + "') as " + column.getColumnDefinition() + ')' ); } case NONE: @@ -233,7 +234,7 @@ public class OracleAggregateSupport extends AggregateSupportImpl { case STRUCT_TABLE: return template.replace( placeholder, aggregateParentReadExpression + "." + columnExpression ); } - throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumn.getTypeCode() ); + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java index 0f469b2048..061132400f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/PostgreSQLAggregateSupport.java @@ -14,6 +14,7 @@ import org.hibernate.mapping.Column; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; @@ -51,12 +52,12 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { String placeholder, String aggregateParentReadExpression, String columnExpression, - AggregateColumn aggregateColumn, - Column column) { - switch ( aggregateColumn.getTypeCode() ) { + int aggregateColumnTypeCode, + SqlTypedMapping column) { + switch ( aggregateColumnTypeCode ) { case JSON_ARRAY: case JSON: - switch ( column.getTypeCode() ) { + switch ( column.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode() ) { case JSON: return template.replace( placeholder, @@ -71,7 +72,7 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { "decode(" + aggregateParentReadExpression + "->>'" + columnExpression + "','hex')" ); case ARRAY: - final BasicPluralType pluralType = (BasicPluralType) column.getValue().getType(); + final BasicPluralType pluralType = (BasicPluralType) column.getJdbcMapping(); switch ( pluralType.getElementType().getJdbcType().getDefaultSqlTypeCode() ) { case BOOLEAN: case TINYINT: @@ -85,7 +86,7 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { // because casting a jsonb[] to text[] will not omit the quotes of the jsonb text values return template.replace( placeholder, - "cast(array(select jsonb_array_elements(" + aggregateParentReadExpression + "->'" + columnExpression + "')) as " + column.getTypeName() + ')' + "cast(array(select jsonb_array_elements(" + aggregateParentReadExpression + "->'" + columnExpression + "')) as " + column.getColumnDefinition() + ')' ); case BINARY: case VARBINARY: @@ -98,13 +99,13 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { default: return template.replace( placeholder, - "cast(array(select jsonb_array_elements_text(" + aggregateParentReadExpression + "->'" + columnExpression + "')) as " + column.getTypeName() + ')' + "cast(array(select jsonb_array_elements_text(" + aggregateParentReadExpression + "->'" + columnExpression + "')) as " + column.getColumnDefinition() + ')' ); } default: return template.replace( placeholder, - "cast(" + aggregateParentReadExpression + "->>'" + columnExpression + "' as " + column.getTypeName() + ')' + "cast(" + aggregateParentReadExpression + "->>'" + columnExpression + "' as " + column.getColumnDefinition() + ')' ); } case STRUCT: @@ -112,7 +113,7 @@ public class PostgreSQLAggregateSupport extends AggregateSupportImpl { case STRUCT_TABLE: return template.replace( placeholder, '(' + aggregateParentReadExpression + ")." + columnExpression ); } - throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumn.getTypeCode() ); + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); } private static String jsonCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 11752fc442..971389b0db 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -7,75 +7,11 @@ package org.hibernate.dialect.function; import java.util.Date; import java.util.Arrays; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.function.array.ArrayAggFunction; -import org.hibernate.dialect.function.array.ArrayAndElementArgumentTypeResolver; -import org.hibernate.dialect.function.array.ArrayAndElementArgumentValidator; -import org.hibernate.dialect.function.array.ArrayArgumentValidator; -import org.hibernate.dialect.function.array.ArrayConcatElementFunction; -import org.hibernate.dialect.function.array.ArrayConcatFunction; -import org.hibernate.dialect.function.array.ArrayConstructorFunction; -import org.hibernate.dialect.function.array.ArrayContainsOperatorFunction; -import org.hibernate.dialect.function.array.ArrayContainsUnnestFunction; -import org.hibernate.dialect.function.array.ArrayIncludesOperatorFunction; -import org.hibernate.dialect.function.array.ArrayIncludesUnnestFunction; -import org.hibernate.dialect.function.array.ArrayIntersectsOperatorFunction; -import org.hibernate.dialect.function.array.ArrayIntersectsUnnestFunction; -import org.hibernate.dialect.function.array.ArrayGetUnnestFunction; -import org.hibernate.dialect.function.array.ArrayRemoveIndexUnnestFunction; -import org.hibernate.dialect.function.array.ArrayReplaceUnnestFunction; -import org.hibernate.dialect.function.array.ArraySetUnnestFunction; -import org.hibernate.dialect.function.array.ArraySliceUnnestFunction; -import org.hibernate.dialect.function.array.ArrayToStringFunction; -import org.hibernate.dialect.function.array.ArrayViaArgumentReturnTypeResolver; -import org.hibernate.dialect.function.array.CockroachArrayFillFunction; -import org.hibernate.dialect.function.array.ElementViaArrayArgumentReturnTypeResolver; -import org.hibernate.dialect.function.array.H2ArrayContainsFunction; -import org.hibernate.dialect.function.array.H2ArrayFillFunction; -import org.hibernate.dialect.function.array.H2ArrayIncludesFunction; -import org.hibernate.dialect.function.array.H2ArrayIntersectsFunction; -import org.hibernate.dialect.function.array.H2ArrayPositionFunction; -import org.hibernate.dialect.function.array.H2ArrayPositionsFunction; -import org.hibernate.dialect.function.array.H2ArrayRemoveFunction; -import org.hibernate.dialect.function.array.H2ArrayRemoveIndexFunction; -import org.hibernate.dialect.function.array.H2ArrayReplaceFunction; -import org.hibernate.dialect.function.array.H2ArraySetFunction; -import org.hibernate.dialect.function.array.H2ArrayToStringFunction; -import org.hibernate.dialect.function.array.HSQLArrayConstructorFunction; -import org.hibernate.dialect.function.array.HSQLArrayFillFunction; -import org.hibernate.dialect.function.array.HSQLArrayPositionFunction; -import org.hibernate.dialect.function.array.HSQLArrayPositionsFunction; -import org.hibernate.dialect.function.array.HSQLArrayRemoveFunction; -import org.hibernate.dialect.function.array.HSQLArraySetFunction; -import org.hibernate.dialect.function.array.HSQLArrayToStringFunction; -import org.hibernate.dialect.function.array.OracleArrayConcatElementFunction; -import org.hibernate.dialect.function.array.OracleArrayConcatFunction; -import org.hibernate.dialect.function.array.OracleArrayFillFunction; -import org.hibernate.dialect.function.array.OracleArrayIncludesFunction; -import org.hibernate.dialect.function.array.OracleArrayIntersectsFunction; -import org.hibernate.dialect.function.array.OracleArrayGetFunction; -import org.hibernate.dialect.function.array.OracleArrayLengthFunction; -import org.hibernate.dialect.function.array.OracleArrayPositionFunction; -import org.hibernate.dialect.function.array.OracleArrayPositionsFunction; -import org.hibernate.dialect.function.array.OracleArrayRemoveFunction; -import org.hibernate.dialect.function.array.OracleArrayRemoveIndexFunction; -import org.hibernate.dialect.function.array.OracleArrayReplaceFunction; -import org.hibernate.dialect.function.array.OracleArraySetFunction; -import org.hibernate.dialect.function.array.OracleArraySliceFunction; -import org.hibernate.dialect.function.array.OracleArrayToStringFunction; -import org.hibernate.dialect.function.array.OracleArrayTrimFunction; -import org.hibernate.dialect.function.array.PostgreSQLArrayConcatElementFunction; -import org.hibernate.dialect.function.array.PostgreSQLArrayConcatFunction; -import org.hibernate.dialect.function.array.PostgreSQLArrayFillFunction; -import org.hibernate.dialect.function.array.PostgreSQLArrayPositionFunction; -import org.hibernate.dialect.function.array.PostgreSQLArrayConstructorFunction; -import org.hibernate.dialect.function.array.OracleArrayAggEmulation; -import org.hibernate.dialect.function.array.OracleArrayConstructorFunction; -import org.hibernate.dialect.function.array.OracleArrayContainsFunction; -import org.hibernate.dialect.function.array.PostgreSQLArrayPositionsFunction; -import org.hibernate.dialect.function.array.PostgreSQLArrayTrimEmulation; +import org.hibernate.dialect.function.array.*; import org.hibernate.dialect.function.json.CockroachDBJsonExistsFunction; import org.hibernate.dialect.function.json.CockroachDBJsonQueryFunction; import org.hibernate.dialect.function.json.CockroachDBJsonRemoveFunction; @@ -4294,4 +4230,61 @@ public class CommonFunctionFactory { public void xmlagg_sqlserver() { functionRegistry.register( "xmlagg", new SQLServerXmlAggFunction( typeConfiguration ) ); } + + /** + * Standard unnest() function + */ + public void unnest(@Nullable String defaultBasicArrayElementColumnName, String defaultIndexSelectionExpression) { + functionRegistry.register( "unnest", new UnnestFunction( defaultBasicArrayElementColumnName, defaultIndexSelectionExpression ) ); + } + + /** + * Standard unnest() function for databases that don't support arrays natively + */ + public void unnest_emulated() { + // Pass an arbitrary value + unnest( "v", "i" ); + } + + /** + * H2 unnest() function + */ + public void unnest_h2(int maxArraySize) { + functionRegistry.register( "unnest", new H2UnnestFunction( maxArraySize ) ); + } + + /** + * Oracle unnest() function + */ + public void unnest_oracle() { + functionRegistry.register( "unnest", new OracleUnnestFunction() ); + } + + /** + * PostgreSQL unnest() function + */ + public void unnest_postgresql() { + functionRegistry.register( "unnest", new PostgreSQLUnnestFunction() ); + } + + /** + * SQL Server unnest() function + */ + public void unnest_sqlserver() { + functionRegistry.register( "unnest", new SQLServerUnnestFunction() ); + } + + /** + * Sybase ASE unnest() function + */ + public void unnest_sybasease() { + functionRegistry.register( "unnest", new SybaseASEUnnestFunction() ); + } + + /** + * HANA unnest() function + */ + public void unnest_hana() { + functionRegistry.register( "unnest", new HANAUnnestFunction() ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java new file mode 100644 index 0000000000..d02c15e190 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/UnnestSetReturningFunctionTypeResolver.java @@ -0,0 +1,222 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function; + +import java.util.ArrayList; +import java.util.List; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.metamodel.mapping.AttributeMapping; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * + * @since 7.0 + */ +public class UnnestSetReturningFunctionTypeResolver implements SetReturningFunctionTypeResolver { + + protected final @Nullable String defaultBasicArrayColumnName; + protected final String defaultIndexSelectionExpression; + + public UnnestSetReturningFunctionTypeResolver(@Nullable String defaultBasicArrayColumnName, String defaultIndexSelectionExpression) { + this.defaultBasicArrayColumnName = defaultBasicArrayColumnName; + this.defaultIndexSelectionExpression = defaultIndexSelectionExpression; + } + + @Override + public AnonymousTupleType resolveTupleType(List> arguments, TypeConfiguration typeConfiguration) { + final SqmTypedNode arrayArgument = arguments.get( 0 ); + final SqmExpressible expressible = arrayArgument.getExpressible(); + if ( expressible == null ) { + throw new IllegalArgumentException( "Couldn't determine array type of argument to function 'unnest'" ); + } + if ( !( expressible.getSqmType() instanceof BasicPluralType pluralType ) ) { + throw new IllegalArgumentException( "Argument passed to function 'unnest' is not a BasicPluralType. Found: " + expressible ); + } + + final BasicType elementType = pluralType.getElementType(); + final SqmExpressible[] componentTypes; + final String[] componentNames; + if ( elementType.getJdbcType() instanceof AggregateJdbcType aggregateJdbcType + && aggregateJdbcType.getEmbeddableMappingType() != null ) { + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); + componentTypes = determineComponentTypes( embeddableMappingType ); + componentNames = new String[componentTypes.length]; + final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); + int index = 0; + for ( int i = 0; i < numberOfAttributeMappings; i++ ) { + final AttributeMapping attributeMapping = embeddableMappingType.getAttributeMapping( i ); + if ( attributeMapping.getMappedType() instanceof SqmExpressible ) { + componentNames[index++] = attributeMapping.getAttributeName(); + } + } + assert index == componentNames.length - 1; + componentTypes[index] = typeConfiguration.getBasicTypeForJavaType( Long.class ); + componentNames[index] = CollectionPart.Nature.INDEX.getName(); + } + else { + componentTypes = new SqmExpressible[]{ elementType, typeConfiguration.getBasicTypeForJavaType( Long.class ) }; + componentNames = new String[]{ CollectionPart.Nature.ELEMENT.getName(), CollectionPart.Nature.INDEX.getName() }; + } + return new AnonymousTupleType<>( componentTypes, componentNames ); + } + + @Override + public SelectableMapping[] resolveFunctionReturnType( + List arguments, + String tableIdentifierVariable, + boolean withOrdinality, + TypeConfiguration typeConfiguration) { + final Expression expression = (Expression) arguments.get( 0 ); + final JdbcMappingContainer expressionType = expression.getExpressionType(); + if ( expressionType == null ) { + throw new IllegalArgumentException( "Couldn't determine array type of argument to function 'unnest'" ); + } + if ( !( expressionType.getSingleJdbcMapping() instanceof BasicPluralType pluralType ) ) { + throw new IllegalArgumentException( "Argument passed to function 'unnest' is not a BasicPluralType. Found: " + expressionType ); + } + + final SelectableMapping indexMapping = withOrdinality ? new SelectableMappingImpl( + "", + defaultIndexSelectionExpression, + new SelectablePath( CollectionPart.Nature.INDEX.getName() ), + null, + null, + null, + null, + null, + null, + null, + false, + false, + false, + false, + false, + false, + typeConfiguration.getBasicTypeForJavaType( Long.class ) + ) : null; + + final BasicType elementType = pluralType.getElementType(); + final SelectableMapping[] returnType; + if ( elementType.getJdbcType() instanceof AggregateJdbcType aggregateJdbcType + && aggregateJdbcType.getEmbeddableMappingType() != null ) { + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); + final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); + returnType = new SelectableMapping[jdbcValueCount + (indexMapping == null ? 0 : 1)]; + for ( int i = 0; i < jdbcValueCount; i++ ) { + final SelectableMapping selectableMapping = embeddableMappingType.getJdbcValueSelectable( i ); + final String selectableName = selectableMapping.getSelectableName(); + returnType[i] = new SelectableMappingImpl( + selectableMapping.getContainingTableExpression(), + selectableName, + new SelectablePath( selectableName ), + null, + null, + selectableMapping.getColumnDefinition(), + selectableMapping.getLength(), + selectableMapping.getPrecision(), + selectableMapping.getScale(), + selectableMapping.getTemporalPrecision(), + selectableMapping.isLob(), + true, + false, + false, + false, + selectableMapping.isFormula(), + selectableMapping.getJdbcMapping() + ); + if ( indexMapping != null ) { + returnType[jdbcValueCount] = indexMapping; + } + } + } + else { + final String elementSelectionExpression = defaultBasicArrayColumnName == null + ? tableIdentifierVariable + : defaultBasicArrayColumnName; + final SelectableMapping elementMapping; + if ( expressionType instanceof SqlTypedMapping typedMapping ) { + elementMapping = new SelectableMappingImpl( + "", + elementSelectionExpression, + new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), + null, + null, + typedMapping.getColumnDefinition(), + typedMapping.getLength(), + typedMapping.getPrecision(), + typedMapping.getScale(), + typedMapping.getTemporalPrecision(), + typedMapping.isLob(), + true, + false, + false, + false, + false, + elementType + ); + } + else { + elementMapping = new SelectableMappingImpl( + "", + elementSelectionExpression, + new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), + null, + null, + null, + null, + null, + null, + null, + false, + true, + false, + false, + false, + false, + elementType + ); + } + if ( indexMapping == null ) { + returnType = new SelectableMapping[]{ elementMapping }; + } + else { + returnType = new SelectableMapping[] {elementMapping, indexMapping}; + } + } + return returnType; + } + + private static SqmExpressible[] determineComponentTypes(EmbeddableMappingType embeddableMappingType) { + final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); + final ArrayList> expressibles = new ArrayList<>( numberOfAttributeMappings + 1 ); + + for ( int i = 0; i < numberOfAttributeMappings; i++ ) { + final AttributeMapping attributeMapping = embeddableMappingType.getAttributeMapping( i ); + final MappingType mappedType = attributeMapping.getMappedType(); + if ( mappedType instanceof SqmExpressible ) { + expressibles.add( (SqmExpressible) mappedType ); + } + } + return expressibles.toArray( new SqmExpressible[expressibles.size() + 1] ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java new file mode 100644 index 0000000000..83ce1278b5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2UnnestFunction.java @@ -0,0 +1,338 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.dialect.function.UnnestSetReturningFunctionTypeResolver; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.NullnessUtil; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.Template; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.from.FunctionTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; +import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 unnest function. + *

+ * H2 does not support "lateral" i.e. the use of a from node within another, + * but we can apply the same trick that we already applied everywhere else for H2, + * which is to join a sequence table to emulate array element rows + * and eliminate non-existing array elements by checking the index against array length. + * Finally, we rewrite the selection expressions to access the array by joined sequence index. + */ +public class H2UnnestFunction extends UnnestFunction { + + private final int maximumArraySize; + + public H2UnnestFunction(int maximumArraySize) { + super( new H2UnnestSetReturningFunctionTypeResolver() ); + this.maximumArraySize = maximumArraySize; + } + + @Override + protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression( + List> arguments, + QueryEngine queryEngine) { + //noinspection unchecked + return new SelfRenderingSqmSetReturningFunction<>( + this, + this, + arguments, + getArgumentsValidator(), + getSetReturningTypeResolver(), + (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), + queryEngine.getCriteriaBuilder(), + getName() + ) { + @Override + public TableGroup convertToSqlAst( + NavigablePath navigablePath, + String identifierVariable, + boolean lateral, + boolean canUseInnerJoins, + boolean withOrdinality, + SqmToSqlAstConverter walker) { + // Register a transformer that adds a join predicate "array_length(array) <= index" + final FunctionTableGroup functionTableGroup = (FunctionTableGroup) super.convertToSqlAst( + navigablePath, + identifierVariable, + lateral, + canUseInnerJoins, + withOrdinality, + walker + ); + //noinspection unchecked + final List sqlArguments = (List) functionTableGroup.getPrimaryTableReference() + .getFunctionExpression() + .getArguments(); + // Can only do this transformation if the argument is a column reference + final ColumnReference columnReference = ( (Expression) sqlArguments.get( 0 ) ).getColumnReference(); + if ( columnReference != null ) { + final String tableQualifier = columnReference.getQualifier(); + // Find the table group which the unnest argument refers to + final FromClauseAccess fromClauseAccess = walker.getFromClauseAccess(); + final TableGroup sourceTableGroup = + fromClauseAccess.findTableGroupByIdentificationVariable( tableQualifier ); + if ( sourceTableGroup != null ) { + // Register a query transformer to register a join predicate + walker.registerQueryTransformer( (cteContainer, querySpec, converter) -> { + final TableGroup parentTableGroup = querySpec.getFromClause().queryTableGroups( + tg -> tg.findTableGroupJoin( functionTableGroup ) == null ? null : tg + ); + final TableGroupJoin join = parentTableGroup.findTableGroupJoin( functionTableGroup ); + final BasicType integerType = walker.getCreationContext() + .getSessionFactory() + .getNodeBuilder() + .getIntegerType(); + final Expression lhs = new SelfRenderingExpression() { + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + sqlAppender.append( "coalesce(array_length(" ); + columnReference.accept( walker ); + sqlAppender.append( "),0)" ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return integerType; + } + }; + final Expression rhs = new ColumnReference( + functionTableGroup.getPrimaryTableReference().getIdentificationVariable(), + // The default column name for the system_range function + "x", + false, + null, + integerType + ); + join.applyPredicate( new ComparisonPredicate( lhs, ComparisonOperator.GREATER_THAN_OR_EQUAL, rhs ) ); + return querySpec; + } ); + } + } + return functionTableGroup; + } + }; + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + renderUnnest( sqlAppender, array, pluralType, sqlTypedMapping, tupleType, tableIdentifierVariable, walker ); + } + + @Override + protected void renderUnnest( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final ColumnReference columnReference = array.getColumnReference(); + if ( columnReference != null ) { + sqlAppender.append( "system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ")" ); + } + else { + super.renderUnnest( sqlAppender, array, pluralType, sqlTypedMapping, tupleType, tableIdentifierVariable, walker ); + } + } + + private static class H2UnnestSetReturningFunctionTypeResolver extends UnnestSetReturningFunctionTypeResolver { + + public H2UnnestSetReturningFunctionTypeResolver() { + // c1 is the default column name for the "unnest()" function + super( "c1", "nord" ); + } + + @Override + public SelectableMapping[] resolveFunctionReturnType( + List arguments, + String tableIdentifierVariable, + boolean withOrdinality, + TypeConfiguration typeConfiguration) { + final Expression expression = (Expression) arguments.get( 0 ); + final JdbcMappingContainer expressionType = expression.getExpressionType(); + if ( expressionType == null ) { + throw new IllegalArgumentException( "Couldn't determine array type of argument to function 'unnest'" ); + } + if ( !( expressionType.getSingleJdbcMapping() instanceof BasicPluralType pluralType ) ) { + throw new IllegalArgumentException( "Argument passed to function 'unnest' is not a BasicPluralType. Found: " + expressionType ); + } + + final SelectableMapping indexMapping = withOrdinality ? new SelectableMappingImpl( + "", + expression.getColumnReference() != null ? "x" : defaultIndexSelectionExpression, + new SelectablePath( CollectionPart.Nature.INDEX.getName() ), + null, + null, + null, + null, + null, + null, + null, + false, + false, + false, + false, + false, + false, + typeConfiguration.getBasicTypeForJavaType( Long.class ) + ) : null; + + final BasicType elementType = pluralType.getElementType(); + final SelectableMapping[] returnType; + if ( elementType.getJdbcType() instanceof AggregateJdbcType aggregateJdbcType + && aggregateJdbcType.getEmbeddableMappingType() != null ) { + final ColumnReference arrayColumnReference = expression.getColumnReference(); + if ( arrayColumnReference == null ) { + throw new IllegalArgumentException( "Argument passed to function 'unnest' is not a column reference, but an aggregate type, which is not yet supported." ); + } + // For column references we render an emulation through system_range(), + // so we need to render an array access to get to the element + final String elementReadExpression = "array_get(" + arrayColumnReference.getExpressionText() + "," + Template.TEMPLATE + ".x)"; + final String arrayReadExpression = NullnessUtil.castNonNull( arrayColumnReference.getReadExpression() ); + final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); + final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); + returnType = new SelectableMapping[jdbcValueCount + (indexMapping == null ? 0 : 1)]; + for ( int i = 0; i < jdbcValueCount; i++ ) { + final SelectableMapping selectableMapping = embeddableMappingType.getJdbcValueSelectable( i ); + // The array expression has to be replaced with the actual array_get read expression in this emulation + final String customReadExpression = selectableMapping.getCustomReadExpression() + .replace( arrayReadExpression, elementReadExpression ); + returnType[i] = new SelectableMappingImpl( + selectableMapping.getContainingTableExpression(), + selectableMapping.getSelectablePath().getSelectableName(), + new SelectablePath( selectableMapping.getSelectablePath().getSelectableName() ), + customReadExpression, + selectableMapping.getCustomWriteExpression(), + selectableMapping.getColumnDefinition(), + selectableMapping.getLength(), + selectableMapping.getPrecision(), + selectableMapping.getScale(), + selectableMapping.getTemporalPrecision(), + selectableMapping.isLob(), + true, + false, + false, + false, + selectableMapping.isFormula(), + selectableMapping.getJdbcMapping() + ); + } + if ( indexMapping != null ) { + returnType[jdbcValueCount] = indexMapping; + } + } + else { + final String elementSelectionExpression; + final String elementReadExpression; + final ColumnReference columnReference = expression.getColumnReference(); + if ( columnReference != null ) { + // For column references we render an emulation through system_range(), + // so we need to render an array access to get to the element + elementSelectionExpression = columnReference.getColumnExpression(); + elementReadExpression = "array_get(" + columnReference.getExpressionText() + "," + Template.TEMPLATE + ".x)"; + } + else { + elementSelectionExpression = defaultBasicArrayColumnName; + elementReadExpression = null; + } + final SelectableMapping elementMapping; + if ( expressionType instanceof SqlTypedMapping typedMapping ) { + elementMapping = new SelectableMappingImpl( + "", + elementSelectionExpression, + new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), + elementReadExpression, + null, + typedMapping.getColumnDefinition(), + typedMapping.getLength(), + typedMapping.getPrecision(), + typedMapping.getScale(), + typedMapping.getTemporalPrecision(), + typedMapping.isLob(), + true, + false, + false, + false, + false, + elementType + ); + } + else { + elementMapping = new SelectableMappingImpl( + "", + elementSelectionExpression, + new SelectablePath( CollectionPart.Nature.ELEMENT.getName() ), + elementReadExpression, + null, + null, + null, + null, + null, + null, + false, + true, + false, + false, + false, + false, + elementType + ); + } + if ( indexMapping == null ) { + returnType = new SelectableMapping[]{ elementMapping }; + } + else { + returnType = new SelectableMapping[] {elementMapping, indexMapping}; + } + } + return returnType; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java new file mode 100644 index 0000000000..6bcc43bdb7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HANAUnnestFunction.java @@ -0,0 +1,533 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.hibernate.QueryException; +import org.hibernate.dialect.XmlHelper; +import org.hibernate.dialect.function.json.ExpressionTypeHelper; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.EntityValuedModelPart; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.ModelPartContainer; +import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.ValuedModelPart; +import org.hibernate.metamodel.mapping.internal.EmbeddedCollectionPart; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.internal.ColumnQualifierCollectorSqlAstWalker; +import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.cte.CteColumn; +import org.hibernate.sql.ast.tree.cte.CteStatement; +import org.hibernate.sql.ast.tree.cte.CteTable; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.from.FunctionTableGroup; +import org.hibernate.sql.ast.tree.from.StandardTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableGroupJoin; +import org.hibernate.sql.ast.tree.from.TableGroupProducer; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.results.internal.SqlSelectionImpl; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.BasicType; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * HANA unnest function. + */ +public class HANAUnnestFunction extends UnnestFunction { + + public HANAUnnestFunction() { + super( "v", "i" ); + } + + @Override + protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression( + List> arguments, + QueryEngine queryEngine) { + //noinspection unchecked + return new SelfRenderingSqmSetReturningFunction<>( + this, + this, + arguments, + getArgumentsValidator(), + getSetReturningTypeResolver(), + (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), + queryEngine.getCriteriaBuilder(), + getName() + ) { + @Override + public TableGroup convertToSqlAst( + NavigablePath navigablePath, + String identifierVariable, + boolean lateral, + boolean canUseInnerJoins, + boolean withOrdinality, + SqmToSqlAstConverter walker) { + // SAP HANA only supports table column references i.e. `TABLE_NAME.COLUMN_NAME` + // or constants as arguments to xmltable/json_table, so it's impossible to do lateral joins. + // There is a nice trick we can apply to make this work though, which is to figure out + // the table group an expression belongs to and render a special CTE returning xml/json that can be joined. + // The xml/json of that CTE needs to be extended by table group primary key data, + // so we can join it later. + final FunctionTableGroup functionTableGroup = (FunctionTableGroup) super.convertToSqlAst( + navigablePath, + identifierVariable, + lateral, + canUseInnerJoins, + withOrdinality, + walker + ); + //noinspection unchecked + final List sqlArguments = (List) functionTableGroup.getPrimaryTableReference() + .getFunctionExpression() + .getArguments(); + final Expression argument = (Expression) sqlArguments.get( 0 ); + final Set qualifiers = ColumnQualifierCollectorSqlAstWalker.determineColumnQualifiers( argument ); + // Can only do this transformation if the argument contains a single column reference qualifier + if ( qualifiers.size() == 1 ) { + final String tableQualifier = qualifiers.iterator().next(); + // Find the table group which the unnest argument refers to + final FromClauseAccess fromClauseAccess = walker.getFromClauseAccess(); + final TableGroup sourceTableGroup = + fromClauseAccess.findTableGroupByIdentificationVariable( tableQualifier ); + if ( sourceTableGroup != null ) { + final List idColumns = new ArrayList<>(); + addIdColumns( sourceTableGroup.getModelPart(), idColumns ); + + // Register a query transformer to register the CTE and rewrite the array argument + walker.registerQueryTransformer( (cteContainer, querySpec, converter) -> { + // Determine a CTE name that is available + final String baseName = "_data"; + String cteName; + int index = 0; + do { + cteName = baseName + ( index++ ); + } while ( cteContainer.getCteStatement( cteName ) != null ); + + final TableGroup parentTableGroup = querySpec.getFromClause().queryTableGroups( + tg -> tg.findTableGroupJoin( functionTableGroup ) == null ? null : tg + ); + final TableGroupJoin join = parentTableGroup.findTableGroupJoin( functionTableGroup ); + final Expression lhs = createExpression( tableQualifier, idColumns ); + final Expression rhs = createExpression( + functionTableGroup.getPrimaryTableReference().getIdentificationVariable(), + idColumns + ); + join.applyPredicate( new ComparisonPredicate( lhs, ComparisonOperator.EQUAL, rhs ) ); + + final String tableName = cteName; + final List cteColumns = List.of( + new CteColumn( "v", argument.getExpressionType().getSingleJdbcMapping() ) + ); + final QuerySpec cteQuery = new QuerySpec( false ); + cteQuery.getFromClause().addRoot( + new StandardTableGroup( + true, + sourceTableGroup.getNavigablePath(), + (TableGroupProducer) sourceTableGroup.getModelPart(), + false, + null, + sourceTableGroup.findTableReference( tableQualifier ), + false, + null, + joinTableName -> false, + (joinTableName, tg) -> null, + null + ) + ); + final Expression wrapperExpression; + if ( ExpressionTypeHelper.isXml( argument ) ) { + wrapperExpression = new XmlWrapperExpression( idColumns, tableQualifier, argument ); + // xmltable is allergic to null values and produces no result if one occurs, + // so we must filter them out + cteQuery.applyPredicate( new NullnessPredicate( argument, true ) ); + } + else { + wrapperExpression = new JsonWrapperExpression( idColumns, tableQualifier, argument ); + } + cteQuery.getSelectClause().addSqlSelection( new SqlSelectionImpl( wrapperExpression ) ); + cteContainer.addCteStatement( new CteStatement( + new CteTable( tableName, cteColumns ), + new SelectStatement( cteQuery ) + ) ); + sqlArguments.set( 0, new TableColumnReferenceExpression( argument, tableName, idColumns ) ); + return querySpec; + } ); + } + } + return functionTableGroup; + } + + private Expression createExpression(String qualifier, List idColumns) { + if ( idColumns.size() == 1 ) { + final ColumnInfo columnInfo = idColumns.get( 0 ); + return new ColumnReference( qualifier, columnInfo.name(), false, null, columnInfo.jdbcMapping() ); + } + else { + final ArrayList expressions = new ArrayList<>( idColumns.size() ); + for ( ColumnInfo columnInfo : idColumns ) { + expressions.add( + new ColumnReference( + qualifier, + columnInfo.name(), + false, + null, + columnInfo.jdbcMapping() + ) + ); + } + return new SqlTuple( expressions, null ); + } + } + + private void addIdColumns(ModelPartContainer modelPartContainer, List idColumns) { + if ( modelPartContainer instanceof EntityValuedModelPart entityValuedModelPart ) { + addIdColumns( entityValuedModelPart.getEntityMappingType(), idColumns ); + } + else if ( modelPartContainer instanceof PluralAttributeMapping pluralAttributeMapping ) { + addIdColumns( pluralAttributeMapping, idColumns ); + } + else if ( modelPartContainer instanceof EmbeddableValuedModelPart embeddableModelPart ) { + addIdColumns( embeddableModelPart, idColumns ); + } + else { + throw new QueryException( "Unsupported model part container: " + modelPartContainer ); + } + } + + private void addIdColumns(EmbeddableValuedModelPart embeddableModelPart, List idColumns) { + if ( embeddableModelPart instanceof EmbeddedCollectionPart collectionPart ) { + addIdColumns( collectionPart.getCollectionAttribute(), idColumns ); + } + else { + addIdColumns( embeddableModelPart.asAttributeMapping().getDeclaringType(), idColumns ); + } + } + + private void addIdColumns(PluralAttributeMapping pluralAttributeMapping, List idColumns) { + final DdlTypeRegistry ddlTypeRegistry = pluralAttributeMapping.getCollectionDescriptor() + .getFactory() + .getTypeConfiguration() + .getDdlTypeRegistry(); + addIdColumns( pluralAttributeMapping.getKeyDescriptor().getKeyPart(), ddlTypeRegistry, idColumns ); + } + + private void addIdColumns(EntityMappingType entityMappingType, List idColumns) { + final DdlTypeRegistry ddlTypeRegistry = entityMappingType.getEntityPersister() + .getFactory() + .getTypeConfiguration() + .getDdlTypeRegistry(); + addIdColumns( entityMappingType.getIdentifierMapping(), ddlTypeRegistry, idColumns ); + } + + private void addIdColumns( + ValuedModelPart modelPart, + DdlTypeRegistry ddlTypeRegistry, + List idColumns) { + modelPart.forEachSelectable( (selectionIndex, selectableMapping) -> { + final JdbcMapping jdbcMapping = selectableMapping.getJdbcMapping().getSingleJdbcMapping(); + idColumns.add( new ColumnInfo( + selectableMapping.getSelectionExpression(), + jdbcMapping, + ddlTypeRegistry.getTypeName( + jdbcMapping.getJdbcType().getDefaultSqlTypeCode(), + selectableMapping.toSize(), + (Type) jdbcMapping + ) + ) ); + } ); + } + + }; + } + + record ColumnInfo(String name, JdbcMapping jdbcMapping, String ddlType) {} + + static class TableColumnReferenceExpression implements SelfRenderingExpression { + + private final Expression argument; + private final String tableName; + private final List idColumns; + + public TableColumnReferenceExpression(Expression argument, String tableName, List idColumns) { + this.argument = argument; + this.tableName = tableName; + this.idColumns = idColumns; + } + + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + sqlAppender.appendSql( tableName ); + sqlAppender.appendSql( ".v" ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return argument.getExpressionType(); + } + + public List getIdColumns() { + return idColumns; + } + } + + @Override + protected void renderXmlTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final XmlHelper.CollectionTags collectionTags = XmlHelper.determineCollectionTags( + (BasicPluralJavaType) pluralType.getJavaTypeDescriptor(), walker.getSessionFactory() + ); + + sqlAppender.appendSql( "xmltable('/" ); + sqlAppender.appendSql( collectionTags.rootName() ); + sqlAppender.appendSql( '/' ); + sqlAppender.appendSql( collectionTags.elementName() ); + sqlAppender.appendSql( "' passing " ); + array.accept( walker ); + sqlAppender.appendSql( " columns" ); + char separator = ' '; + final int offset; + if ( array instanceof TableColumnReferenceExpression expression ) { + offset = expression.getIdColumns().size(); + for ( ColumnInfo columnInfo : expression.getIdColumns() ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( columnInfo.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( columnInfo.ddlType() ); + sqlAppender.appendSql( " path 'ancestor::" ); + sqlAppender.appendSql( collectionTags.rootName() ); + sqlAppender.appendSql( "/@" ); + sqlAppender.appendSql( columnInfo.name() ); + sqlAppender.appendSql( '\'' ); + separator = ','; + } + } + else { + offset = 0; + } + if ( tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ) == null ) { + tupleType.forEachSelectable( offset, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '" ); + sqlAppender.appendSql( selectableMapping.getSelectableName() ); + sqlAppender.appendSql( "'" ); + } + } ); + } + else { + tupleType.forEachSelectable( offset, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '" ); + sqlAppender.appendSql( "." ); + sqlAppender.appendSql( "'" ); + } + } ); + } + + sqlAppender.appendSql( ')' ); + } + + static class XmlWrapperExpression implements SelfRenderingExpression { + private final List idColumns; + private final String tableQualifier; + private final Expression argument; + + public XmlWrapperExpression(List idColumns, String tableQualifier, Expression argument) { + this.idColumns = idColumns; + this.tableQualifier = tableQualifier; + this.argument = argument; + } + + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + final BasicPluralType pluralType = (BasicPluralType) argument.getExpressionType().getSingleJdbcMapping(); + final XmlHelper.CollectionTags collectionTags = XmlHelper.determineCollectionTags( + (BasicPluralJavaType) pluralType.getJavaTypeDescriptor(), + sessionFactory + ); + + // Produce a XML string e.g. ... + // which will contain the original XML as well as id column information for correlation + sqlAppender.appendSql( "trim('/>' from (select" ); + char separator = ' '; + for ( ColumnInfo columnInfo : idColumns ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( tableQualifier ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( columnInfo.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendDoubleQuoteEscapedString( columnInfo.name() ); + separator = ','; + } + sqlAppender.appendSql( " from sys.dummy for xml('root'='no','columnstyle'='attribute','rowname'='Strings','format'='no')))||" ); + sqlAppender.appendSql( "substring(" ); + argument.accept( walker ); + sqlAppender.appendSql( ",locate('<" ); + sqlAppender.appendSql( collectionTags.rootName() ); + sqlAppender.appendSql( ">'," ); + argument.accept( walker ); + sqlAppender.appendSql( ")+" ); + sqlAppender.appendSql( collectionTags.rootName().length() + 2 ); + sqlAppender.appendSql( ",length(" ); + argument.accept( walker ); + sqlAppender.appendSql( "))" ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return argument.getExpressionType(); + } + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final BasicType elementType = pluralType.getElementType(); + final String columnType = walker.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry().getTypeName( + elementType.getJdbcType().getDdlTypeCode(), + sqlTypedMapping == null ? Size.nil() : sqlTypedMapping.toSize(), + elementType + ); + sqlAppender.appendSql( "json_table(" ); + array.accept( walker ); + + if ( array instanceof TableColumnReferenceExpression expression ) { + sqlAppender.appendSql( ",'$' columns(" ); + for ( ColumnInfo columnInfo : expression.getIdColumns() ) { + sqlAppender.appendSql( columnInfo.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( columnInfo.ddlType() ); + sqlAppender.appendSql( " path '$." ); + sqlAppender.appendSql( columnInfo.name() ); + sqlAppender.appendSql( "'," ); + } + + sqlAppender.appendSql( "nested path '$.v' columns (" ); + sqlAppender.append( tupleType.getColumnNames().get( 0 ) ); + sqlAppender.appendSql( ' ' ); + sqlAppender.append( columnType ); + sqlAppender.appendSql( " path '$')))" ); + } + else { + sqlAppender.appendSql( ",'$[*]' columns(" ); + sqlAppender.append( tupleType.getColumnNames().get( 0 ) ); + sqlAppender.appendSql( ' ' ); + sqlAppender.append( columnType ); + sqlAppender.appendSql( " path '$'))" ); + } + } + + static class JsonWrapperExpression implements SelfRenderingExpression { + private final List idColumns; + private final String tableQualifier; + private final Expression argument; + + public JsonWrapperExpression(List idColumns, String tableQualifier, Expression argument) { + this.idColumns = idColumns; + this.tableQualifier = tableQualifier; + this.argument = argument; + } + + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + // Produce a JSON string e.g. {"id":1,"v":[...]} + // which will contain the original JSON as well as id column information for correlation + sqlAppender.appendSql( "'{'||trim('{}' from (select" ); + char separator = ' '; + for ( ColumnInfo columnInfo : idColumns ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( tableQualifier ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( columnInfo.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendDoubleQuoteEscapedString( columnInfo.name() ); + separator = ','; + } + sqlAppender.appendSql( " from sys.dummy for json('arraywrap'='no')))||" ); + sqlAppender.appendSql( "'\"v\":'||" ); + argument.accept( walker ); + sqlAppender.appendSql( "||'}'" ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return argument.getExpressionType(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java new file mode 100644 index 0000000000..3cd41f4269 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleUnnestFunction.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Oracle unnest function. + */ +public class OracleUnnestFunction extends UnnestFunction { + + public OracleUnnestFunction() { + super( "column_value", "i" ); + } + + @Override + protected void renderUnnest( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final ModelPart ordinalitySubPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + final boolean withOrdinality = ordinalitySubPart != null; + if ( withOrdinality ) { + sqlAppender.appendSql( "lateral (select t.*, rownum " ); + sqlAppender.appendSql( ordinalitySubPart.asBasicValuedModelPart().getSelectionExpression() ); + sqlAppender.appendSql( " from " ); + } + sqlAppender.appendSql( "table(" ); + array.accept( walker ); + sqlAppender.appendSql( ")" ); + if ( withOrdinality ) { + sqlAppender.appendSql( " t)" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java new file mode 100644 index 0000000000..575e19aac7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/PostgreSQLUnnestFunction.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.dialect.aggregate.AggregateSupport; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.SqlTypes; + + +/** + * PostgreSQL unnest function. + */ +public class PostgreSQLUnnestFunction extends UnnestFunction { + + public PostgreSQLUnnestFunction() { + super( null, "ordinality" ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final AggregateSupport aggregateSupport = walker.getSessionFactory().getJdbcServices().getDialect() + .getAggregateSupport(); + sqlAppender.appendSql( "(select" ); + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.appendSql( "t.ordinality" ); + } + else { + sqlAppender.append( aggregateSupport.aggregateComponentCustomReadExpression( + "", + "", + "t.value", + selectableMapping.getSelectableName(), + SqlTypes.JSON, + selectableMapping + ) ); + } + sqlAppender.append( ' ' ); + sqlAppender.append( selectableMapping.getSelectionExpression() ); + } ); + sqlAppender.appendSql( " from jsonb_array_elements(" ); + array.accept( walker ); + sqlAppender.appendSql( ')' ); + if ( tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ) != null ) { + sqlAppender.appendSql( " with ordinality" ); + } + sqlAppender.appendSql( " t)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java new file mode 100644 index 0000000000..c1f2f385c9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SQLServerUnnestFunction.java @@ -0,0 +1,162 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + + +import org.hibernate.dialect.XmlHelper; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * SQL Server unnest function. + */ +public class SQLServerUnnestFunction extends UnnestFunction { + + public SQLServerUnnestFunction() { + super( "v", "i" ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + sqlAppender.appendSql( "openjson(" ); + array.accept( walker ); + sqlAppender.appendSql( ",'$[*]') with (" ); + + if ( tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ) == null ) { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( selectableMapping.getSelectionExpression() ); + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( selectableMapping.getSelectionExpression() ); + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '$." ); + sqlAppender.append( selectableMapping.getSelectableName() ); + sqlAppender.appendSql( '\'' ); + } + } ); + } + else { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( selectableMapping.getSelectionExpression() ); + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( selectableMapping.getSelectionExpression() ); + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '$'" ); + } + } ); + } + + sqlAppender.appendSql( ')' ); + } + + @Override + protected void renderXmlTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final XmlHelper.CollectionTags collectionTags = XmlHelper.determineCollectionTags( + (BasicPluralJavaType) pluralType.getJavaTypeDescriptor(), walker.getSessionFactory() + ); + + sqlAppender.appendSql( "(select" ); + + if ( tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ) == null ) { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.appendSql( "t.v.value('count(for $a in . return $a/../" ); + sqlAppender.appendSql( collectionTags.elementName() ); + sqlAppender.appendSql( "[.<<$a])+1','" ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( "') " ); + sqlAppender.appendSql( selectableMapping.getSelectionExpression() ); + } + else { + sqlAppender.appendSql( "t.v.value('"); + sqlAppender.appendSql( selectableMapping.getSelectableName() ); + sqlAppender.appendSql( "/text()[1]','" ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( "') " ); + sqlAppender.appendSql( selectableMapping.getSelectionExpression() ); + } + } ); + } + else { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.appendSql( "t.v.value('count(for $a in . return $a/../" ); + sqlAppender.appendSql( collectionTags.elementName() ); + sqlAppender.appendSql( "[.<<$a])+1','" ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( "') " ); + sqlAppender.appendSql( selectableMapping.getSelectionExpression() ); + } + else { + sqlAppender.appendSql( "t.v.value('text()[1]','" ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( "') " ); + sqlAppender.appendSql( selectableMapping.getSelectionExpression() ); + } + } ); + } + + sqlAppender.appendSql( " from " ); + array.accept( walker ); + sqlAppender.appendSql( ".nodes('/" ); + sqlAppender.appendSql( collectionTags.rootName() ); + sqlAppender.appendSql( '/' ); + sqlAppender.appendSql( collectionTags.elementName() ); + sqlAppender.appendSql( "') t(v))" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java new file mode 100644 index 0000000000..ff7da74a94 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/SybaseASEUnnestFunction.java @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import org.hibernate.dialect.XmlHelper; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Sybase ASE unnest function. + */ +public class SybaseASEUnnestFunction extends UnnestFunction { + + public SybaseASEUnnestFunction() { + super( "v", "i" ); + } + + @Override + protected void renderXmlTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final XmlHelper.CollectionTags collectionTags = XmlHelper.determineCollectionTags( + (BasicPluralJavaType) pluralType.getJavaTypeDescriptor(), walker.getSessionFactory() + ); + sqlAppender.appendSql( "xmltable('/" ); + sqlAppender.appendSql( collectionTags.rootName() ); + sqlAppender.appendSql( '/' ); + sqlAppender.appendSql( collectionTags.elementName() ); + sqlAppender.appendSql( "' passing " ); + array.accept( walker ); + sqlAppender.appendSql( " columns" ); + + if ( tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ) == null ) { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " bigint for ordinality" ); + } + else { + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '" ); + sqlAppender.appendSql( selectableMapping.getSelectableName() ); + sqlAppender.appendSql( "'" ); + } + } ); + } + else { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " bigint for ordinality" ); + } + else { + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '" ); + sqlAppender.appendSql( "." ); + sqlAppender.appendSql( "'" ); + } + } ); + } + + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java new file mode 100644 index 0000000000..c70caf7aa1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/UnnestFunction.java @@ -0,0 +1,216 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.array; + +import java.util.List; + +import org.hibernate.dialect.XmlHelper; +import org.hibernate.dialect.function.UnnestSetReturningFunctionTypeResolver; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingSetReturningFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Standard unnest function. + */ +public class UnnestFunction extends AbstractSqmSelfRenderingSetReturningFunctionDescriptor { + + public UnnestFunction(@Nullable String defaultBasicArrayColumnName, String defaultIndexSelectionExpression) { + this( new UnnestSetReturningFunctionTypeResolver( defaultBasicArrayColumnName, defaultIndexSelectionExpression ) ); + } + + protected UnnestFunction(SetReturningFunctionTypeResolver setReturningFunctionTypeResolver) { + super( + "unnest", + null, + setReturningFunctionTypeResolver, + null + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final Expression array = (Expression) sqlAstArguments.get( 0 ); + final @Nullable SqlTypedMapping sqlTypedMapping = array.getExpressionType() instanceof SqlTypedMapping + ? (SqlTypedMapping) array.getExpressionType() + : null; + final BasicPluralType pluralType = (BasicPluralType) array.getExpressionType().getSingleJdbcMapping(); + final int ddlTypeCode = pluralType.getJdbcType().getDefaultSqlTypeCode(); + if ( ddlTypeCode == SqlTypes.JSON_ARRAY ) { + renderJsonTable( sqlAppender, array, pluralType, sqlTypedMapping, tupleType, tableIdentifierVariable, walker ); + } + else if ( ddlTypeCode == SqlTypes.XML_ARRAY ) { + renderXmlTable( sqlAppender, array, pluralType, sqlTypedMapping, tupleType, tableIdentifierVariable, walker ); + } + else { + renderUnnest( sqlAppender, array, pluralType, sqlTypedMapping, tupleType, tableIdentifierVariable, walker ); + } + } + + protected String getDdlType(SqlTypedMapping sqlTypedMapping, SqlAstTranslator translator) { + final String columnDefinition = sqlTypedMapping.getColumnDefinition(); + if ( columnDefinition != null ) { + return columnDefinition; + } + return translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry().getTypeName( + sqlTypedMapping.getJdbcMapping().getJdbcType().getDdlTypeCode(), + sqlTypedMapping.toSize(), + (Type) sqlTypedMapping.getJdbcMapping() + ); + } + + protected void renderJsonTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_table(" ); + array.accept( walker ); + sqlAppender.appendSql( ",'$[*]' columns(" ); + if ( tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ) == null ) { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + sqlAppender.append( ' ' ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '$." ); + sqlAppender.append( selectableMapping.getSelectableName() ); + sqlAppender.appendSql( '\'' ); + } + } ); + } + else { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '$'" ); + } + } ); + } + sqlAppender.appendSql( "))" ); + } + + protected void renderXmlTable( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + final XmlHelper.CollectionTags collectionTags = XmlHelper.determineCollectionTags( + (BasicPluralJavaType) pluralType.getJavaTypeDescriptor(), walker.getSessionFactory() + ); + + sqlAppender.appendSql( "xmltable('$d/" ); + sqlAppender.appendSql( collectionTags.rootName() ); + sqlAppender.appendSql( '/' ); + sqlAppender.appendSql( collectionTags.elementName() ); + sqlAppender.appendSql( "' passing " ); + array.accept( walker ); + sqlAppender.appendSql( " as \"d\" columns" ); + if ( tupleType.findSubPart( CollectionPart.Nature.ELEMENT.getName(), null ) == null ) { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '" ); + sqlAppender.appendSql( selectableMapping.getSelectableName() ); + sqlAppender.appendSql( "/text()" ); + sqlAppender.appendSql( "'" ); + } + } ); + } + else { + tupleType.forEachSelectable( 0, (selectionIndex, selectableMapping) -> { + if ( selectionIndex == 0 ) { + sqlAppender.append( ' ' ); + } + else { + sqlAppender.append( ',' ); + } + sqlAppender.append( selectableMapping.getSelectionExpression() ); + if ( CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + sqlAppender.append( " for ordinality" ); + } + else { + sqlAppender.append( ' ' ); + sqlAppender.append( getDdlType( selectableMapping, walker ) ); + sqlAppender.appendSql( " path '" ); + sqlAppender.appendSql( "text()" ); + sqlAppender.appendSql( "'" ); + } + } ); + } + + sqlAppender.appendSql( ')' ); + } + + protected void renderUnnest( + SqlAppender sqlAppender, + Expression array, + BasicPluralType pluralType, + @Nullable SqlTypedMapping sqlTypedMapping, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + sqlAppender.appendSql( "unnest(" ); + array.accept( walker ); + sqlAppender.appendSql( ')' ); + if ( tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ) != null ) { + sqlAppender.append( " with ordinality" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java index 66dbc8c971..7d75302a88 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java @@ -38,7 +38,7 @@ public class JsonArrayAggFunction extends AbstractSqmSelfRenderingFunctionDescri FunctionKind.ORDERED_SET_AGGREGATE, StandardArgumentsValidators.between( 1, 2 ), StandardFunctionReturnTypeResolvers.invariant( - typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON_ARRAY ) + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) ), null ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java index 230af1419b..373dbfef3a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java @@ -28,7 +28,7 @@ public class JsonArrayFunction extends AbstractSqmSelfRenderingFunctionDescripto FunctionKind.NORMAL, null, StandardFunctionReturnTypeResolvers.invariant( - typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON_ARRAY ) + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) ), null ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java index 125740f723..938d4393bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/SQLServerXmlAggFunction.java @@ -119,8 +119,10 @@ public class SQLServerXmlAggFunction extends XmlAggFunction { ), alias, List.of("v"), + Set.of(), true, true, + false, null ); tableGroup.addTableGroupJoin( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java index 826ae02065..83f7332fe0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlElementFunction.java @@ -7,6 +7,7 @@ package org.hibernate.dialect.function.xml; import java.util.List; import java.util.Map; +import org.hibernate.dialect.XmlHelper; import org.hibernate.query.ReturnableType; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; @@ -33,8 +34,6 @@ import org.hibernate.type.spi.TypeConfiguration; import org.checkerframework.checker.nullness.qual.Nullable; -import static java.lang.Character.isLetter; -import static java.lang.Character.isLetterOrDigit; import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; /** @@ -56,7 +55,7 @@ public class XmlElementFunction extends AbstractSqmSelfRenderingFunctionDescript TypeConfiguration typeConfiguration) { //noinspection unchecked final String elementName = ( (SqmLiteral) arguments.get( 0 ) ).getLiteralValue(); - if ( !isValidXmlName( elementName ) ) { + if ( !XmlHelper.isValidXmlName( elementName ) ) { throw new FunctionArgumentException( String.format( "Invalid XML element name passed to 'xmlelement()': %s", @@ -68,7 +67,7 @@ public class XmlElementFunction extends AbstractSqmSelfRenderingFunctionDescript && arguments.get( 1 ) instanceof SqmXmlAttributesExpression attributesExpression ) { final Map> attributes = attributesExpression.getAttributes(); for ( Map.Entry> entry : attributes.entrySet() ) { - if ( !isValidXmlName( entry.getKey() ) ) { + if ( !XmlHelper.isValidXmlName( entry.getKey() ) ) { throw new FunctionArgumentException( String.format( "Invalid XML attribute name passed to 'xmlattributes()': %s", @@ -79,29 +78,6 @@ public class XmlElementFunction extends AbstractSqmSelfRenderingFunctionDescript } } } - - private static boolean isValidXmlName(String name) { - if ( name.isEmpty() - || !isValidXmlNameStart( name.charAt( 0 ) ) - || name.regionMatches( true, 0, "xml", 0, 3 ) ) { - return false; - } - for ( int i = 1; i < name.length(); i++ ) { - if ( !isValidXmlNameChar( name.charAt( i ) ) ) { - return false; - } - } - return true; - } - - private static boolean isValidXmlNameStart(char c) { - return isLetter( c ) || c == '_' || c == ':'; - } - - private static boolean isValidXmlNameChar(char c) { - return isLetterOrDigit( c ) || c == '_' || c == ':' || c == '-' || c == '.'; - } - } ), StandardFunctionReturnTypeResolvers.invariant( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlForestFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlForestFunction.java index 7a081c5330..32a1142890 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlForestFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/xml/XmlForestFunction.java @@ -6,6 +6,7 @@ package org.hibernate.dialect.function.xml; import java.util.List; +import org.hibernate.dialect.XmlHelper; import org.hibernate.query.ReturnableType; import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; import org.hibernate.query.sqm.function.FunctionKind; @@ -22,9 +23,6 @@ import org.hibernate.sql.ast.tree.expression.AliasedExpression; import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; -import static java.lang.Character.isLetter; -import static java.lang.Character.isLetterOrDigit; - /** * Standard xmlforest function. */ @@ -52,7 +50,7 @@ public class XmlForestFunction extends AbstractSqmSelfRenderingFunctionDescripto ) ); } - if ( !isValidXmlName( namedExpression.getName() ) ) { + if ( !XmlHelper.isValidXmlName( namedExpression.getName() ) ) { throw new FunctionArgumentException( String.format( "Invalid XML element name passed to 'xmlforest()': %s", @@ -63,28 +61,6 @@ public class XmlForestFunction extends AbstractSqmSelfRenderingFunctionDescripto } } - private static boolean isValidXmlName(String name) { - if ( name.isEmpty() - || !isValidXmlNameStart( name.charAt( 0 ) ) - || name.regionMatches( true, 0, "xml", 0, 3 ) ) { - return false; - } - for ( int i = 1; i < name.length(); i++ ) { - if ( !isValidXmlNameChar( name.charAt( i ) ) ) { - return false; - } - } - return true; - } - - private static boolean isValidXmlNameStart(char c) { - return isLetter( c ) || c == '_' || c == ':'; - } - - private static boolean isValidXmlNameChar(char c) { - return isLetterOrDigit( c ) || c == '_' || c == ':' || c == '-' || c == '.'; - } - } ), StandardFunctionReturnTypeResolvers.invariant( diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/LazySessionWrapperOptions.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/LazySessionWrapperOptions.java new file mode 100644 index 0000000000..c358d96af6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/LazySessionWrapperOptions.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.engine.spi; + +import java.util.TimeZone; + +import org.hibernate.Internal; +import org.hibernate.type.descriptor.WrapperOptions; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * A lazy session implementation that is needed for rendering literals. + * Usually, only the {@link WrapperOptions} interface is needed, + * but for creating LOBs, it might be to have a full-blown session. + */ +@Internal +public class LazySessionWrapperOptions extends AbstractDelegatingWrapperOptions { + + private final SessionFactoryImplementor sessionFactory; + private @Nullable SessionImplementor session; + + public LazySessionWrapperOptions(SessionFactoryImplementor sessionFactory) { + this.sessionFactory = sessionFactory; + } + + public void cleanup() { + if ( session != null ) { + session.close(); + session = null; + } + } + + @Override + protected SessionImplementor delegate() { + if ( session == null ) { + session = sessionFactory.openTemporarySession(); + } + return session; + } + + @Override + public SharedSessionContractImplementor getSession() { + return delegate(); + } + + @Override + public SessionFactoryImplementor getSessionFactory() { + return sessionFactory; + } + + @Override + public boolean useStreamForLobBinding() { + return sessionFactory.getFastSessionServices().useStreamForLobBinding(); + } + + @Override + public int getPreferredSqlTypeCodeForBoolean() { + return sessionFactory.getFastSessionServices().getPreferredSqlTypeCodeForBoolean(); + } + + @Override + public TimeZone getJdbcTimeZone() { + return sessionFactory.getSessionFactoryOptions().getJdbcTimeZone(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index 7cc5d5c605..bccf68f4c9 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -1160,7 +1160,6 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol this.jdbcTypeCode = jdbcTypeCode; } - @Override public Integer getExplicitJdbcTypeCode() { return jdbcTypeCode == null ? getPreferredSqlTypeCodeForArray() : jdbcTypeCode; } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java index db94ae0c1c..9d423ae617 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java @@ -32,6 +32,8 @@ import org.hibernate.type.EntityType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.JdbcTypeNameMapper; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import org.hibernate.type.MappingContext; @@ -116,6 +118,10 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn this.value = value; } + public JdbcMapping getType() { + return getValue().getSelectableType( getValue().getBuildingContext().getMetadataCollector(), getTypeIndex() ); + } + public String getName() { return name; } @@ -316,10 +322,22 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn return sqlTypeCode; } - private String getSqlTypeName(DdlTypeRegistry ddlTypeRegistry, Dialect dialect, MappingContext mapping) { + private String getSqlTypeName(TypeConfiguration typeConfiguration, Dialect dialect, MappingContext mapping) { if ( sqlTypeName == null ) { - final int typeCode = getSqlTypeCode( mapping ); - final DdlType descriptor = ddlTypeRegistry.getDescriptor( typeCode ); + final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); + final int sqlTypeCode = getSqlTypeCode( mapping ); + final JdbcTypeConstructor constructor = jdbcTypeRegistry.getConstructor( sqlTypeCode ); + final JdbcType jdbcType; + if ( constructor == null ) { + jdbcType = jdbcTypeRegistry.findDescriptor( sqlTypeCode ); + } + else { + jdbcType = ( (BasicType) getUnderlyingType( mapping, getValue().getType(), typeIndex ) ).getJdbcType(); + } + final DdlType descriptor = jdbcType == null + ? null + : ddlTypeRegistry.getDescriptor( jdbcType.getDdlTypeCode() ); if ( descriptor == null ) { throw new MappingException( String.format( @@ -327,8 +345,8 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn "Unable to determine SQL type name for column '%s' of table '%s' because there is no type mapping for org.hibernate.type.SqlTypes code: %s (%s)", getName(), getValue().getTable().getName(), - typeCode, - JdbcTypeNameMapper.getTypeName( typeCode ) + sqlTypeCode, + JdbcTypeNameMapper.getTypeName( sqlTypeCode ) ) ); } @@ -400,7 +418,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn public String getSqlType(Metadata mapping) { final Database database = mapping.getDatabase(); - return getSqlTypeName( database.getTypeConfiguration().getDdlTypeRegistry(), database.getDialect(), mapping ); + return getSqlTypeName( database.getTypeConfiguration(), database.getDialect(), mapping ); } /** @@ -408,7 +426,7 @@ public class Column implements Selectable, Serializable, Cloneable, ColumnTypeIn */ @Deprecated(since = "6.2") public String getSqlType(TypeConfiguration typeConfiguration, Dialect dialect, Mapping mapping) { - return getSqlTypeName( typeConfiguration.getDdlTypeRegistry(), dialect, mapping ); + return getSqlTypeName( typeConfiguration, dialect, mapping ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java index 39faa44b02..1f42017a5d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java @@ -6,17 +6,19 @@ package org.hibernate.metamodel.mapping; import org.hibernate.engine.jdbc.Size; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Models the type of a thing that can be used as an expression in a SQL query * * @author Christian Beikov */ public interface SqlTypedMapping { - String getColumnDefinition(); - Long getLength(); - Integer getPrecision(); - Integer getScale(); - Integer getTemporalPrecision(); + @Nullable String getColumnDefinition(); + @Nullable Long getLength(); + @Nullable Integer getPrecision(); + @Nullable Integer getScale(); + @Nullable Integer getTemporalPrecision(); default boolean isLob() { return getJdbcMapping().getJdbcType().isLob(); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java index 8110936845..718d602da5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java @@ -77,6 +77,7 @@ import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.spi.CompositeTypeImplementor; import org.hibernate.type.spi.TypeConfiguration; +import static org.hibernate.type.SqlTypes.ARRAY; import static org.hibernate.type.SqlTypes.JSON; import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.SQLXML; @@ -263,13 +264,18 @@ public class EmbeddableMappingTypeImpl extends AbstractEmbeddableMapping impleme final TypeConfiguration typeConfiguration = creationContext.getTypeConfiguration(); final BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); final Column aggregateColumn = bootDescriptor.getAggregateColumn(); - Integer aggregateSqlTypeCode = aggregateColumn.getSqlTypeCode(); + final BasicValue basicValue = (BasicValue) aggregateColumn.getValue(); + final BasicValue.Resolution resolution = basicValue.getResolution(); + final int aggregateColumnSqlTypeCode = resolution.getJdbcType().getDefaultSqlTypeCode(); + final int aggregateSqlTypeCode; boolean isArray = false; String structTypeName = null; - switch ( aggregateSqlTypeCode ) { + switch ( aggregateColumnSqlTypeCode ) { case STRUCT: + aggregateSqlTypeCode = STRUCT; structTypeName = aggregateColumn.getSqlType( creationContext.getMetadata() ); break; + case ARRAY: case STRUCT_ARRAY: case STRUCT_TABLE: isArray = true; @@ -290,6 +296,9 @@ public class EmbeddableMappingTypeImpl extends AbstractEmbeddableMapping impleme isArray = true; aggregateSqlTypeCode = SQLXML; break; + default: + aggregateSqlTypeCode = aggregateColumnSqlTypeCode; + break; } final JdbcTypeRegistry jdbcTypeRegistry = typeConfiguration.getJdbcTypeRegistry(); final AggregateJdbcType aggregateJdbcType = jdbcTypeRegistry.resolveAggregateDescriptor( @@ -307,7 +316,6 @@ public class EmbeddableMappingTypeImpl extends AbstractEmbeddableMapping impleme basicTypeRegistry.register( basicType, bootDescriptor.getStructName().render() ); basicTypeRegistry.register( basicType, getMappedJavaType().getJavaTypeClass().getName() ); } - final BasicValue basicValue = (BasicValue) aggregateColumn.getValue(); final BasicType resolvedJdbcMapping; if ( isArray ) { final JdbcTypeConstructor arrayConstructor = jdbcTypeRegistry.getConstructor( SqlTypes.ARRAY ); @@ -315,7 +323,7 @@ public class EmbeddableMappingTypeImpl extends AbstractEmbeddableMapping impleme throw new IllegalArgumentException( "No JdbcTypeConstructor registered for SqlTypes.ARRAY" ); } //noinspection rawtypes,unchecked - final BasicType arrayType = ( (BasicPluralJavaType) basicValue.getResolution().getDomainJavaType() ).resolveType( + final BasicType arrayType = ( (BasicPluralJavaType) resolution.getDomainJavaType() ).resolveType( typeConfiguration, creationContext.getDialect(), basicType, @@ -328,7 +336,7 @@ public class EmbeddableMappingTypeImpl extends AbstractEmbeddableMapping impleme else { resolvedJdbcMapping = basicType; } - basicValue.getResolution().updateResolution( resolvedJdbcMapping ); + resolution.updateResolution( resolvedJdbcMapping ); return resolvedJdbcMapping; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java index 975ec34d0d..03c66cdc74 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java @@ -7,24 +7,30 @@ package org.hibernate.metamodel.mapping.internal; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Christian Beikov */ public class SqlTypedMappingImpl implements SqlTypedMapping { - private final String columnDefinition; - private final Long length; - private final Integer precision; - private final Integer scale; - private final Integer temporalPrecision; + private final @Nullable String columnDefinition; + private final @Nullable Long length; + private final @Nullable Integer precision; + private final @Nullable Integer scale; + private final @Nullable Integer temporalPrecision; private final JdbcMapping jdbcMapping; + public SqlTypedMappingImpl(JdbcMapping jdbcMapping) { + this( null, null, null, null, null, jdbcMapping ); + } + public SqlTypedMappingImpl( - String columnDefinition, - Long length, - Integer precision, - Integer scale, - Integer temporalPrecision, + @Nullable String columnDefinition, + @Nullable Long length, + @Nullable Integer precision, + @Nullable Integer scale, + @Nullable Integer temporalPrecision, JdbcMapping jdbcMapping) { // Save memory by using interned strings. Probability is high that we have multiple duplicate strings this.columnDefinition = columnDefinition == null ? null : columnDefinition.intern(); @@ -36,27 +42,27 @@ public class SqlTypedMappingImpl implements SqlTypedMapping { } @Override - public String getColumnDefinition() { + public @Nullable String getColumnDefinition() { return columnDefinition; } @Override - public Long getLength() { + public @Nullable Long getLength() { return length; } @Override - public Integer getPrecision() { + public @Nullable Integer getPrecision() { return precision; } @Override - public Integer getTemporalPrecision() { + public @Nullable Integer getTemporalPrecision() { return temporalPrecision; } @Override - public Integer getScale() { + public @Nullable Integer getScale() { return scale; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java index f0b785a03b..2345c61738 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java @@ -60,6 +60,7 @@ import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.spi.PersisterFactory; import org.hibernate.query.BindableType; +import org.hibernate.query.derived.AnonymousTupleSimpleSqmPathSource; import org.hibernate.query.derived.AnonymousTupleSqmPathSource; import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.tree.domain.SqmPath; @@ -742,7 +743,8 @@ public class MappingMetamodelImpl extends QueryParameterBindingTypeResolverImpl return getTypeConfiguration().getBasicTypeForJavaType( sqmExpressible.getRelationalJavaType().getJavaType() ); } - if ( sqmExpressible instanceof BasicSqmPathSource ) { + if ( sqmExpressible instanceof BasicSqmPathSource + || sqmExpressible instanceof AnonymousTupleSimpleSqmPathSource ) { return resolveMappingExpressible( sqmExpressible.getSqmType(), tableGroupLocator ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java index 559eb7eb1e..6813835094 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/SingularAttributeImpl.java @@ -27,10 +27,14 @@ import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; -import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; +import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.spi.EntityIdentifierNavigablePath; import org.hibernate.spi.NavigablePath; +import org.hibernate.type.BasicPluralType; import org.hibernate.type.descriptor.java.JavaType; import static jakarta.persistence.metamodel.Bindable.BindableType.SINGULAR_ATTRIBUTE; @@ -143,7 +147,7 @@ public class SingularAttributeImpl } @Override - public SqmAttributeJoin createSqmJoin( + public SqmJoin createSqmJoin( SqmFrom lhs, SqmJoinType joinType, String alias, @@ -152,6 +156,21 @@ public class SingularAttributeImpl if ( getType() instanceof AnyMappingDomainType ) { throw new SemanticException( "An @Any attribute cannot be join fetched" ); } + else if ( sqmPathSource.getSqmPathType() instanceof BasicPluralType ) { + final SqmSetReturningFunction setReturningFunction = creationState.getCreationContext() + .getNodeBuilder() + .unnestArray( lhs.get( getName() ) ); + //noinspection unchecked + return (SqmJoin) new SqmFunctionJoin<>( + createNavigablePath( lhs, alias ), + setReturningFunction, + true, + setReturningFunction.getType(), + alias, + joinType, + (SqmRoot) lhs + ); + } else { return new SqmSingularJoin<>( lhs, diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java index 9400e0f6a8..292fc686ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/CriteriaDefinition.java @@ -469,6 +469,11 @@ public abstract class CriteriaDefinition return query.from(cte); } + @Override + public JpaFunctionRoot from(JpaSetReturningFunction function) { + return query.from( function ); + } + @Override public JpaCriteriaQuery createCountQuery() { return query.createCountQuery(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 7b0d2030d4..adf78c5943 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -4193,6 +4193,37 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Incubating JpaExpression named(Expression expression, String name); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Set-Returning functions + + /** + * Create a new set-returning function expression. + * + * @since 7.0 + * @see JpaSelectCriteria#from(JpaSetReturningFunction) + * @see JpaFrom#join(JpaSetReturningFunction) + */ + @Incubating + JpaSetReturningFunction setReturningFunction(String name, Expression... args); + + /** + * Creates an unnest function expression to turn an array into a set of rows. + * + * @since 7.0 + * @see JpaFrom#join(JpaSetReturningFunction) + */ + @Incubating + JpaSetReturningFunction unnestArray(Expression array); + + /** + * Creates an unnest function expression to turn an array into a set of rows. + * + * @since 7.0 + * @see JpaFrom#join(JpaSetReturningFunction) + */ + @Incubating + JpaSetReturningFunction unnestCollection(Expression> collection); + @Override JpaPredicate and(List restrictions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java index b52b8e3b8d..8f2c8303d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFrom.java @@ -4,10 +4,13 @@ */ package org.hibernate.query.criteria; +import java.util.Collection; + import org.hibernate.Incubating; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.sqm.tree.SqmJoinType; +import jakarta.persistence.criteria.Expression; import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.Subquery; @@ -62,6 +65,130 @@ public interface JpaFrom extends JpaPath, JpaFetchParent, From @Incubating JpaDerivedJoin join(Subquery subquery, SqmJoinType joinType, boolean lateral); + /** + * Like calling the overload {@link #join(JpaSetReturningFunction, SqmJoinType)} with {@link SqmJoinType#INNER}. + * + * @see #join(JpaSetReturningFunction, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin join(JpaSetReturningFunction function); + + /** + * Like calling the overload {@link #join(JpaSetReturningFunction, SqmJoinType, boolean)} passing {@code false} + * for the {@code lateral} parameter. + * + * @see #join(JpaSetReturningFunction, SqmJoinType, boolean) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin join(JpaSetReturningFunction function, SqmJoinType joinType); + + /** + * Like calling the overload {@link #joinLateral(JpaSetReturningFunction, SqmJoinType)} with {@link SqmJoinType#INNER}. + * + * @see #joinLateral(JpaSetReturningFunction, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinLateral(JpaSetReturningFunction function); + + /** + * Like calling the overload {@link #join(JpaSetReturningFunction, SqmJoinType, boolean)} passing {@code true} + * for the {@code lateral} parameter. + * + * @see #join(JpaSetReturningFunction, SqmJoinType, boolean) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinLateral(JpaSetReturningFunction function, SqmJoinType joinType); + + /** + * Creates and returns a join node for the given set returning function. + * If function arguments refer to correlated paths, the {@code lateral} argument must be set to {@code true}. + * Failing to do so when necessary may lead to an error during query compilation or execution. + * + * @since 7.0 + */ + @Incubating + JpaFunctionJoin join(JpaSetReturningFunction function, SqmJoinType joinType, boolean lateral); + + /** + * Like calling the overload {@link #joinArray(String, SqmJoinType)} with {@link SqmJoinType#INNER}. + * + * @see #joinArray(String, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArray(String arrayAttributeName); + + /** + * Like calling the overload {@link #join(JpaSetReturningFunction, SqmJoinType)} with {@link HibernateCriteriaBuilder#unnestArray(Expression)} + * with the result of {@link #get(String)} passing the given attribute name. + * + * @see #joinLateral(JpaSetReturningFunction, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArray(String arrayAttributeName, SqmJoinType joinType); + + /** + * Like calling the overload {@link #joinArray(SingularAttribute, SqmJoinType)} with {@link SqmJoinType#INNER}. + * + * @see #joinArray(SingularAttribute, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArray(SingularAttribute arrayAttribute); + + /** + * Like calling the overload {@link #join(JpaSetReturningFunction, SqmJoinType)} with {@link HibernateCriteriaBuilder#unnestArray(Expression)} + * with the given attribute. + * + * @see #joinLateral(JpaSetReturningFunction, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArray(SingularAttribute arrayAttribute, SqmJoinType joinType); + + /** + * Like calling the overload {@link #joinArrayCollection(String, SqmJoinType)} with {@link SqmJoinType#INNER}. + * + * @see #joinArrayCollection(String, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArrayCollection(String collectionAttributeName); + + /** + * Like calling the overload {@link #join(JpaSetReturningFunction, SqmJoinType)} with {@link HibernateCriteriaBuilder#unnestCollection(Expression)} + * with the result of {@link #get(String)} passing the given attribute name. + * + * @see #joinLateral(JpaSetReturningFunction, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArrayCollection(String collectionAttributeName, SqmJoinType joinType); + + /** + * Like calling the overload {@link #joinArrayCollection(SingularAttribute, SqmJoinType)} with {@link SqmJoinType#INNER}. + * + * @see #joinArrayCollection(SingularAttribute, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArrayCollection(SingularAttribute> collectionAttribute); + + /** + * Like calling the overload {@link #join(JpaSetReturningFunction, SqmJoinType)} with {@link HibernateCriteriaBuilder#unnestCollection(Expression)} + * with the given attribute. + * + * @see #joinLateral(JpaSetReturningFunction, SqmJoinType) + * @since 7.0 + */ + @Incubating + JpaFunctionJoin joinArrayCollection(SingularAttribute> collectionAttribute, SqmJoinType joinType); + @Incubating JpaJoin join(JpaCteCriteria cte); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionFrom.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionFrom.java new file mode 100644 index 0000000000..de2c878887 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionFrom.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +/** + * @since 7.0 + */ +@Incubating +public interface JpaFunctionFrom extends JpaFrom { + + /** + * The function for this from node. + */ + JpaSetReturningFunction getFunction(); + + /** + * The expression referring to an iteration variable, indexing the rows produced by the function. + * This is the equivalent of the SQL {@code with ordinality} clause. + */ + JpaExpression index(); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionJoin.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionJoin.java new file mode 100644 index 0000000000..0b02ebc319 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionJoin.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Predicate; + +/** + * @since 7.0 + */ +@Incubating +public interface JpaFunctionJoin extends JpaFunctionFrom, JpaJoin { + /** + * Specifies whether the function arguments can refer to previous from node aliases. + * Normally, functions in the from clause are unable to access other from nodes, + * but when specifying them as lateral, they are allowed to do so. + * Refer to the SQL standard definition of LATERAL for more details. + */ + boolean isLateral(); + + @Override + JpaFunctionJoin on(JpaExpression restriction); + + @Override + JpaFunctionJoin on(Expression restriction); + + @Override + JpaFunctionJoin on(JpaPredicate... restrictions); + + @Override + JpaFunctionJoin on(Predicate... restrictions); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionRoot.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionRoot.java new file mode 100644 index 0000000000..a549a5f116 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaFunctionRoot.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +/** + * @since 7.0 + */ +@Incubating +public interface JpaFunctionRoot extends JpaFunctionFrom, JpaRoot { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java index 7305d23091..de0eba6d0e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSelectCriteria.java @@ -45,6 +45,15 @@ public interface JpaSelectCriteria extends AbstractQuery, JpaCriteriaBase */ JpaRoot from(JpaCteCriteria cte); + /** + * Create and add a query root corresponding to the given set-returning function, + * forming a cartesian product with any existing roots. + * + * @param function the set-returning function + * @return query root corresponding to the given function + */ + JpaFunctionRoot from(JpaSetReturningFunction function); + @Override JpaSelectCriteria distinct(boolean distinct); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSetReturningFunction.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSetReturningFunction.java new file mode 100644 index 0000000000..63dff37574 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaSetReturningFunction.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +/** + * A set returning function criteria. + */ +@Incubating +public interface JpaSetReturningFunction extends JpaCriteriaNode { + + /** + * The name of the function. + */ + String getFunctionName(); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index 67d738ff11..fece7b7584 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -52,6 +52,7 @@ import org.hibernate.query.criteria.JpaSearchOrder; import org.hibernate.query.criteria.JpaSearchedCase; import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.JpaSetJoin; +import org.hibernate.query.criteria.JpaSetReturningFunction; import org.hibernate.query.criteria.JpaSimpleCase; import org.hibernate.query.criteria.JpaSubQuery; import org.hibernate.query.criteria.JpaValues; @@ -3734,4 +3735,22 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde public JpaExpression named(Expression expression, String name) { return criteriaBuilder.named( expression, name ); } + + @Incubating + @Override + public JpaSetReturningFunction setReturningFunction(String name, Expression... args) { + return criteriaBuilder.setReturningFunction( name, args ); + } + + @Override + @Incubating + public JpaSetReturningFunction unnestArray(Expression array) { + return criteriaBuilder.unnestArray( array ); + } + + @Override + @Incubating + public JpaSetReturningFunction unnestCollection(Expression> collection) { + return criteriaBuilder.unnestCollection( collection ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java index 04bf252cdd..46a69f0ced 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java @@ -12,6 +12,7 @@ import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; import org.hibernate.metamodel.mapping.MappingType; +import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.query.sqm.SqmExpressible; @@ -35,6 +36,15 @@ public class AnonymousTupleBasicEntityIdentifierMapping this.delegate = delegate; } + public AnonymousTupleBasicEntityIdentifierMapping( + MappingType declaringType, + SelectableMapping selectableMapping, + SqmExpressible expressible, + BasicEntityIdentifierMapping delegate) { + super( declaringType, delegate.getAttributeName(), selectableMapping, expressible, -1 ); + this.delegate = delegate; + } + @Override public Nature getNature() { return Nature.SIMPLE; diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicValuedModelPart.java index 952eb0eea3..5953d0c51d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicValuedModelPart.java @@ -18,6 +18,9 @@ import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.OwnedValuedModelPart; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.spi.NavigablePath; @@ -44,9 +47,8 @@ public class AnonymousTupleBasicValuedModelPart implements OwnedValuedModelPart, private static final FetchOptions FETCH_OPTIONS = FetchOptions.valueOf( FetchTiming.IMMEDIATE, FetchStyle.JOIN ); private final MappingType declaringType; private final String partName; - private final String selectionExpression; + private final SelectableMapping selectableMapping; private final SqmExpressible expressible; - private final JdbcMapping jdbcMapping; private final int fetchableIndex; public AnonymousTupleBasicValuedModelPart( @@ -56,11 +58,43 @@ public class AnonymousTupleBasicValuedModelPart implements OwnedValuedModelPart, SqmExpressible expressible, JdbcMapping jdbcMapping, int fetchableIndex) { + this( + declaringType, + partName, + new SelectableMappingImpl( + "", + selectionExpression, + new SelectablePath( partName ), + null, + null, + null, + null, + null, + null, + null, + false, + true, + false, + false, + false, + false, + jdbcMapping + ), + expressible, + fetchableIndex + ); + } + + public AnonymousTupleBasicValuedModelPart( + MappingType declaringType, + String partName, + SelectableMapping selectableMapping, + SqmExpressible expressible, + int fetchableIndex) { this.declaringType = declaringType; this.partName = partName; - this.selectionExpression = selectionExpression; + this.selectableMapping = selectableMapping; this.expressible = expressible; - this.jdbcMapping = jdbcMapping; this.fetchableIndex = fetchableIndex; } @@ -99,84 +133,99 @@ public class AnonymousTupleBasicValuedModelPart implements OwnedValuedModelPart, return null; } + @Override + public String getSelectableName() { + return selectableMapping.getSelectableName(); + } + + @Override + public SelectablePath getSelectablePath() { + return selectableMapping.getSelectablePath(); + } + + @Override + public String getWriteExpression() { + return selectableMapping.getWriteExpression(); + } + @Override public JdbcMapping getJdbcMapping() { - return jdbcMapping; + return selectableMapping.getJdbcMapping(); } @Override public String getContainingTableExpression() { - return ""; + return selectableMapping.getContainingTableExpression(); } @Override public String getSelectionExpression() { - return selectionExpression; + return selectableMapping.getSelectionExpression(); } @Override public String getCustomReadExpression() { - return null; + return selectableMapping.getCustomReadExpression(); } @Override public String getCustomWriteExpression() { - return null; + return selectableMapping.getCustomWriteExpression(); } @Override public boolean isFormula() { - return false; + return selectableMapping.isFormula(); } @Override public boolean isNullable() { - return false; + return selectableMapping.isNullable(); } @Override public boolean isInsertable() { - return true; + return selectableMapping.isInsertable(); } @Override public boolean isUpdateable() { - return false; + return selectableMapping.isUpdateable(); } @Override public boolean isPartitioned() { - return false; + return selectableMapping.isPartitioned(); } @Override public boolean hasPartitionedSelectionMapping() { - return false; + return selectableMapping.isPartitioned(); } @Override public String getColumnDefinition() { - return null; + return selectableMapping.getColumnDefinition(); } @Override public Long getLength() { - return null; + return selectableMapping.getLength(); } @Override public Integer getPrecision() { - return null; + return selectableMapping.getPrecision(); } @Override public Integer getScale() { - return null; + return selectableMapping.getScale(); } @Override public Integer getTemporalPrecision() { - return null; + return selectableMapping.getTemporalPrecision(); } @Override @@ -215,7 +264,7 @@ public class AnonymousTupleBasicValuedModelPart implements OwnedValuedModelPart, return new BasicResult<>( sqlSelection.getValuesArrayPosition(), resultVariable, - jdbcMapping, + getJdbcMapping(), navigablePath, false, !sqlSelection.isVirtual() diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java index 99c51a6e86..63541d0b0e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java @@ -28,6 +28,7 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectableMappings; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.model.domain.DomainType; @@ -82,7 +83,7 @@ public class AnonymousTupleEmbeddableValuedModelPart implements EmbeddableValued public AnonymousTupleEmbeddableValuedModelPart( SqmExpressible sqmExpressible, - List sqlSelections, + SqlTypedMapping[] sqlTypedMappings, int selectionIndex, String selectionExpression, Set compatibleTableExpressions, @@ -93,7 +94,7 @@ public class AnonymousTupleEmbeddableValuedModelPart implements EmbeddableValued int fetchableIndex) { this.modelPartMap = createModelParts( sqmExpressible, - sqlSelections, + sqlTypedMappings, selectionIndex, selectionExpression, compatibleTableExpressions, @@ -109,7 +110,7 @@ public class AnonymousTupleEmbeddableValuedModelPart implements EmbeddableValued private Map createModelParts( SqmExpressible sqmExpressible, - List sqlSelections, + SqlTypedMapping[] sqlTypedMappings, int selectionIndex, String selectionExpression, Set compatibleTableExpressions, @@ -126,7 +127,7 @@ public class AnonymousTupleEmbeddableValuedModelPart implements EmbeddableValued this, sqmExpressible, attributeType, - sqlSelections, + sqlTypedMappings, selectionIndex, selectionExpression + "_" + attribute.getName(), attribute.getName(), diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java index 9065873a80..2c50771b74 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java @@ -4,7 +4,6 @@ */ package org.hibernate.query.derived; -import java.util.List; import java.util.Set; import org.hibernate.Incubating; @@ -13,11 +12,11 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.query.sqm.SqmExpressible; -import org.hibernate.sql.ast.spi.SqlSelection; import jakarta.persistence.metamodel.Attribute; @@ -32,7 +31,7 @@ public class AnonymousTupleEmbeddedEntityIdentifierMapping extends AnonymousTupl public AnonymousTupleEmbeddedEntityIdentifierMapping( SqmExpressible sqmExpressible, - List sqlSelections, + SqlTypedMapping[] sqlTypedMappings, int selectionIndex, String selectionExpression, Set compatibleTableExpressions, @@ -41,7 +40,7 @@ public class AnonymousTupleEmbeddedEntityIdentifierMapping extends AnonymousTupl CompositeIdentifierMapping delegate) { super( sqmExpressible, - sqlSelections, + sqlTypedMappings, selectionIndex, selectionExpression, compatibleTableExpressions, diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java index 29974301d7..8f9f4b312f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java @@ -83,7 +83,6 @@ public class AnonymousTupleEntityValuedModelPart private final EntityIdentifierMapping identifierMapping; private final DomainType domainType; - private final String componentName; private final EntityValuedModelPart delegate; // private final Set targetKeyPropertyNames; // private final int fetchableIndex; @@ -91,12 +90,10 @@ public class AnonymousTupleEntityValuedModelPart public AnonymousTupleEntityValuedModelPart( EntityIdentifierMapping identifierMapping, DomainType domainType, - String componentName, EntityValuedModelPart delegate, int fetchableIndex) { this.identifierMapping = identifierMapping; this.domainType = domainType; - this.componentName = componentName; this.delegate = delegate; final EntityPersister persister = ((EntityMappingType) delegate.getPartMappingType()) .getEntityPersister(); @@ -158,7 +155,7 @@ public class AnonymousTupleEntityValuedModelPart @Override public String getPartName() { - return componentName; + return delegate.getPartName(); } @Override @@ -500,7 +497,7 @@ public class AnonymousTupleEntityValuedModelPart @Override public String getSqlAliasStem() { - return getPartName(); + return ((TableGroupJoinProducer) delegate).getSqlAliasStem(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java index a88a2cdfe0..c74fc7cedf 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java @@ -4,7 +4,6 @@ */ package org.hibernate.query.derived; -import java.util.List; import java.util.Set; import org.hibernate.Incubating; @@ -15,11 +14,11 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.IdClassEmbeddable; import org.hibernate.metamodel.mapping.internal.VirtualIdEmbeddable; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.query.sqm.SqmExpressible; -import org.hibernate.sql.ast.spi.SqlSelection; import jakarta.persistence.metamodel.Attribute; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,7 +34,7 @@ public class AnonymousTupleNonAggregatedEntityIdentifierMapping extends Anonymou public AnonymousTupleNonAggregatedEntityIdentifierMapping( SqmExpressible sqmExpressible, - List sqlSelections, + SqlTypedMapping[] sqlTypedMappings, int selectionIndex, String selectionExpression, Set compatibleTableExpressions, @@ -45,7 +44,7 @@ public class AnonymousTupleNonAggregatedEntityIdentifierMapping extends Anonymou NonAggregatedIdentifierMapping delegate) { super( sqmExpressible, - sqlSelections, + sqlTypedMappings, selectionIndex, selectionExpression, compatibleTableExpressions, diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmAssociationPathSourceNew.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmAssociationPathSourceNew.java new file mode 100644 index 0000000000..6ccac5f2fe --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmAssociationPathSourceNew.java @@ -0,0 +1,127 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.derived; + +import java.lang.reflect.Member; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.AttributeClassification; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; +import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; +import org.hibernate.query.hql.spi.SqmCreationState; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; +import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleSqmAssociationPathSourceNew extends AnonymousTupleSqmPathSourceNew implements + SingularPersistentAttribute { + + private final SimpleDomainType domainType; + + public AnonymousTupleSqmAssociationPathSourceNew( + String localPathName, + SqmPathSource pathSource, + DomainType sqmPathType, + SimpleDomainType domainType) { + super( localPathName, pathSource, sqmPathType ); + this.domainType = domainType; + } + + @Override + public SqmJoin createSqmJoin( + SqmFrom lhs, + SqmJoinType joinType, + String alias, + boolean fetched, + SqmCreationState creationState) { + return new SqmSingularJoin<>( + lhs, + this, + alias, + joinType, + fetched, + creationState.getCreationContext().getNodeBuilder() + ); + } + + @Override + public SimpleDomainType getType() { + return domainType; + } + + @Override + public ManagedDomainType getDeclaringType() { + return null; + } + + @Override + public SqmPathSource getPathSource() { + return this; + } + + @Override + public boolean isId() { + return false; + } + + @Override + public boolean isVersion() { + return false; + } + + @Override + public boolean isOptional() { + return true; + } + + @Override + public JavaType getAttributeJavaType() { + return domainType.getExpressibleJavaType(); + } + + @Override + public AttributeClassification getAttributeClassification() { + return AttributeClassification.MANY_TO_ONE; + } + + @Override + public SimpleDomainType getKeyGraphType() { + return domainType; + } + + @Override + public String getName() { + return getPathName(); + } + + @Override + public PersistentAttributeType getPersistentAttributeType() { + return PersistentAttributeType.MANY_TO_ONE; + } + + @Override + public Member getJavaMember() { + return null; + } + + @Override + public boolean isAssociation() { + return true; + } + + @Override + public boolean isCollection() { + return false; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSourceNew.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSourceNew.java new file mode 100644 index 0000000000..0056ca0387 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleSqmPathSourceNew.java @@ -0,0 +1,94 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.derived; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.model.domain.BasicDomainType; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EmbeddableDomainType; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.internal.PathHelper; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * @author Christian Beikov + */ +@Incubating +public class AnonymousTupleSqmPathSourceNew implements SqmPathSource { + private final String localPathName; + private final SqmPathSource pathSource; + private final DomainType sqmPathType; + + public AnonymousTupleSqmPathSourceNew(String localPathName, SqmPathSource pathSource, DomainType sqmPathType) { + this.localPathName = localPathName; + this.pathSource = pathSource; + this.sqmPathType = sqmPathType; + } + + @Override + public Class getBindableJavaType() { + return pathSource.getExpressibleJavaType().getJavaTypeClass(); + } + + @Override + public String getPathName() { + return localPathName; + } + + @Override + public DomainType getSqmPathType() { + return sqmPathType; + } + + @Override + public BindableType getBindableType() { + return pathSource.getBindableType(); + } + + @Override + public JavaType getExpressibleJavaType() { + return pathSource.getExpressibleJavaType(); + } + + @Override + public SqmPathSource findSubPathSource(String name) { + return pathSource.findSubPathSource( name ); + } + + @Override + public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePathSource) { + if ( sqmPathType instanceof BasicDomainType ) { + return new SqmBasicValuedSimplePath<>( + PathHelper.append( lhs, this, intermediatePathSource ), + this, + lhs, + lhs.nodeBuilder() + ); + } + else if ( sqmPathType instanceof EmbeddableDomainType ) { + return new SqmEmbeddedValuedSimplePath<>( + PathHelper.append( lhs, this, intermediatePathSource ), + this, + lhs, + lhs.nodeBuilder() + ); + } + else if ( sqmPathType instanceof EntityDomainType ) { + return new SqmEntityValuedSimplePath<>( + PathHelper.append( lhs, this, intermediatePathSource ), + this, + lhs, + lhs.nodeBuilder() + ); + } + + throw new UnsupportedOperationException( "Unsupported path source: " + sqmPathType ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java index f9c0aeee8b..d024fe6328 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java @@ -4,6 +4,7 @@ */ package org.hibernate.query.derived; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -28,15 +29,16 @@ import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.query.sqm.SqmExpressible; -import org.hibernate.query.sqm.tree.domain.SqmPath; -import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlSelection; @@ -46,6 +48,7 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupProducer; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; import jakarta.persistence.metamodel.Attribute; @@ -69,7 +72,7 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map public AnonymousTupleTableGroupProducer( AnonymousTupleType tupleType, String aliasStem, - List sqlSelections, + SqlTypedMapping[] sqlTypedMappings, FromClauseAccess fromClauseAccess) { this.aliasStem = aliasStem; this.javaTypeDescriptor = tupleType.getExpressibleJavaType(); @@ -80,19 +83,19 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map final int componentCount = tupleType.componentCount(); final Map modelParts = CollectionHelper.linkedMapOfSize( componentCount ); int selectionIndex = 0; - for ( int i = 0; i < componentCount; i++ ) { - final SqmSelectableNode selectableNode = tupleType.getSelectableNode( i ); + for ( int i = 0; i < componentCount && selectionIndex < sqlTypedMappings.length; i++ ) { + final SqmExpressible expressible = tupleType.get( i ); + final DomainType sqmType = expressible.getSqmType(); final String partName = tupleType.getComponentName( i ); - final SqlSelection sqlSelection = sqlSelections.get( i ); final ModelPart modelPart; - if ( selectableNode instanceof SqmPath ) { - final SqmPath sqmPath = (SqmPath) selectableNode; - final TableGroup tableGroup = fromClauseAccess.findTableGroup( sqmPath.getNavigablePath() ); + if ( expressible instanceof PluralPersistentAttribute + || !( sqmType instanceof BasicType ) ) { + final TableGroup tableGroup = fromClauseAccess.findTableGroup( tupleType.getComponentSourcePath( i ) ); modelPart = createModelPart( this, - selectableNode.getExpressible(), - sqmPath.getNodeType().getSqmPathType(), - sqlSelections, + expressible, + sqmType, + sqlTypedMappings, selectionIndex, partName, partName, @@ -101,13 +104,23 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map modelParts.size() ); } + else if ( sqlTypedMappings[selectionIndex] instanceof SelectableMapping selectable ) { + modelPart = new AnonymousTupleBasicValuedModelPart( + this, + partName, + selectable, + expressible, + modelParts.size() + ); + compatibleTableExpressions.add( selectable.getContainingTableExpression() ); + } else { modelPart = new AnonymousTupleBasicValuedModelPart( this, partName, partName, - selectableNode.getExpressible(), - sqlSelection.getExpressionType().getSingleJdbcMapping(), + expressible, + sqlTypedMappings[selectionIndex].getJdbcMapping(), modelParts.size() ); } @@ -132,7 +145,7 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map MappingType mappingType, SqmExpressible sqmExpressible, DomainType domainType, - List sqlSelections, + SqlTypedMapping[] sqlTypedMappings, int selectionIndex, String selectionExpression, String partName, @@ -150,7 +163,7 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map final Set> attributes = (Set>) ( (ManagedDomainType) ( (EntityDomainType) domainType ).getIdentifierDescriptor().getSqmPathType() ).getAttributes(); newIdentifierMapping = new AnonymousTupleEmbeddedEntityIdentifierMapping( sqmExpressible, - sqlSelections, + sqlTypedMappings, selectionIndex, selectionExpression + "_" + identifierMapping.getAttributeName(), compatibleTableExpressions, @@ -159,12 +172,21 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map (CompositeIdentifierMapping) identifierMapping ); } + else if ( sqlTypedMappings[selectionIndex] instanceof SelectableMapping selectable ) { + newIdentifierMapping = new AnonymousTupleBasicEntityIdentifierMapping( + mappingType, + selectable, + sqmExpressible, + (BasicEntityIdentifierMapping) identifierMapping + ); + compatibleTableExpressions.add( selectable.getContainingTableExpression() ); + } else { newIdentifierMapping = new AnonymousTupleBasicEntityIdentifierMapping( mappingType, selectionExpression + "_" + identifierMapping.getAttributeName(), sqmExpressible, - sqlSelections.get( selectionIndex ).getExpressionType().getSingleJdbcMapping(), + sqlTypedMappings[selectionIndex].getJdbcMapping(), (BasicEntityIdentifierMapping) identifierMapping ); } @@ -174,7 +196,7 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map final Set> attributes = (Set>) ( (ManagedDomainType) ( (EntityDomainType) domainType ).getIdentifierDescriptor().getSqmPathType() ).getAttributes(); newIdentifierMapping = new AnonymousTupleNonAggregatedEntityIdentifierMapping( sqmExpressible, - sqlSelections, + sqlTypedMappings, selectionIndex, selectionExpression, compatibleTableExpressions, @@ -191,7 +213,6 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map return new AnonymousTupleEntityValuedModelPart( newIdentifierMapping, domainType, - selectionExpression, existingModelPartContainer, fetchableIndex ); @@ -201,7 +222,7 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map final Set> attributes = (Set>) ( (ManagedDomainType) domainType ).getAttributes(); return new AnonymousTupleEmbeddableValuedModelPart( sqmExpressible, - sqlSelections, + sqlTypedMappings, selectionIndex, selectionExpression, compatibleTableExpressions, @@ -212,18 +233,34 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map fetchableIndex ); } + else if ( sqlTypedMappings[selectionIndex] instanceof SelectableMapping selectable ) { + compatibleTableExpressions.add( selectable.getContainingTableExpression() ); + return new AnonymousTupleBasicValuedModelPart( + mappingType, + partName, + selectable, + sqmExpressible, + fetchableIndex + ); + } else { return new AnonymousTupleBasicValuedModelPart( mappingType, partName, selectionExpression, sqmExpressible, - sqlSelections.get( selectionIndex ).getExpressionType().getSingleJdbcMapping(), + sqlTypedMappings[selectionIndex].getJdbcMapping(), fetchableIndex ); } } + public List getColumnNames() { + final ArrayList columnNames = new ArrayList<>( modelParts.size() ); + forEachSelectable( (index, selectableMapping) -> columnNames.add( selectableMapping.getSelectionExpression() ) ); + return columnNames; + } + public Set getCompatibleTableExpressions() { return compatibleTableExpressions; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java index 8ec345b9a2..58b6b1e64b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java @@ -12,12 +12,12 @@ import java.util.Map; import org.hibernate.Incubating; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.UnsupportedMappingException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.metamodel.model.domain.DomainType; -import org.hibernate.metamodel.model.domain.EntityDomainType; -import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; import org.hibernate.metamodel.model.domain.SimpleDomainType; -import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.TupleType; import org.hibernate.query.ReturnableType; import org.hibernate.query.SemanticException; @@ -27,12 +27,14 @@ import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.select.SqmSelectClause; import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.ObjectArrayJavaType; -import jakarta.persistence.metamodel.Attribute; +import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -42,7 +44,9 @@ import jakarta.persistence.metamodel.Attribute; public class AnonymousTupleType implements TupleType, DomainType, ReturnableType, SqmPathSource { private final ObjectArrayJavaType javaTypeDescriptor; - private final SqmSelectableNode[] components; + private final @Nullable NavigablePath[] componentSourcePaths; + private final SqmExpressible[] expressibles; + private final String[] componentNames; private final Map componentIndexMap; public AnonymousTupleType(SqmSubQuery subQuery) { @@ -50,7 +54,17 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur } public AnonymousTupleType(SqmSelectableNode[] components) { - this.components = components; + final SqmExpressible[] expressibles = new SqmExpressible[components.length]; + final NavigablePath[] componentSourcePaths = new NavigablePath[components.length]; + for ( int i = 0; i < components.length; i++ ) { + expressibles[i] = components[i].getNodeType(); + if ( components[i] instanceof SqmPath path ) { + componentSourcePaths[i] = path.getNavigablePath(); + } + } + this.expressibles = expressibles; + this.componentSourcePaths = componentSourcePaths; + this.componentNames = new String[components.length]; this.javaTypeDescriptor = new ObjectArrayJavaType( getTypeDescriptors( components ) ); final Map map = CollectionHelper.linkedMapOfSize( components.length ); for ( int i = 0; i < components.length; i++ ) { @@ -61,6 +75,19 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur + " (aliases are required in CTEs and in subqueries occurring in from clause)" ); } map.put( alias, i ); + componentNames[i] = alias; + } + this.componentIndexMap = map; + } + + public AnonymousTupleType(SqmExpressible[] expressibles, String[] componentNames) { + this.componentSourcePaths = new NavigablePath[componentNames.length]; + this.expressibles = expressibles; + this.componentNames = componentNames; + this.javaTypeDescriptor = new ObjectArrayJavaType( getTypeDescriptors( expressibles ) ); + final Map map = CollectionHelper.linkedMapOfSize( expressibles.length ); + for ( int i = 0; i < componentNames.length; i++ ) { + map.put( componentNames[i], i ); } this.componentIndexMap = map; } @@ -84,62 +111,49 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur return typeDescriptors; } + private static JavaType[] getTypeDescriptors(SqmExpressible[] components) { + final JavaType[] typeDescriptors = new JavaType[components.length]; + for ( int i = 0; i < components.length; i++ ) { + typeDescriptors[i] = components[i].getExpressibleJavaType(); + } + return typeDescriptors; + } + + public static SqlTypedMapping[] toSqlTypedMappings(List sqlSelections) { + final SqlTypedMapping[] jdbcMappings = new SqlTypedMapping[sqlSelections.size()]; + for ( int i = 0; i < sqlSelections.size(); i++ ) { + final JdbcMappingContainer expressionType = sqlSelections.get( i ).getExpressionType(); + if ( expressionType instanceof SqlTypedMapping sqlTypedMapping ) { + jdbcMappings[i] = sqlTypedMapping; + } + else { + jdbcMappings[i] = new SqlTypedMappingImpl( expressionType.getSingleJdbcMapping() ); + } + } + return jdbcMappings; + } public AnonymousTupleTableGroupProducer resolveTableGroupProducer( String aliasStem, List sqlSelections, FromClauseAccess fromClauseAccess) { - return new AnonymousTupleTableGroupProducer( this, aliasStem, sqlSelections, fromClauseAccess ); + return resolveTableGroupProducer( aliasStem, toSqlTypedMappings( sqlSelections ), fromClauseAccess ); } - public List determineColumnNames() { - final int componentCount = componentCount(); - final List columnNames = new ArrayList<>( componentCount ); - for ( int i = 0; i < componentCount; i++ ) { - final SqmSelectableNode selectableNode = getSelectableNode( i ); - final String componentName = getComponentName( i ); - if ( selectableNode instanceof SqmPath ) { - addColumnNames( - columnNames, - ( (SqmPath) selectableNode ).getNodeType().getSqmPathType(), - componentName - ); - } - else { - columnNames.add( componentName ); - } - } - return columnNames; - } - - private static void addColumnNames(List columnNames, DomainType domainType, String componentName) { - if ( domainType instanceof EntityDomainType ) { - final EntityDomainType entityDomainType = (EntityDomainType) domainType; - final SingularPersistentAttribute idAttribute = entityDomainType.findIdAttribute(); - final String idPath = idAttribute == null ? componentName : componentName + "_" + idAttribute.getName(); - addColumnNames( columnNames, entityDomainType.getIdentifierDescriptor().getSqmPathType(), idPath ); - } - else if ( domainType instanceof ManagedDomainType ) { - for ( Attribute attribute : ( (ManagedDomainType) domainType ).getAttributes() ) { - if ( !( attribute instanceof SingularPersistentAttribute ) ) { - throw new IllegalArgumentException( "Only embeddables without collections are supported" ); - } - final DomainType attributeType = ( (SingularPersistentAttribute) attribute ).getType(); - addColumnNames( columnNames, attributeType, componentName + "_" + attribute.getName() ); - } - } - else { - columnNames.add( componentName ); - } + public AnonymousTupleTableGroupProducer resolveTableGroupProducer( + String aliasStem, + SqlTypedMapping[] jdbcMappings, + FromClauseAccess fromClauseAccess) { + return new AnonymousTupleTableGroupProducer( this, aliasStem, jdbcMappings, fromClauseAccess ); } @Override public int componentCount() { - return components.length; + return expressibles.length; } @Override public String getComponentName(int index) { - return components[index].getAlias(); + return componentNames[index]; } @Override @@ -149,21 +163,21 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur @Override public SqmExpressible get(int index) { - return components[index].getExpressible(); + return expressibles[index]; } @Override public SqmExpressible get(String componentName) { final Integer index = componentIndexMap.get( componentName ); - return index == null ? null : components[index].getExpressible(); + return index == null ? null : expressibles[index]; } protected Integer getIndex(String componentName) { return componentIndexMap.get( componentName ); } - public SqmSelectableNode getSelectableNode(int index) { - return components[index]; + public @Nullable NavigablePath getComponentSourcePath(int index) { + return componentSourcePaths[index]; } @Override @@ -172,44 +186,33 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur if ( index == null ) { return null; } - final SqmSelectableNode component = components[index]; - if ( component instanceof SqmPath ) { - final SqmPath sqmPath = (SqmPath) component; - if ( sqmPath.getNodeType() instanceof SingularPersistentAttribute ) { - //noinspection unchecked,rawtypes - return new AnonymousTupleSqmAssociationPathSource( - name, - sqmPath, - ( (SingularPersistentAttribute) sqmPath.getNodeType() ).getType() - ); - } - else if ( sqmPath.getNodeType() instanceof PluralPersistentAttribute ) { - //noinspection unchecked,rawtypes - return new AnonymousTupleSqmAssociationPathSource( - name, - sqmPath, - ( (PluralPersistentAttribute) sqmPath.getNodeType() ).getElementType() - ); - } - else if ( sqmPath.getNodeType() instanceof EntityDomainType ) { - //noinspection unchecked,rawtypes - return new AnonymousTupleSqmAssociationPathSource( - name, - sqmPath, - (SimpleDomainType) sqmPath.getNodeType() - ); - } - else { - return new AnonymousTupleSqmPathSource<>( name, sqmPath ); - } + final SqmExpressible expressible = expressibles[index]; + final DomainType sqmType = expressible.getSqmType(); + if ( expressible instanceof PluralPersistentAttribute pluralAttribute ) { + //noinspection unchecked,rawtypes + return new AnonymousTupleSqmAssociationPathSourceNew( + name, + pluralAttribute, + sqmType, + pluralAttribute.getElementType() + ); } - else { + else if ( sqmType instanceof BasicType ) { return new AnonymousTupleSimpleSqmPathSource<>( name, - component.getExpressible().getSqmType(), + sqmType, BindableType.SINGULAR_ATTRIBUTE ); } + else { + //noinspection unchecked,rawtypes + return new AnonymousTupleSqmAssociationPathSourceNew( + name, + (SqmPathSource) expressible, + sqmType, + (SimpleDomainType) sqmType + ); + } } @Override @@ -263,7 +266,7 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur @Override public String toString() { - return "AnonymousTupleType" + Arrays.toString( components ); + return "AnonymousTupleType" + Arrays.toString( expressibles ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/CteTupleTableGroupProducer.java b/hibernate-core/src/main/java/org/hibernate/query/derived/CteTupleTableGroupProducer.java index dc61e2e841..076f46eea9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/CteTupleTableGroupProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/CteTupleTableGroupProducer.java @@ -11,10 +11,10 @@ import org.hibernate.Incubating; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.cte.SqmCteTable; import org.hibernate.sql.ast.spi.FromClauseAccess; -import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.cte.CteColumn; import org.hibernate.type.BasicType; @@ -35,9 +35,9 @@ public class CteTupleTableGroupProducer extends AnonymousTupleTableGroupProducer public CteTupleTableGroupProducer( SqmCteTable sqmCteTable, String aliasStem, - List sqlSelections, + SqlTypedMapping[] sqlTypedMappings, FromClauseAccess fromClauseAccess) { - super( sqmCteTable, aliasStem, sqlSelections, fromClauseAccess ); + super( sqmCteTable, aliasStem, sqlTypedMappings, fromClauseAccess ); final SqmCteStatement cteStatement = sqmCteTable.getCteStatement(); final BasicType stringType = cteStatement.nodeBuilder() .getTypeConfiguration() @@ -71,7 +71,7 @@ public class CteTupleTableGroupProducer extends AnonymousTupleTableGroupProducer } public List determineCteColumns() { - final List columns = new ArrayList<>( getModelParts().size() ); + final List columns = new ArrayList<>( getModelParts().size() + 3 ); forEachSelectable( (selectionIndex, selectableMapping) -> { columns.add( diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index 1810bd0ee1..d7f56c28ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -65,6 +65,7 @@ import org.hibernate.query.criteria.JpaCteCriteriaAttribute; import org.hibernate.query.criteria.JpaCteCriteriaType; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaSearchOrder; +import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.query.hql.HqlLogging; import org.hibernate.query.hql.spi.DotIdentifierConsumer; import org.hibernate.query.hql.spi.SemanticPathPart; @@ -95,6 +96,7 @@ import org.hibernate.query.sqm.function.FunctionKind; import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SqmSetReturningFunctionDescriptor; import org.hibernate.query.sqm.internal.ParameterCollector; import org.hibernate.query.sqm.internal.SqmCreationProcessingStateImpl; import org.hibernate.query.sqm.internal.SqmDmlCreationProcessingState; @@ -119,6 +121,7 @@ import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; +import org.hibernate.query.sqm.tree.domain.SqmFunctionRoot; import org.hibernate.query.sqm.tree.domain.SqmIndexAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmListJoin; import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference; @@ -160,6 +163,7 @@ import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.SqmSummarization; import org.hibernate.query.sqm.tree.expression.SqmToDuration; @@ -174,6 +178,7 @@ import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; +import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmConflictClause; @@ -2198,6 +2203,23 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem return sqmRoot; } + @Override + public SqmRoot visitRootFunction(HqlParser.RootFunctionContext ctx) { + if ( getCreationOptions().useStrictJpaCompliance() ) { + throw new StrictJpaComplianceViolation( + "The JPA specification does not support functions in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature.", + StrictJpaComplianceViolation.Type.FROM_FUNCTION + ); + } + + final SqmSetReturningFunction function = (SqmSetReturningFunction) ctx.setReturningFunction().accept( this ); + final String alias = extractAlias( ctx.variable() ); + final SqmFunctionRoot sqmRoot = new SqmFunctionRoot<>( function, alias ); + processingStateStack.getCurrent().getPathRegistry().register( sqmRoot ); + return sqmRoot; + } + @Override public String visitVariable(HqlParser.VariableContext ctx) { return extractAlias( ctx ); @@ -2299,6 +2321,9 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem else if ( joinTargetContext instanceof HqlParser.JoinSubqueryContext ) { return ((HqlParser.JoinSubqueryContext) joinTargetContext).variable(); } + else if ( joinTargetContext instanceof HqlParser.JoinFunctionContext ) { + return ((HqlParser.JoinFunctionContext) joinTargetContext).variable(); + } else { throw new ParsingException( "unexpected join type" ); } @@ -2336,6 +2361,34 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem processingStateStack.getCurrent().getPathRegistry().register( join ); return join; } + else if ( joinTargetContext instanceof HqlParser.JoinFunctionContext ) { + if ( fetch ) { + throw new SemanticException( "The 'from' clause of a set returning function has a 'fetch' join", query ); + } + if ( getCreationOptions().useStrictJpaCompliance() ) { + throw new StrictJpaComplianceViolation( + "The JPA specification does not support functions in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature.", + StrictJpaComplianceViolation.Type.FROM_FUNCTION + ); + } + + final HqlParser.JoinFunctionContext joinFunctionContext = (HqlParser.JoinFunctionContext) joinTargetContext; + final boolean lateral = joinFunctionContext.LATERAL() != null; + final DotIdentifierConsumer identifierConsumer = dotIdentifierConsumerStack.pop(); + final SqmSetReturningFunction function = (SqmSetReturningFunction) joinFunctionContext.setReturningFunction().accept( this ); + dotIdentifierConsumerStack.push( identifierConsumer ); + final SqmFunctionJoin join = new SqmFunctionJoin<>( + function, + alias, + joinType, + lateral, + (SqmRoot) sqmRoot + ); + processingStateStack.getCurrent().getPathRegistry().register( join ); + sqmRoot.addSqmJoin( (SqmJoin) join ); + return (SqmJoin) join; + } else { throw new ParsingException( "unexpected join type" ); } @@ -4490,6 +4543,26 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem : unquoteStringLiteral( ctx.STRING_LITERAL().getText() ).toLowerCase(); } + @Override + public SqmSetReturningFunction visitSimpleSetReturningFunction(HqlParser.SimpleSetReturningFunctionContext ctx) { + final String functionName = visitIdentifier( ctx.identifier() ); + final HqlParser.GenericFunctionArgumentsContext argumentsContext = ctx.genericFunctionArguments(); + @SuppressWarnings("unchecked") + final List> functionArguments = + argumentsContext == null + ? emptyList() + : (List>) argumentsContext.accept(this); + + SqmSetReturningFunctionDescriptor functionTemplate = getSetReturningFunctionDescriptor( functionName ); + if ( functionTemplate == null ) { + throw new SemanticException( + "The %s() set-returning function was not registered for the dialect".formatted( functionName ), + query + ); + } + return functionTemplate.generateSqmExpression( functionArguments, creationContext.getQueryEngine() ); + } + @Override public SqmExpression visitJpaNonstandardFunction(HqlParser.JpaNonstandardFunctionContext ctx) { final String functionName = toName( ctx.jpaNonstandardFunctionName() ); @@ -4834,6 +4907,10 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem return creationContext.getQueryEngine().getSqmFunctionRegistry().findFunctionDescriptor( name ); } + private SqmSetReturningFunctionDescriptor getSetReturningFunctionDescriptor(String name) { + return creationContext.getQueryEngine().getSqmFunctionRegistry().findSetReturningFunctionDescriptor( name ); + } + @Override public SqmExtractUnit visitDatetimeField(HqlParser.DatetimeFieldContext ctx) { final NodeBuilder nodeBuilder = creationContext.getNodeBuilder(); @@ -6021,7 +6098,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem public SqmPath visitMapKeyNavigablePath(HqlParser.MapKeyNavigablePathContext ctx) { final DotIdentifierConsumer consumer = dotIdentifierConsumerStack.getCurrent(); final boolean madeNested; - if ( consumer instanceof QualifiedJoinPathConsumer) { + if ( consumer instanceof QualifiedJoinPathConsumer ) { final QualifiedJoinPathConsumer qualifiedJoinPathConsumer = (QualifiedJoinPathConsumer) consumer; madeNested = !qualifiedJoinPathConsumer.isNested(); if ( madeNested ) { @@ -6035,8 +6112,21 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem final boolean hasContinuation = ctx.getChildCount() == 5; final SqmPathSource referencedPathSource = sqmPath.getReferencedPathSource(); - final TerminalNode firstNode = (TerminalNode) ctx.indexKeyQuantifier().getChild(0); - checkPluralPath( sqmPath, referencedPathSource, firstNode ); + final TerminalNode firstNode = (TerminalNode) ctx.indexKeyQuantifier().getChild( 0 ); + if ( firstNode.getSymbol().getType() == HqlParser.INDEX + && referencedPathSource instanceof AnonymousTupleType tupleType ) { + if ( tupleType.findSubPathSource( CollectionPart.Nature.INDEX.getName() ) == null ) { + throw new FunctionArgumentException( + String.format( + "The set-returning from node '%s' does not specify an index/ordinality", + sqmPath.getNavigablePath() + ) + ); + } + } + else { + checkPluralPath( sqmPath, referencedPathSource, firstNode ); + } if ( getCreationOptions().useStrictJpaCompliance() ) { final PluralPersistentAttribute attribute = (PluralPersistentAttribute) referencedPathSource; @@ -6068,6 +6158,18 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem SqmListJoin listJoin = (SqmListJoin) sqmPath; result = listJoin.resolvePathPart( CollectionPart.Nature.INDEX.getName(), true, this ); } + else if ( sqmPath instanceof SqmFunctionRoot functionRoot ) { + if ( hasContinuation ) { + throw new TerminalPathException("List index has no attributes"); + } + result = functionRoot.index(); + } + else if ( sqmPath instanceof SqmFunctionJoin functionJoin ) { + if ( hasContinuation ) { + throw new TerminalPathException("List index has no attributes"); + } + result = functionJoin.index(); + } else { assert sqmPath instanceof SqmPluralValuedSimplePath; final SqmPluralValuedSimplePath mapPath = (SqmPluralValuedSimplePath) sqmPath; @@ -6104,7 +6206,7 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem } private void checkPluralPath(SqmPath pluralAttributePath, SqmPathSource referencedPathSource, TerminalNode firstNode) { - if ( !(referencedPathSource instanceof PluralPersistentAttribute ) ) { + if ( !( referencedPathSource instanceof PluralPersistentAttribute ) ) { throw new FunctionArgumentException( String.format( "Argument of '%s' is not a plural path '%s'", diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/FromClauseAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/FromClauseAccessImpl.java index f32d457ddd..e79086b9c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/FromClauseAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/FromClauseAccessImpl.java @@ -11,6 +11,8 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Steve Ebersole */ @@ -37,6 +39,16 @@ public class FromClauseAccessImpl implements FromClauseAccess { return null; } + @Override + public @Nullable TableGroup findTableGroupByIdentificationVariable(String identificationVariable) { + for ( TableGroup tableGroup : tableGroupByPath.values() ) { + if ( tableGroup.findTableReference( identificationVariable ) != null ) { + return tableGroup; + } + } + return null; + } + @Override public TableGroup findTableGroupOnCurrentFromClause(NavigablePath navigablePath) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 131a67bc15..076ce95d8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -46,6 +46,7 @@ import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; import org.hibernate.query.sqm.tree.from.SqmRoot; @@ -801,6 +802,15 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext { @Override SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument); + @Override + SqmSetReturningFunction setReturningFunction(String name, Expression... args); + + @Override + SqmSetReturningFunction unnestArray(Expression array); + + @Override + SqmSetReturningFunction unnestCollection(Expression> collection); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java index a98e6bfd35..7fcf4e0932 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java @@ -32,6 +32,7 @@ import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; import org.hibernate.query.sqm.tree.domain.SqmFunctionPath; +import org.hibernate.query.sqm.tree.domain.SqmFunctionRoot; import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath; import org.hibernate.query.sqm.tree.domain.SqmListJoin; import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference; @@ -75,6 +76,7 @@ import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.SqmSummarization; import org.hibernate.query.sqm.tree.expression.SqmToDuration; @@ -88,6 +90,7 @@ import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFromClause; +import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmConflictClause; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; @@ -159,6 +162,8 @@ public interface SemanticQueryWalker { T visitRootDerived(SqmDerivedRoot sqmRoot); + T visitRootFunction(SqmFunctionRoot sqmRoot); + T visitRootCte(SqmCteRoot sqmRoot); T visitCrossJoin(SqmCrossJoin joinedFromElement); @@ -223,6 +228,8 @@ public interface SemanticQueryWalker { T visitQualifiedDerivedJoin(SqmDerivedJoin joinedFromElement); + T visitQualifiedFunctionJoin(SqmFunctionJoin joinedFromElement); + T visitQualifiedCteJoin(SqmCteJoin joinedFromElement); T visitBasicValuedPath(SqmBasicValuedSimplePath path); @@ -333,6 +340,8 @@ public interface SemanticQueryWalker { T visitFunction(SqmFunction tSqmFunction); + T visitSetReturningFunction(SqmSetReturningFunction tSqmFunction); + T visitExtractUnit(SqmExtractUnit extractUnit); T visitFormat(SqmFormat sqmFormat); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java index 6f56288072..de6e826f40 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/StrictJpaComplianceViolation.java @@ -27,6 +27,7 @@ public class StrictJpaComplianceViolation extends SemanticException { COLLATIONS( "use of collations" ), SUBQUERY_ORDER_BY( "use of ORDER BY clause in subquery" ), FROM_SUBQUERY( "use of subquery in FROM clause" ), + FROM_FUNCTION( "use of functions in FROM clause" ), SET_OPERATIONS( "use of set operations" ), CTES( "use of CTEs (common table expressions)" ), LIMIT_OFFSET_CLAUSE( "use of LIMIT/OFFSET clause" ), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingSetReturningFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingSetReturningFunctionDescriptor.java new file mode 100644 index 0000000000..c4be3e89cb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSelfRenderingSetReturningFunctionDescriptor.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.function; + +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentTypeResolver; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.query.sqm.tree.SqmTypedNode; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * @since 7.0 + */ +@Incubating +public abstract class AbstractSqmSelfRenderingSetReturningFunctionDescriptor + extends AbstractSqmSetReturningFunctionDescriptor implements SetReturningFunctionRenderer { + + public AbstractSqmSelfRenderingSetReturningFunctionDescriptor( + String name, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver typeResolver, + @Nullable FunctionArgumentTypeResolver argumentTypeResolver) { + super( name, argumentsValidator, typeResolver, argumentTypeResolver ); + } + + @Override + protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression( + List> arguments, + QueryEngine queryEngine) { + //noinspection unchecked + return new SelfRenderingSqmSetReturningFunction<>( + this, + this, + arguments, + getArgumentsValidator(), + getSetReturningTypeResolver(), + (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), + queryEngine.getCriteriaBuilder(), + getName() + ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSetReturningFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSetReturningFunctionDescriptor.java new file mode 100644 index 0000000000..31a7f681aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/AbstractSqmSetReturningFunctionDescriptor.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.function; + +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentTypeResolver; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.tree.SqmTypedNode; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * @since 7.0 + */ +@Incubating +public abstract class AbstractSqmSetReturningFunctionDescriptor implements SqmSetReturningFunctionDescriptor { + private final ArgumentsValidator argumentsValidator; + private final SetReturningFunctionTypeResolver setReturningTypeResolver; + private final FunctionArgumentTypeResolver functionArgumentTypeResolver; + private final String name; + + public AbstractSqmSetReturningFunctionDescriptor(String name, SetReturningFunctionTypeResolver typeResolver) { + this( name, null, typeResolver, null ); + } + + public AbstractSqmSetReturningFunctionDescriptor( + String name, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver typeResolver) { + this( name, argumentsValidator, typeResolver, null ); + } + + public AbstractSqmSetReturningFunctionDescriptor( + String name, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver typeResolver, + @Nullable FunctionArgumentTypeResolver argumentTypeResolver) { + this.name = name; + this.argumentsValidator = argumentsValidator == null + ? StandardArgumentsValidators.NONE + : argumentsValidator; + this.setReturningTypeResolver = typeResolver; + this.functionArgumentTypeResolver = argumentTypeResolver == null + ? StandardFunctionArgumentTypeResolvers.NULL + : argumentTypeResolver; + } + + public String getName() { + return name; + } + + public String getSignature(String name) { + return name + getArgumentListSignature(); + } + + @Override + public ArgumentsValidator getArgumentsValidator() { + return argumentsValidator; + } + + public SetReturningFunctionTypeResolver getSetReturningTypeResolver() { + return setReturningTypeResolver; + } + + public FunctionArgumentTypeResolver getArgumentTypeResolver() { + return functionArgumentTypeResolver; + } + + public String getArgumentListSignature() { + return argumentsValidator.getSignature(); + } + + @Override + public final SelfRenderingSqmSetReturningFunction generateSqmExpression( + List> arguments, + QueryEngine queryEngine) { + argumentsValidator.validate( arguments, getName(), queryEngine.getTypeConfiguration() ); + + return generateSqmSetReturningFunctionExpression( arguments, queryEngine ); + } + + /** + * Return an SQM node or subtree representing an invocation of this function + * with the given arguments. This method may be overridden in the case of + * function descriptors that wish to customize creation of the node. + * + * @param arguments the arguments of the function invocation + */ + protected abstract SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression( + List> arguments, + QueryEngine queryEngine); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java index ed58b09c13..fa97832280 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/FunctionKind.java @@ -13,5 +13,6 @@ public enum FunctionKind { NORMAL, AGGREGATE, ORDERED_SET_AGGREGATE, - WINDOW + WINDOW, + SET_RETURNING } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmSetReturningFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmSetReturningFunctionDescriptor.java new file mode 100644 index 0000000000..8eab6bc813 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/NamedSqmSetReturningFunctionDescriptor.java @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.function; + +import java.util.List; +import java.util.Locale; + +import org.hibernate.Incubating; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentTypeResolver; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Provides a standard implementation that supports the majority of the HQL + * functions that are translated to SQL. The Dialect and its sub-classes use + * this class to provide details required for processing of the associated + * function. + * + * @since 7.0 + */ +@Incubating +public class NamedSqmSetReturningFunctionDescriptor + extends AbstractSqmSelfRenderingSetReturningFunctionDescriptor { + private final String functionName; + private final String argumentListSignature; + private final SqlAstNodeRenderingMode argumentRenderingMode; + + public NamedSqmSetReturningFunctionDescriptor( + String functionName, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver returnTypeResolver, + @Nullable FunctionArgumentTypeResolver argumentTypeResolver, + String name, + String argumentListSignature, + SqlAstNodeRenderingMode argumentRenderingMode) { + super( name, argumentsValidator, returnTypeResolver, argumentTypeResolver ); + + this.functionName = functionName; + this.argumentListSignature = argumentListSignature; + this.argumentRenderingMode = argumentRenderingMode; + } + + /** + * Function name accessor + * + * @return The function name. + */ + public String getName() { + return functionName; + } + + @Override + public String getArgumentListSignature() { + return argumentListSignature == null ? super.getArgumentListSignature() : argumentListSignature; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + AnonymousTupleTableGroupProducer returnType, + String tableIdentifierVariable, + SqlAstTranslator translator) { + translator.renderNamedSetReturningFunction( functionName, sqlAstArguments, returnType, tableIdentifierVariable, argumentRenderingMode ); + } + + @Override + public String toString() { + return String.format( + Locale.ROOT, + "NamedSqmSetReturningFunctionTemplate(%s)", + functionName + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmSetReturningFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmSetReturningFunction.java new file mode 100644 index 0000000000..f717f5613c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmSetReturningFunction.java @@ -0,0 +1,185 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.function; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.FunctionExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.from.FunctionTableGroup; +import org.hibernate.sql.ast.tree.from.TableGroup; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Collections.emptyList; + +/** + * @since 7.0 + */ +@Incubating +public class SelfRenderingSqmSetReturningFunction extends SqmSetReturningFunction { + private final @Nullable ArgumentsValidator argumentsValidator; + private final SetReturningFunctionTypeResolver setReturningTypeResolver; + private final SetReturningFunctionRenderer renderer; + + public SelfRenderingSqmSetReturningFunction( + SqmSetReturningFunctionDescriptor descriptor, + SetReturningFunctionRenderer renderer, + List> arguments, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver setReturningTypeResolver, + AnonymousTupleType type, + NodeBuilder nodeBuilder, + String name) { + super( name, descriptor, type, arguments, nodeBuilder ); + this.renderer = renderer; + this.argumentsValidator = argumentsValidator; + this.setReturningTypeResolver = setReturningTypeResolver; + } + + @Override + public SelfRenderingSqmSetReturningFunction copy(SqmCopyContext context) { + final SelfRenderingSqmSetReturningFunction existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = new ArrayList<>( getArguments().size() ); + for ( SqmTypedNode argument : getArguments() ) { + arguments.add( argument.copy( context ) ); + } + return context.registerCopy( + this, + new SelfRenderingSqmSetReturningFunction<>( + getFunctionDescriptor(), + getFunctionRenderer(), + arguments, + getArgumentsValidator(), + getSetReturningTypeResolver(), + getType(), + nodeBuilder(), + getFunctionName() + ) + ); + } + + public SetReturningFunctionRenderer getFunctionRenderer() { + return renderer; + } + + protected @Nullable ArgumentsValidator getArgumentsValidator() { + return argumentsValidator; + } + + public SetReturningFunctionTypeResolver getSetReturningTypeResolver() { + return setReturningTypeResolver; + } + + protected List resolveSqlAstArguments(List> sqmArguments, SqmToSqlAstConverter walker) { + if ( sqmArguments.isEmpty() ) { + return emptyList(); + } + final ArrayList sqlAstArguments = new ArrayList<>( sqmArguments.size() ); + for ( int i = 0; i < sqmArguments.size(); i++ ) { + sqlAstArguments.add( + (SqlAstNode) sqmArguments.get( i ).accept( walker ) + ); + } + return sqlAstArguments; + } + + @Override + public TableGroup convertToSqlAst( + NavigablePath navigablePath, + String identifierVariable, + boolean lateral, + boolean canUseInnerJoins, + boolean withOrdinality, + SqmToSqlAstConverter walker) { + final List arguments = resolveSqlAstArguments( getArguments(), walker ); + final ArgumentsValidator validator = argumentsValidator; + if ( validator != null ) { + validator.validateSqlTypes( arguments, getFunctionName() ); + } + final SelectableMapping[] selectableMappings = getSetReturningTypeResolver().resolveFunctionReturnType( + arguments, + identifierVariable, + withOrdinality, + walker.getCreationContext().getTypeConfiguration() + ); + final AnonymousTupleTableGroupProducer tableGroupProducer = getType().resolveTableGroupProducer( + identifierVariable, + selectableMappings, + walker.getFromClauseAccess() + ); + return new FunctionTableGroup( + navigablePath, + tableGroupProducer, + new SetReturningFunctionExpression( + getFunctionName(), + getFunctionRenderer(), + arguments, + tableGroupProducer, + identifierVariable + ), + identifierVariable, + tableGroupProducer.getColumnNames(), + tableGroupProducer.getCompatibleTableExpressions(), + lateral, + canUseInnerJoins, + getFunctionRenderer().rendersIdentifierVariable( arguments, walker.getCreationContext().getSessionFactory() ), + walker.getCreationContext().getSessionFactory() + ); + } + + private record SetReturningFunctionExpression( + String functionName, + SetReturningFunctionRenderer functionRenderer, + List arguments, + AnonymousTupleTableGroupProducer tableGroupProducer, + String tableIdentifierVariable + ) implements SelfRenderingExpression, FunctionExpression { + + @Override + public String getFunctionName() { + return functionName; + } + + @Override + public List getArguments() { + return arguments; + } + + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + functionRenderer.render( sqlAppender, arguments, tableGroupProducer, tableIdentifierVariable, walker ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return tableGroupProducer; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SetReturningFunctionRenderer.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SetReturningFunctionRenderer.java new file mode 100644 index 0000000000..b8583b4570 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SetReturningFunctionRenderer.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.function; + +import java.util.List; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.from.FunctionTableGroup; + +/** + * Support for {@link SqmSetReturningFunctionDescriptor}s that ultimately want + * to perform SQL rendering themselves. This is a protocol passed + * from the {@link AbstractSqmSelfRenderingSetReturningFunctionDescriptor} + * along to its {@link SelfRenderingSqmSetReturningFunction} and ultimately to + * the {@link FunctionTableGroup} which calls it + * to finally render SQL. + * + * @since 7.0 + */ +@FunctionalInterface +public interface SetReturningFunctionRenderer { + + void render( + SqlAppender sqlAppender, + List sqlAstArguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker); + + default boolean rendersIdentifierVariable(List arguments, SessionFactoryImplementor sessionFactory) { + return false; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java index ba391a48f8..3b108b59d3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionRegistry.java @@ -12,12 +12,16 @@ import java.util.stream.Stream; import org.hibernate.internal.util.collections.CaseInsensitiveDictionary; import org.hibernate.query.sqm.produce.function.FunctionParameterType; import org.hibernate.query.sqm.produce.function.NamedFunctionDescriptorBuilder; +import org.hibernate.query.sqm.produce.function.NamedSetReturningFunctionDescriptorBuilder; import org.hibernate.query.sqm.produce.function.PatternFunctionDescriptorBuilder; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; import org.hibernate.type.BasicType; import org.hibernate.type.spi.TypeConfiguration; import org.jboss.logging.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.lang.String.CASE_INSENSITIVE_ORDER; import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.useArgType; @@ -35,6 +39,7 @@ public class SqmFunctionRegistry { private static final Logger log = Logger.getLogger( SqmFunctionRegistry.class ); private final CaseInsensitiveDictionary functionMap = new CaseInsensitiveDictionary<>(); + private final CaseInsensitiveDictionary setReturningFunctionMap = new CaseInsensitiveDictionary<>(); private final CaseInsensitiveDictionary alternateKeyMap = new CaseInsensitiveDictionary<>(); public SqmFunctionRegistry() { @@ -61,11 +66,27 @@ public class SqmFunctionRegistry { return sortedFunctionMap.entrySet().stream(); } + /** + * Useful for diagnostics - not efficient: do not use in production code. + * + * @return + */ + public Stream> getSetReturningFunctionsByName() { + final Map sortedFunctionMap = new TreeMap<>( CASE_INSENSITIVE_ORDER ); + for ( Map.Entry e : setReturningFunctionMap.unmodifiableEntrySet() ) { + sortedFunctionMap.put( e.getKey(), e.getValue() ); + } + for ( Map.Entry e : alternateKeyMap.unmodifiableEntrySet() ) { + sortedFunctionMap.put( e.getKey(), setReturningFunctionMap.get( e.getValue() ) ); + } + return sortedFunctionMap.entrySet().stream(); + } + /** * Find a {@link SqmFunctionDescriptor} by name. * Returns {@code null} if no such function is found. */ - public SqmFunctionDescriptor findFunctionDescriptor(String functionName) { + public @Nullable SqmFunctionDescriptor findFunctionDescriptor(String functionName) { SqmFunctionDescriptor found = null; final String alternateKeyResolution = alternateKeyMap.get( functionName ); @@ -80,6 +101,25 @@ public class SqmFunctionRegistry { return found; } + /** + * Find a {@link SqmSetReturningFunctionDescriptor} by name. + * Returns {@code null} if no such function is found. + */ + public @Nullable SqmSetReturningFunctionDescriptor findSetReturningFunctionDescriptor(String functionName) { + SqmSetReturningFunctionDescriptor found = null; + + final String alternateKeyResolution = alternateKeyMap.get( functionName ); + if ( alternateKeyResolution != null ) { + found = setReturningFunctionMap.get( alternateKeyResolution ); + } + + if ( found == null ) { + found = setReturningFunctionMap.get( functionName ); + } + + return found; + } + /** * Register a function descriptor by name */ @@ -95,6 +135,21 @@ public class SqmFunctionRegistry { return function; } + /** + * Register a set returning function descriptor by name + */ + public SqmSetReturningFunctionDescriptor register(String registrationKey, SqmSetReturningFunctionDescriptor function) { + final SqmSetReturningFunctionDescriptor priorRegistration = setReturningFunctionMap.put( registrationKey, function ); + log.debugf( + "Registered SqmSetReturningFunctionTemplate [%s] under %s; prior registration was %s", + function, + registrationKey, + priorRegistration + ); + alternateKeyMap.remove( registrationKey ); + return function; + } + /** * Register a pattern-based descriptor by name. Shortcut for building the descriptor * via {@link #patternDescriptorBuilder} accepting its defaults. @@ -211,6 +266,22 @@ public class SqmFunctionRegistry { return namedWindowDescriptorBuilder( name, name ); } + /** + * Get a builder for creating and registering a name-based set-returning function descriptor + * using the passed name as both the registration key and underlying SQL + * function name + * + * @param name The function name (and registration key) + * @param typeResolver The type resolver to use + * + * @return The builder + */ + public NamedSetReturningFunctionDescriptorBuilder namedSetReturningDescriptorBuilder( + String name, + SetReturningFunctionTypeResolver typeResolver) { + return namedSetReturningDescriptorBuilder( name, name, typeResolver ); + } + /** * Get a builder for creating and registering a name-based function descriptor. * @@ -259,6 +330,22 @@ public class SqmFunctionRegistry { return new NamedFunctionDescriptorBuilder( this, registrationKey, FunctionKind.WINDOW, name ); } + /** + * Get a builder for creating and registering a name-based set-returning function descriptor. + * + * @param registrationKey The name under which the descriptor will get registered + * @param name The underlying SQL function name to use + * @param typeResolver The type resolver to use + * + * @return The builder + */ + public NamedSetReturningFunctionDescriptorBuilder namedSetReturningDescriptorBuilder( + String registrationKey, + String name, + SetReturningFunctionTypeResolver typeResolver) { + return new NamedSetReturningFunctionDescriptorBuilder( this, registrationKey, name, typeResolver ); + } + public NamedFunctionDescriptorBuilder noArgsBuilder(String name) { return noArgsBuilder( name, name ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmSetReturningFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmSetReturningFunctionDescriptor.java new file mode 100644 index 0000000000..1dcf63beff --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmSetReturningFunctionDescriptor.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.function; + +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.query.sqm.tree.SqmTypedNode; + +/** + * A factory for SQM nodes representing invocations of a certain + * named set-returning function. + *

+ * When a function call is encountered in the text of an HQL query, + * a {@code SqmSetReturningFunctionDescriptor} for the given name is obtained + * from the {@link SqmFunctionRegistry}, and the + * {@link #generateSqmExpression} method is called with SQM nodes + * representing the invocation arguments. It is the responsibility + * of the {@code SqmSetReturningFunctionDescriptor} to produce a subtree of SQM + * nodes representing the function invocation. + *

+ * The resulting subtree might be quite complex, since the + * {@code SqmSetReturningFunctionDescriptor} is permitted to perform syntactic + * de-sugaring. On the other hand, {@link #generateSqmExpression} + * returns {@link SelfRenderingSqmSetReturningFunction}, which is an object + * that is permitted to take over the logic of producing the + * SQL AST subtree, so de-sugaring may also be performed there. + *

+ * User-written function descriptors may be contributed via a + * {@link org.hibernate.boot.model.FunctionContributor}. + * The {@link SqmFunctionRegistry} exposes methods which simplify + * the definition of a function, including + * {@link SqmFunctionRegistry#namedSetReturningDescriptorBuilder(String, SetReturningFunctionTypeResolver)}. + * + * @see SqmFunctionRegistry + * @see org.hibernate.boot.model.FunctionContributor + * + * @since 7.0 + */ +@Incubating +public interface SqmSetReturningFunctionDescriptor { + + /** + * Instantiate this template with the given arguments and. + * This produces a tree of SQM nodes + * representing a tree of function invocations. This allows + * a single HQL function to be defined in terms of other + * predefined (database independent) HQL functions, + * simplifying the task of writing HQL functions which are + * portable between databases. + * + */ + SelfRenderingSqmSetReturningFunction generateSqmExpression( + List> arguments, + QueryEngine queryEngine); + + /** + * Used only for pretty-printing the function signature in the log. + * + * @param name the function name + * @return the signature of the function + */ + default String getSignature(String name) { + return name; + } + + /** + * The object responsible for validating arguments of the function. + * + * @return an instance of {@link ArgumentsValidator} + */ + ArgumentsValidator getArgumentsValidator(); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 7223bdc6b9..e5c3da489c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -85,6 +85,7 @@ import org.hibernate.query.sqm.TrimSpec; import org.hibernate.query.sqm.UnaryArithmeticOperator; import org.hibernate.query.sqm.function.NamedSqmFunctionDescriptor; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SqmSetReturningFunctionDescriptor; import org.hibernate.query.sqm.produce.function.FunctionArgumentException; import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; @@ -131,6 +132,7 @@ import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; import org.hibernate.query.sqm.tree.expression.SqmOver; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.SqmToDuration; import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification; @@ -2256,6 +2258,10 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { return queryEngine.getSqmFunctionRegistry().findFunctionDescriptor( name ); } + private SqmSetReturningFunctionDescriptor getSetReturningFunctionDescriptor(String name) { + return queryEngine.getSqmFunctionRegistry().findSetReturningFunctionDescriptor( name ); + } + @Override public SqmCaseSimple selectCase(Expression expression) { //noinspection unchecked @@ -5812,4 +5818,28 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { public SqmExpression xmlagg(JpaOrder order, JpaPredicate filter, JpaWindow window, Expression argument) { return functionWithinGroup( "xmlagg", String.class, order, filter, window, argument ); } + + @Override + public SqmSetReturningFunction setReturningFunction(String name, Expression... args) { + return getSetReturningFunctionDescriptor( name ).generateSqmExpression( + expressionList( args ), + queryEngine + ); + } + + @Override + public SqmSetReturningFunction unnestArray(Expression array) { + return getSetReturningFunctionDescriptor( "unnest" ).generateSqmExpression( + asList( (SqmTypedNode) array ), + queryEngine + ); + } + + @Override + public SqmSetReturningFunction unnestCollection(Expression> collection) { + return getSetReturningFunctionDescriptor( "unnest" ).generateSqmExpression( + asList( (SqmTypedNode) collection ), + queryEngine + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java index 0c6f87b72e..20faf7cc8f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmTreePrinter.java @@ -25,6 +25,7 @@ import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; import org.hibernate.query.sqm.tree.domain.SqmFunctionPath; +import org.hibernate.query.sqm.tree.domain.SqmFunctionRoot; import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath; import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference; import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; @@ -64,6 +65,7 @@ import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.SqmSummarization; import org.hibernate.query.sqm.tree.expression.SqmToDuration; @@ -78,6 +80,7 @@ import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; +import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmConflictClause; @@ -555,6 +558,18 @@ public class SqmTreePrinter implements SemanticQueryWalker { return null; } + @Override + public Object visitRootFunction(SqmFunctionRoot sqmRoot) { + processStanza( + "derived", + "`" + sqmRoot.getNavigablePath() + "`", + () -> { + processJoins( sqmRoot ); + } + ); + return null; + } + @Override public Object visitRootCte(SqmCteRoot sqmRoot) { processStanza( @@ -670,6 +685,24 @@ public class SqmTreePrinter implements SemanticQueryWalker { return null; } + @Override + public Object visitQualifiedFunctionJoin(SqmFunctionJoin joinedFromElement) { + if ( inJoinPredicate ) { + logWithIndentation( "-> [joined-path] - `%s`", joinedFromElement.getNavigablePath() ); + } + else { + processStanza( + "derived", + "`" + joinedFromElement.getNavigablePath() + "`", + () -> { + processJoinPredicate( joinedFromElement ); + processJoins( joinedFromElement ); + } + ); + } + return null; + } + @Override public Object visitQualifiedCteJoin(SqmCteJoin joinedFromElement) { if ( inJoinPredicate ) { @@ -828,6 +861,11 @@ public class SqmTreePrinter implements SemanticQueryWalker { return null; } + @Override + public Object visitSetReturningFunction(SqmSetReturningFunction tSqmFunction) { + return null; + } + @Override public Object visitCoalesce(SqmCoalesce sqmCoalesce) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/NamedSetReturningFunctionDescriptorBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/NamedSetReturningFunctionDescriptorBuilder.java new file mode 100644 index 0000000000..b2a101de64 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/NamedSetReturningFunctionDescriptorBuilder.java @@ -0,0 +1,96 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.produce.function; + +import org.hibernate.Incubating; +import org.hibernate.query.sqm.function.NamedSqmSetReturningFunctionDescriptor; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.query.sqm.function.SqmSetReturningFunctionDescriptor; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; + +/** + * @since 7.0 + */ +@Incubating +public class NamedSetReturningFunctionDescriptorBuilder { + + private final SqmFunctionRegistry registry; + private final String registrationKey; + + private final String functionName; + private final SetReturningFunctionTypeResolver setReturningTypeResolver; + + private ArgumentsValidator argumentsValidator; + private FunctionArgumentTypeResolver argumentTypeResolver; + + private String argumentListSignature; + private SqlAstNodeRenderingMode argumentRenderingMode = SqlAstNodeRenderingMode.DEFAULT; + + public NamedSetReturningFunctionDescriptorBuilder( + SqmFunctionRegistry registry, + String registrationKey, + String functionName, + SetReturningFunctionTypeResolver typeResolver) { + this.registry = registry; + this.registrationKey = registrationKey; + this.functionName = functionName; + this.setReturningTypeResolver = typeResolver; + } + + public NamedSetReturningFunctionDescriptorBuilder setArgumentsValidator(ArgumentsValidator argumentsValidator) { + this.argumentsValidator = argumentsValidator; + return this; + } + + public NamedSetReturningFunctionDescriptorBuilder setArgumentTypeResolver(FunctionArgumentTypeResolver argumentTypeResolver) { + this.argumentTypeResolver = argumentTypeResolver; + return this; + } + + public NamedSetReturningFunctionDescriptorBuilder setArgumentCountBetween(int min, int max) { + return setArgumentsValidator( StandardArgumentsValidators.between( min, max ) ); + } + + public NamedSetReturningFunctionDescriptorBuilder setExactArgumentCount(int exactArgumentCount) { + return setArgumentsValidator( StandardArgumentsValidators.exactly( exactArgumentCount ) ); + } + + public NamedSetReturningFunctionDescriptorBuilder setMinArgumentCount(int min) { + return setArgumentsValidator( StandardArgumentsValidators.min( min ) ); + } + + public NamedSetReturningFunctionDescriptorBuilder setParameterTypes(FunctionParameterType... types) { + setArgumentsValidator( new ArgumentTypesValidator(argumentsValidator, types) ); + setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.invariant( types ) ); + return this; + } + + public NamedSetReturningFunctionDescriptorBuilder setArgumentListSignature(String argumentListSignature) { + this.argumentListSignature = argumentListSignature; + return this; + } + + public NamedSetReturningFunctionDescriptorBuilder setArgumentRenderingMode(SqlAstNodeRenderingMode argumentRenderingMode) { + this.argumentRenderingMode = argumentRenderingMode; + return this; + } + + public SqmSetReturningFunctionDescriptor register() { + return registry.register( registrationKey, descriptor() ); + } + + public SqmSetReturningFunctionDescriptor descriptor() { + return new NamedSqmSetReturningFunctionDescriptor( + functionName, + argumentsValidator, + setReturningTypeResolver, + argumentTypeResolver, + registrationKey, + argumentListSignature, + argumentRenderingMode + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/SetReturningFunctionTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/SetReturningFunctionTypeResolver.java new file mode 100644 index 0000000000..e4352e211d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/SetReturningFunctionTypeResolver.java @@ -0,0 +1,105 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.produce.function; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlExpressible; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.sqm.produce.function.internal.SetReturningFunctionTypeResolverBuilder; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.BasicType; +import org.hibernate.type.BasicTypeReference; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +/** + * Pluggable strategy for resolving a function return type for a specific call. + * + * @since 7.0 + */ +@Incubating +public interface SetReturningFunctionTypeResolver { + + /** + * Resolve the return type for a function given its arguments to this call. + * + * @return The resolved type. + */ + AnonymousTupleType resolveTupleType(List> arguments, TypeConfiguration typeConfiguration); + + /** + * Resolve the tuple elements {@link SqlExpressible} for a function given its arguments to this call. + * + * @return The resolved JdbcMapping. + */ + SelectableMapping[] resolveFunctionReturnType( + List arguments, + String tableIdentifierVariable, + boolean withOrdinality, + TypeConfiguration typeConfiguration); + + /** + * Creates a builder for a type resolver. + */ + static Builder builder() { + return new SetReturningFunctionTypeResolverBuilder(); + } + + /** + * Pluggable strategy for resolving a function return type for a specific call. + * + * @since 7.0 + */ + @Incubating + interface Builder { + + /** + * Like {@link #invariant(String, BasicTypeReference, String)}, but passing the component as selection expression. + * + * @see #invariant(String, BasicTypeReference, String) + */ + Builder invariant(String component, BasicTypeReference invariantType); + + /** + * Specifies that the return type has a component with the given name being selectable through the given + * selection expression, which has the given invariant type. + */ + Builder invariant(String component, BasicTypeReference invariantType, String selectionExpression); + + /** + * Like {@link #invariant(String, BasicType, String)}, but passing the component as selection expression. + * + * @see #invariant(String, BasicType, String) + */ + Builder invariant(String component, BasicType invariantType); + + /** + * Specifies that the return type has a component with the given name being selectable through the given + * selection expression, which has the given invariant type. + */ + Builder invariant(String component, BasicType invariantType, String selectionExpression); + + /** + * Like {@link #useArgType(String, int, String)}, but passing the component as selection expression. + * + * @see #useArgType(String, int, String) + */ + Builder useArgType(String component, int argPosition); + + /** + * Specifies that the return type has a component with the given name being selectable through the given + * selection expression, which has the same type as the argument of the given 0-based position. + */ + Builder useArgType(String component, int argPosition, String selectionExpression); + + /** + * Builds a type resolver. + */ + SetReturningFunctionTypeResolver build(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/SetReturningFunctionTypeResolverBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/SetReturningFunctionTypeResolverBuilder.java new file mode 100644 index 0000000000..d133f02574 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/internal/SetReturningFunctionTypeResolverBuilder.java @@ -0,0 +1,251 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.produce.function.internal; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicType; +import org.hibernate.type.BasicTypeReference; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.LinkedHashMap; +import java.util.List; + +/** + * @since 7.0 + */ +@Incubating +public class SetReturningFunctionTypeResolverBuilder implements SetReturningFunctionTypeResolver.Builder { + + private final LinkedHashMap typeResolvers = new LinkedHashMap<>(); + + @Override + public SetReturningFunctionTypeResolver.Builder invariant(String component, BasicTypeReference invariantType) { + return invariant( component, invariantType, component ); + } + + @Override + public SetReturningFunctionTypeResolverBuilder invariant(String component, BasicType invariantType) { + return invariant( component, invariantType, component ); + } + + @Override + public SetReturningFunctionTypeResolverBuilder useArgType(String component, int argPosition) { + return useArgType( component, argPosition, component ); + } + + @Override + public SetReturningFunctionTypeResolver.Builder invariant(String component, BasicTypeReference invariantType, String selectionExpression) { + if ( invariantType == null ) { + throw new IllegalArgumentException( "Passed `invariantType` for function return cannot be null" ); + } + if ( selectionExpression == null ) { + throw new IllegalArgumentException( "Passed `selectionExpression` for function return cannot be null" ); + } + return withComponent( component, new BasicTypeReferenceTypeResolver( component, selectionExpression, invariantType ) ); + } + + @Override + public SetReturningFunctionTypeResolverBuilder invariant(String component, BasicType invariantType, String selectionExpression) { + if ( invariantType == null ) { + throw new IllegalArgumentException( "Passed `invariantType` for function return cannot be null" ); + } + if ( selectionExpression == null ) { + throw new IllegalArgumentException( "Passed `selectionExpression` for function return cannot be null" ); + } + return withComponent( component, new BasicTypeTypeResolver( component, selectionExpression, invariantType ) ); + } + + @Override + public SetReturningFunctionTypeResolverBuilder useArgType(String component, int argPosition, String selectionExpression) { + if ( selectionExpression == null ) { + throw new IllegalArgumentException( "Passed `selectionExpression` for function return cannot be null" ); + } + return withComponent( component, new ArgTypeTypeResolver( component, selectionExpression, argPosition ) ); + } + + private SetReturningFunctionTypeResolverBuilder withComponent(String component, TypeResolver resolver) { + if ( component == null ) { + throw new IllegalArgumentException( "Passed `component` for function return cannot be null" ); + } + typeResolvers.put( component, resolver ); + return this; + } + + @Override + public SetReturningFunctionTypeResolver build() { + return new SetReturningFunctionTypeResolverImpl( this ); + } + + private static class SetReturningFunctionTypeResolverImpl implements SetReturningFunctionTypeResolver { + + private final TypeResolver[] typeResolvers; + + public SetReturningFunctionTypeResolverImpl(SetReturningFunctionTypeResolverBuilder builder) { + this.typeResolvers = builder.typeResolvers.values().toArray( new TypeResolver[0] ); + } + + @Override + public AnonymousTupleType resolveTupleType(List> arguments, TypeConfiguration typeConfiguration) { + final SqmExpressible[] componentTypes = new SqmExpressible[typeResolvers.length + 1]; + final String[] componentNames = new String[typeResolvers.length + 1]; + int i = 0; + for ( TypeResolver typeResolver : typeResolvers ) { + componentNames[i] = typeResolver.componentName(); + componentTypes[i] = typeResolver.resolveTupleType( arguments, typeConfiguration ); + i++; + } + componentTypes[i] = typeConfiguration.getBasicTypeForJavaType( Long.class ); + componentNames[i] = CollectionPart.Nature.INDEX.getName(); + return new AnonymousTupleType<>( componentTypes, componentNames ); + } + + @Override + public SelectableMapping[] resolveFunctionReturnType( + List arguments, + String tableIdentifierVariable, + boolean withOrdinality, + TypeConfiguration typeConfiguration) { + final SelectableMapping[] selectableMappings = new SelectableMapping[typeResolvers.length + (withOrdinality ? 1 : 0)]; + int i = 0; + for ( TypeResolver typeResolver : typeResolvers ) { + final JdbcMapping jdbcMapping = typeResolver.resolveFunctionReturnType( arguments, typeConfiguration ); + selectableMappings[i] = new SelectableMappingImpl( + "", + typeResolver.selectionExpression(), + new SelectablePath( typeResolver.componentName() ), + null, + null, + null, + null, + null, + null, + null, + false, + true, + false, + false, + false, + false, + jdbcMapping + ); + i++; + } + if ( withOrdinality ) { + selectableMappings[i] = new SelectableMappingImpl( + "", + determineIndexSelectionExpression( selectableMappings, tableIdentifierVariable, typeConfiguration ), + new SelectablePath( CollectionPart.Nature.INDEX.getName() ), + null, + null, + null, + null, + null, + null, + null, + false, + false, + false, + false, + false, + false, + typeConfiguration.getBasicTypeForJavaType( Long.class ) + ); + } + return selectableMappings; + } + + private String determineIndexSelectionExpression(SelectableMapping[] selectableMappings, String tableIdentifierVariable, TypeConfiguration typeConfiguration) { + final String defaultOrdinalityColumnName = typeConfiguration.getSessionFactory().getJdbcServices() + .getDialect() + .getDefaultOrdinalityColumnName(); + String name = defaultOrdinalityColumnName == null ? "i" : defaultOrdinalityColumnName; + OUTER: for ( int i = 0; i < selectableMappings.length; i++ ) { + for ( SelectableMapping selectableMapping : selectableMappings ) { + if ( selectableMapping != null ) { + if ( selectableMapping.getSelectionExpression().equals( name ) ) { + name += '_'; + continue OUTER; + } + } + } + break; + } + return name; + } + } + + private interface TypeResolver { + + String componentName(); + + String selectionExpression(); + + SqmExpressible resolveTupleType(List> arguments, TypeConfiguration typeConfiguration); + + JdbcMapping resolveFunctionReturnType(List arguments, TypeConfiguration typeConfiguration); + } + + private record BasicTypeReferenceTypeResolver( + String componentName, + String selectionExpression, + BasicTypeReference basicTypeReference + ) implements TypeResolver { + + @Override + public SqmExpressible resolveTupleType(List> arguments, TypeConfiguration typeConfiguration) { + return typeConfiguration.getBasicTypeRegistry().resolve( basicTypeReference ); + } + + @Override + public JdbcMapping resolveFunctionReturnType(List arguments, TypeConfiguration typeConfiguration) { + return typeConfiguration.getBasicTypeRegistry().resolve( basicTypeReference ); + } + } + + private record BasicTypeTypeResolver( + String componentName, + String selectionExpression, + BasicType basicType + ) implements TypeResolver { + + @Override + public SqmExpressible resolveTupleType(List> arguments, TypeConfiguration typeConfiguration) { + return basicType; + } + + @Override + public JdbcMapping resolveFunctionReturnType(List arguments, TypeConfiguration typeConfiguration) { + return basicType; + } + } + + private record ArgTypeTypeResolver( + String componentName, + String selectionExpression, + int argPosition + ) implements TypeResolver { + + @Override + public SqmExpressible resolveTupleType(List> arguments, TypeConfiguration typeConfiguration) { + return arguments.get( argPosition ).getExpressible(); + } + + @Override + public JdbcMapping resolveFunctionReturnType(List arguments, TypeConfiguration typeConfiguration) { + return ((Expression) arguments.get( argPosition )).getExpressionType().getSingleJdbcMapping(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java index 3dd3012500..29f386c518 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/BaseSemanticQueryWalker.java @@ -10,7 +10,7 @@ import org.hibernate.metamodel.model.domain.DiscriminatorSqmPath; import org.hibernate.metamodel.model.domain.internal.AnyDiscriminatorSqmPath; import org.hibernate.query.sqm.InterpretationException; import org.hibernate.query.sqm.SemanticQueryWalker; -import org.hibernate.query.sqm.tree.SqmVisitableNode; +import org.hibernate.query.sqm.tree.SqmTypedNode; import org.hibernate.query.sqm.tree.cte.SqmCteContainer; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; @@ -24,6 +24,7 @@ import org.hibernate.query.sqm.tree.domain.SqmElementAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; +import org.hibernate.query.sqm.tree.domain.SqmFunctionRoot; import org.hibernate.query.sqm.tree.domain.SqmIndexAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmFunctionPath; import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath; @@ -67,6 +68,7 @@ import org.hibernate.query.sqm.tree.expression.SqmOver; import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.SqmSummarization; import org.hibernate.query.sqm.tree.expression.SqmToDuration; @@ -81,6 +83,7 @@ import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; +import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmConflictClause; @@ -326,6 +329,9 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker ) { consumeDerivedJoin( ( (SqmDerivedJoin) sqmJoin ), transitive ); } + else if ( sqmJoin instanceof SqmFunctionJoin ) { + consumeFunctionJoin( (SqmFunctionJoin) sqmJoin, transitive ); + } else if ( sqmJoin instanceof SqmCteJoin ) { consumeCteJoin( ( (SqmCteJoin) sqmJoin ), transitive ); } @@ -371,6 +377,16 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker sqmJoin, boolean transitive) { + sqmJoin.getFunction().accept( this ); + if ( sqmJoin.getJoinPredicate() != null ) { + sqmJoin.getJoinPredicate().accept( this ); + } + if ( transitive ) { + consumeExplicitJoins( sqmJoin ); + } + } + protected void consumeCteJoin(SqmCteJoin sqmJoin, boolean transitive) { if ( sqmJoin.getJoinPredicate() != null ) { sqmJoin.getJoinPredicate().accept( this ); @@ -399,6 +415,11 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker sqmRoot) { return sqmRoot; @@ -431,6 +452,11 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker joinedFromElement) { + return joinedFromElement; + } + @Override public Object visitQualifiedCteJoin(SqmCteJoin joinedFromElement) { return joinedFromElement; @@ -752,13 +778,9 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker sqmFunction) { - sqmFunction.getArguments().forEach( - e -> { - if ( e instanceof SqmVisitableNode ) { - ( (SqmVisitableNode) e ).accept( this ); - } - } - ); + for ( SqmTypedNode argument : sqmFunction.getArguments() ) { + argument.accept( this ); + } if ( sqmFunction instanceof SqmAggregateFunction ) { final SqmPredicate filter = ( (SqmAggregateFunction) sqmFunction ).getFilter(); if ( filter != null ) { @@ -771,6 +793,14 @@ public abstract class BaseSemanticQueryWalker implements SemanticQueryWalker sqmFunction) { + for ( SqmTypedNode argument : sqmFunction.getArguments() ) { + argument.accept( this ); + } + return sqmFunction; + } + @Override public Object visitModifiedSubQueryExpression(SqmModifiedSubQueryExpression expression) { return expression.getSubQuery().accept( this ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index f58139ac0b..dd96542bfe 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -4,7 +4,6 @@ */ package org.hibernate.query.sqm.sql; -import jakarta.annotation.Nullable; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.PreparedStatement; @@ -184,6 +183,7 @@ import org.hibernate.query.sqm.tree.domain.SqmEmbeddedValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmEntityValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmFkExpression; import org.hibernate.query.sqm.tree.domain.SqmFunctionPath; +import org.hibernate.query.sqm.tree.domain.SqmFunctionRoot; import org.hibernate.query.sqm.tree.domain.SqmIndexAggregateFunction; import org.hibernate.query.sqm.tree.domain.SqmIndexedCollectionAccessPath; import org.hibernate.query.sqm.tree.domain.SqmMapEntryReference; @@ -231,6 +231,7 @@ import org.hibernate.query.sqm.tree.expression.SqmOverflow; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameterizedEntityType; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.expression.SqmStar; import org.hibernate.query.sqm.tree.expression.SqmSummarization; import org.hibernate.query.sqm.tree.expression.SqmToDuration; @@ -244,6 +245,7 @@ import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; +import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmConflictClause; @@ -354,6 +356,7 @@ import org.hibernate.sql.ast.tree.from.CorrelatedPluralTableGroup; import org.hibernate.sql.ast.tree.from.CorrelatedTableGroup; import org.hibernate.sql.ast.tree.from.EmbeddableFunctionTableGroup; import org.hibernate.sql.ast.tree.from.FromClause; +import org.hibernate.sql.ast.tree.from.FunctionTableGroup; import org.hibernate.sql.ast.tree.from.NamedTableReference; import org.hibernate.sql.ast.tree.from.PluralTableGroup; import org.hibernate.sql.ast.tree.from.QueryPartTableGroup; @@ -430,6 +433,7 @@ import org.jboss.logging.Logger; import jakarta.persistence.TemporalType; import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.Type; +import org.checkerframework.checker.nullness.qual.Nullable; import static jakarta.persistence.metamodel.Type.PersistenceType.ENTITY; import static java.util.Collections.singletonList; @@ -722,6 +726,10 @@ public abstract class BaseSqmToSqlAstConverter extends Base throw new UnsupportedOperationException(); } + @Override + public @Nullable TableGroup findTableGroupByIdentificationVariable(String identificationVariable) { + return getFromClauseAccess().findTableGroupByIdentificationVariable( identificationVariable ); + } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SqlAstCreationState @@ -2783,7 +2791,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base sqlSelections, lastPoppedFromClauseIndex ); - final List columnNames = tupleType.determineColumnNames(); + final List columnNames = tableGroupProducer.getColumnNames(); final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( derivedRoot.getExplicitAlias() == null ? "derived" : derivedRoot.getExplicitAlias() ); @@ -2800,6 +2808,16 @@ public abstract class BaseSqmToSqlAstConverter extends Base creationContext.getSessionFactory() ); } + else if ( sqmRoot instanceof SqmFunctionRoot functionRoot ) { + tableGroup = createFunctionTableGroup( + functionRoot.getFunction(), + functionRoot.getNavigablePath(), + functionRoot.getExplicitAlias(), + false, + true, + functionRoot.getReusablePath( CollectionPart.Nature.INDEX.getName() ) != null + ); + } else if ( sqmRoot instanceof SqmCteRoot ) { final SqmCteRoot cteRoot = (SqmCteRoot) sqmRoot; tableGroup = createCteTableGroup( @@ -2847,6 +2865,40 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } + private TableGroup createFunctionTableGroup( + SqmSetReturningFunction function, + NavigablePath navigablePath, + String explicitAlias, + boolean lateral, + boolean canUseInnerJoins, + boolean withOrdinality) { + if ( !lateral ) { + // Temporarily push an empty FromClauseIndex to disallow access to aliases from the top query + // Only lateral subqueries are allowed to see the aliases + fromClauseIndexStack.push( new FromClauseIndex( null ) ); + } + final boolean oldInNestedContext = inNestedContext; + inNestedContext = true; + final Supplier> oldFunctionImpliedResultTypeAccess = functionImpliedResultTypeAccess; + functionImpliedResultTypeAccess = inferrableTypeAccessStack.getCurrent(); + inferrableTypeAccessStack.push( () -> null ); + try { + final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( + explicitAlias == null ? "derived" : explicitAlias + ); + final String identifierVariable = sqlAliasBase.generateNewAlias(); + return function.convertToSqlAst( navigablePath, identifierVariable, lateral, canUseInnerJoins, withOrdinality, this ); + } + finally { + inferrableTypeAccessStack.pop(); + functionImpliedResultTypeAccess = oldFunctionImpliedResultTypeAccess; + inNestedContext = oldInNestedContext; + if ( !lateral ) { + fromClauseIndexStack.pop(); + } + } + } + private TableGroup createCteTableGroup( String cteName, NavigablePath navigablePath, @@ -3298,6 +3350,9 @@ public abstract class BaseSqmToSqlAstConverter extends Base else if ( sqmJoin instanceof SqmDerivedJoin ) { return consumeDerivedJoin( ( (SqmDerivedJoin) sqmJoin ), lhsTableGroup, transitive ); } + else if ( sqmJoin instanceof SqmFunctionJoin functionJoin ) { + return consumeFunctionJoin( functionJoin, lhsTableGroup, transitive ); + } else if ( sqmJoin instanceof SqmCteJoin ) { return consumeCteJoin( ( (SqmCteJoin) sqmJoin ), lhsTableGroup, transitive ); } @@ -3574,7 +3629,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base sqlSelections, lastPoppedFromClauseIndex ); - final List columnNames = tupleType.determineColumnNames(); + final List columnNames = tableGroupProducer.getColumnNames(); final SqlAliasBase sqlAliasBase = getSqlAliasBaseGenerator().createSqlAliasBase( sqmJoin.getExplicitAlias() == null ? "derived" : sqmJoin.getExplicitAlias() ); @@ -3614,6 +3669,40 @@ public abstract class BaseSqmToSqlAstConverter extends Base return queryPartTableGroup; } + private TableGroup consumeFunctionJoin(SqmFunctionJoin sqmJoin, TableGroup parentTableGroup, boolean transitive) { + final SqlAstJoinType correspondingSqlJoinType = sqmJoin.getSqmJoinType().getCorrespondingSqlJoinType(); + final TableGroup tableGroup = createFunctionTableGroup( + sqmJoin.getFunction(), + sqmJoin.getNavigablePath(), + sqmJoin.getExplicitAlias(), + sqmJoin.isLateral(), + correspondingSqlJoinType == SqlAstJoinType.INNER || correspondingSqlJoinType == SqlAstJoinType.CROSS, + sqmJoin.getReusablePath( CollectionPart.Nature.INDEX.getName() ) != null + ); + getFromClauseIndex().register( sqmJoin, tableGroup ); + + final TableGroupJoin tableGroupJoin = new TableGroupJoin( + tableGroup.getNavigablePath(), + correspondingSqlJoinType, + tableGroup, + null + ); + parentTableGroup.addTableGroupJoin( tableGroupJoin ); + + // add any additional join restrictions + if ( sqmJoin.getJoinPredicate() != null ) { + final SqmJoin oldJoin = currentlyProcessingJoin; + currentlyProcessingJoin = sqmJoin; + tableGroupJoin.applyPredicate( visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() ) ); + currentlyProcessingJoin = oldJoin; + } + + if ( transitive ) { + consumeExplicitJoins( sqmJoin, tableGroup ); + } + return tableGroup; + } + private TableGroup consumeCteJoin(SqmCteJoin sqmJoin, TableGroup parentTableGroup, boolean transitive) { final SqlAstJoinType correspondingSqlJoinType = sqmJoin.getSqmJoinType().getCorrespondingSqlJoinType(); final TableGroup tableGroup = createCteTableGroup( @@ -4039,6 +4128,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base throw new InterpretationException( "SqmDerivedRoot not yet resolved to TableGroup" ); } + @Override + public Object visitRootFunction(SqmFunctionRoot sqmRoot) { + final TableGroup resolved = getFromClauseAccess().findTableGroup( sqmRoot.getNavigablePath() ); + if ( resolved != null ) { + log.tracef( "SqmFunctionRoot [%s] resolved to existing TableGroup [%s]", sqmRoot, resolved ); + return visitTableGroup( resolved, sqmRoot ); + } + + throw new InterpretationException( "SqmFunctionRoot not yet resolved to TableGroup" ); + } + @Override public Object visitRootCte(SqmCteRoot sqmRoot) { final TableGroup resolved = getFromClauseAccess().findTableGroup( sqmRoot.getNavigablePath() ); @@ -4072,6 +4172,17 @@ public abstract class BaseSqmToSqlAstConverter extends Base throw new InterpretationException( "SqmDerivedJoin not yet resolved to TableGroup" ); } + @Override + public Object visitQualifiedFunctionJoin(SqmFunctionJoin sqmJoin) { + final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); + if ( existing != null ) { + log.tracef( "SqmFunctionJoin [%s] resolved to existing TableGroup [%s]", sqmJoin, existing ); + return visitTableGroup( existing, sqmJoin ); + } + + throw new InterpretationException( "SqmFunctionJoin not yet resolved to TableGroup" ); + } + @Override public Object visitQualifiedCteJoin(SqmCteJoin sqmJoin) { final TableGroup existing = getFromClauseAccess().findTableGroup( sqmJoin.getNavigablePath() ); @@ -4129,7 +4240,14 @@ public abstract class BaseSqmToSqlAstConverter extends Base actualModelPart = tableGroupModelPart; navigablePath = tableGroup.getNavigablePath(); } + return createExpression( tableGroup, navigablePath, actualModelPart, path ); + } + private Expression createExpression( + TableGroup tableGroup, + NavigablePath navigablePath, + ModelPart actualModelPart, + SqmPath path) { final Expression result; if ( actualModelPart instanceof EntityValuedModelPart ) { final EntityValuedModelPart entityValuedModelPart = (EntityValuedModelPart) actualModelPart; @@ -4334,12 +4452,21 @@ public abstract class BaseSqmToSqlAstConverter extends Base tableGroup ); } - else if ( actualModelPart instanceof AnonymousTupleTableGroupProducer ) { - throw new SemanticException( - "The derived SqmFrom" + ( (AnonymousTupleType) path.getReferencedPathSource() ).getComponentNames() + " can not be used in a context where the expression needs to " + - "be expanded to identifying parts, because a derived model part does not have identifying parts. " + - "Replace uses of the root with paths instead e.g. `derivedRoot.get(\"alias1\")` or `derivedRoot.alias1`" + else if ( actualModelPart instanceof AnonymousTupleTableGroupProducer tableGroupProducer ) { + final ModelPart subPart = tableGroupProducer.findSubPart( + CollectionPart.Nature.ELEMENT.getName(), + null ); + if ( subPart != null ) { + return createExpression( tableGroup, navigablePath, subPart, path ); + } + else { + throw new SemanticException( + "The derived SqmFrom" + ( (AnonymousTupleType) path.getReferencedPathSource() ).getComponentNames() + " can not be used in a context where the expression needs to " + + "be expanded to identifying parts, because a derived model part does not have identifying parts. " + + "Replace uses of the root with paths instead e.g. `derivedRoot.get(\"alias1\")` or `derivedRoot.alias1`" + ); + } } else { throw new SemanticException( @@ -6464,6 +6591,11 @@ public abstract class BaseSqmToSqlAstConverter extends Base } } + @Override + public FunctionTableGroup visitSetReturningFunction(SqmSetReturningFunction sqmFunction) { + throw new UnsupportedOperationException("Should be handled by #consumeFromClauseRoot"); + } + @Override public void registerQueryTransformer(QueryTransformer transformer) { queryTransformers.getCurrent().add( transformer ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java index 60e43cc21e..1cd92d4bf6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java @@ -23,7 +23,7 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.QueryTransformer; import org.hibernate.sql.ast.tree.predicate.Predicate; -import jakarta.annotation.Nullable; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Specialized SemanticQueryWalker (SQM visitor) for producing SQL AST. diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java index 50367af595..44947f9454 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java @@ -7,6 +7,7 @@ package org.hibernate.query.sqm.tree.cte; import java.util.ArrayList; import java.util.List; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.query.criteria.JpaCteCriteriaAttribute; import org.hibernate.query.criteria.JpaCteCriteriaType; @@ -67,7 +68,15 @@ public class SqmCteTable extends AnonymousTupleType implements JpaCteCrite String aliasStem, List sqlSelections, FromClauseAccess fromClauseAccess) { - return new CteTupleTableGroupProducer( this, aliasStem, sqlSelections, fromClauseAccess ); + return new CteTupleTableGroupProducer( this, aliasStem, toSqlTypedMappings( sqlSelections ), fromClauseAccess ); + } + + @Override + public CteTupleTableGroupProducer resolveTableGroupProducer( + String aliasStem, + SqlTypedMapping[] sqlTypedMappings, + FromClauseAccess fromClauseAccess) { + return new CteTupleTableGroupProducer( this, aliasStem, sqlTypedMappings, fromClauseAccess ); } public String getCteName() { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 58f193d29b..d7d20501e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -5,6 +5,7 @@ package org.hibernate.query.sqm.tree.domain; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -26,8 +27,10 @@ import org.hibernate.query.SemanticException; import org.hibernate.query.criteria.JpaCrossJoin; import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaDerivedJoin; +import org.hibernate.query.criteria.JpaFunctionJoin; import org.hibernate.query.criteria.JpaPath; import org.hibernate.query.criteria.JpaSelection; +import org.hibernate.query.criteria.JpaSetReturningFunction; import org.hibernate.query.hql.spi.SqmCreationState; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SqmPathSource; @@ -35,12 +38,14 @@ import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; import org.hibernate.query.sqm.tree.from.SqmCteJoin; import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmFunctionJoin; import org.hibernate.query.sqm.tree.from.SqmJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.select.SqmSubQuery; @@ -621,6 +626,78 @@ public abstract class AbstractSqmFrom extends AbstractSqmPath implements return join; } + @Override + public JpaFunctionJoin joinLateral(JpaSetReturningFunction function, SqmJoinType joinType) { + return join( function, joinType, true ); + } + + @Override + public JpaFunctionJoin joinLateral(JpaSetReturningFunction function) { + return join( function, SqmJoinType.INNER, true ); + } + + @Override + public JpaFunctionJoin join(JpaSetReturningFunction function, SqmJoinType joinType) { + return join( function, joinType, false ); + } + + @Override + public JpaFunctionJoin join(JpaSetReturningFunction function) { + return join( function, SqmJoinType.INNER, false ); + } + + @Override + public JpaFunctionJoin join(JpaSetReturningFunction function, SqmJoinType joinType, boolean lateral) { + validateComplianceFromFunction(); + //noinspection unchecked + final SqmFunctionJoin join = new SqmFunctionJoin<>( (SqmSetReturningFunction) function, alias, joinType, lateral, (SqmRoot) findRoot() ); + //noinspection unchecked + addSqmJoin( (SqmJoin) join ); + return join; + } + + @Override + public JpaFunctionJoin joinArray(String arrayAttributeName) { + return joinArray( arrayAttributeName, SqmJoinType.INNER ); + } + + @Override + public JpaFunctionJoin joinArray(String arrayAttributeName, SqmJoinType joinType) { + return join( nodeBuilder().unnestArray( get( arrayAttributeName ) ), joinType, true ); + } + + @Override + public JpaFunctionJoin joinArray(SingularAttribute arrayAttribute) { + return joinArray( arrayAttribute, SqmJoinType.INNER ); + } + + @Override + public JpaFunctionJoin joinArray(SingularAttribute arrayAttribute, SqmJoinType joinType) { + return join( nodeBuilder().unnestArray( get( arrayAttribute ) ), joinType, true ); + } + + @Override + public JpaFunctionJoin joinArrayCollection(String collectionAttributeName) { + return joinArrayCollection( collectionAttributeName, SqmJoinType.INNER ); + } + + @Override + public JpaFunctionJoin joinArrayCollection(String collectionAttributeName, SqmJoinType joinType) { + return join( nodeBuilder().unnestCollection( get( collectionAttributeName ) ), joinType, true ); + } + + @Override + public JpaFunctionJoin joinArrayCollection(SingularAttribute> collectionAttribute) { + return joinArrayCollection( collectionAttribute, SqmJoinType.INNER ); + } + + @Override + public JpaFunctionJoin joinArrayCollection( + SingularAttribute> collectionAttribute, + SqmJoinType joinType) { + return join( nodeBuilder().unnestCollection( get( collectionAttribute ) ), joinType, true ); + } + private void validateComplianceFromSubQuery() { if ( nodeBuilder().isJpaQueryComplianceEnabled() ) { throw new IllegalStateException( @@ -629,6 +706,14 @@ public abstract class AbstractSqmFrom extends AbstractSqmPath implements } } + private void validateComplianceFromFunction() { + if ( nodeBuilder().isJpaQueryComplianceEnabled() ) { + throw new IllegalStateException( + "The JPA specification does not support functions in the from clause. " + + "Please disable the JPA query compliance if you want to use this feature." ); + } + } + @Override public JpaCrossJoin crossJoin(Class entityJavaType) { return crossJoin( nodeBuilder().getDomainModel().entity( entityJavaType ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java index e26fdf01ff..a8dff6b3f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java @@ -191,9 +191,9 @@ public abstract class AbstractSqmPath extends AbstractSqmExpression implem } @Override - @SuppressWarnings("unchecked") - public SqmPath get(String attributeName) { - final SqmPathSource subNavigable = + public SqmPath get(String attributeName) { + //noinspection unchecked + final SqmPathSource subNavigable = (SqmPathSource) getResolvedModel().getSubPathSource( attributeName, nodeBuilder().getJpaMetamodel() ); return resolvePath( attributeName, subNavigable ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionRoot.java new file mode 100644 index 0000000000..94b0933854 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmFunctionRoot.java @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.domain; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.PathException; +import org.hibernate.query.criteria.JpaFunctionRoot; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.spi.NavigablePath; + + +/** + * @author Christian Beikov + */ +@Incubating +public class SqmFunctionRoot extends SqmRoot implements JpaFunctionRoot { + + private final SqmSetReturningFunction function; + + public SqmFunctionRoot(SqmSetReturningFunction function, String alias) { + this( + SqmCreationHelper.buildRootNavigablePath( "<>", alias ), + function, + function.getType(), + alias + ); + } + + protected SqmFunctionRoot( + NavigablePath navigablePath, + SqmSetReturningFunction function, + SqmPathSource pathSource, + String alias) { + super( + navigablePath, + pathSource, + alias, + true, + function.nodeBuilder() + ); + this.function = function; + } + + @Override + public SqmFunctionRoot copy(SqmCopyContext context) { + final SqmFunctionRoot existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmFunctionRoot path = context.registerCopy( + this, + new SqmFunctionRoot<>( + getNavigablePath(), + getFunction().copy( context ), + getReferencedPathSource(), + getExplicitAlias() + ) + ); + copyTo( path, context ); + return path; + } + + @Override + public SqmSetReturningFunction getFunction() { + return function; + } + + @Override + public SqmPath index() { + return get( CollectionPart.Nature.INDEX.getName() ); + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitRootFunction( this ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public EntityDomainType getModel() { + // Or should we throw an exception instead? + return null; + } + + @Override + public String getEntityName() { + return null; + } + + @Override + public SqmPathSource getResolvedModel() { + return getReferencedPathSource(); + } + + @Override + public SqmCorrelatedRoot createCorrelation() { + throw new UnsupportedOperationException(); + } + + @Override + public SqmTreatedFrom treatAs(Class treatJavaType) throws PathException { + throw new UnsupportedOperationException( "Function roots can not be treated" ); + } + + @Override + public SqmTreatedFrom treatAs(EntityDomainType treatTarget) throws PathException { + throw new UnsupportedOperationException( "Function roots can not be treated" ); + } + + @Override + public SqmTreatedRoot treatAs(Class treatJavaType, String alias) { + throw new UnsupportedOperationException( "Function roots can not be treated" ); + } + + @Override + public SqmTreatedRoot treatAs(EntityDomainType treatTarget, String alias) { + throw new UnsupportedOperationException( "Function roots can not be treated" ); + } + + @Override + public SqmTreatedRoot treatAs(Class treatJavaType, String alias, boolean fetch) { + throw new UnsupportedOperationException( "Function roots can not be treated" ); + } + + @Override + public SqmTreatedRoot treatAs(EntityDomainType treatTarget, String alias, boolean fetch) { + throw new UnsupportedOperationException( "Function roots can not be treated" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmSetReturningFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmSetReturningFunction.java new file mode 100644 index 0000000000..d71b01b632 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmSetReturningFunction.java @@ -0,0 +1,101 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.List; + +import org.hibernate.Incubating; +import org.hibernate.query.criteria.JpaSetReturningFunction; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.function.SqmSetReturningFunctionDescriptor; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.AbstractSqmNode; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.SqmVisitableNode; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.tree.from.TableGroup; + +/** + * A SQM set-returning function + * + * @since 7.0 + */ +@Incubating +public abstract class SqmSetReturningFunction extends AbstractSqmNode implements SqmVisitableNode, + JpaSetReturningFunction { + // this function-name is the one used to resolve the descriptor from + // the function registry (which may or may not be a db function name) + private final String functionName; + private final SqmSetReturningFunctionDescriptor functionDescriptor; + + private final AnonymousTupleType type; + private final List> arguments; + + public SqmSetReturningFunction( + String functionName, + SqmSetReturningFunctionDescriptor functionDescriptor, + AnonymousTupleType type, + List> arguments, + NodeBuilder criteriaBuilder) { + super( criteriaBuilder ); + this.functionName = functionName; + this.functionDescriptor = functionDescriptor; + this.type = type; + this.arguments = arguments; + } + + @Override + public abstract SqmSetReturningFunction copy(SqmCopyContext context); + + public SqmSetReturningFunctionDescriptor getFunctionDescriptor() { + return functionDescriptor; + } + + @Override + public String getFunctionName() { + return functionName; + } + + public AnonymousTupleType getType() { + return type; + } + + public List> getArguments() { + return arguments; + } + + public abstract TableGroup convertToSqlAst( + NavigablePath navigablePath, + String identifierVariable, + boolean lateral, + boolean canUseInnerJoins, + boolean withOrdinality, + SqmToSqlAstConverter walker); + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitSetReturningFunction( this ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( functionName ); + if ( arguments.isEmpty() ) { + sb.append( "()" ); + return; + } + sb.append( '(' ); + arguments.get( 0 ).appendHqlString( sb ); + for ( int i = 1; i < arguments.size(); i++ ) { + sb.append( ", " ); + arguments.get( i ).appendHqlString( sb ); + } + + sb.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFunctionJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFunctionJoin.java new file mode 100644 index 0000000000..ef2a43585b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFunctionJoin.java @@ -0,0 +1,227 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.from; + +import org.hibernate.Incubating; +import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.metamodel.model.domain.PersistentAttribute; +import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaFunctionJoin; +import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.spi.SqmCreationHelper; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.domain.AbstractSqmJoin; +import org.hibernate.query.sqm.tree.domain.SqmCorrelation; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmTreatedJoin; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; +import org.hibernate.spi.NavigablePath; + +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Predicate; + +/** + * @author Christian Beikov + */ +@Incubating +public class SqmFunctionJoin extends AbstractSqmJoin implements JpaFunctionJoin { + private final SqmSetReturningFunction function; + private final boolean lateral; + + public SqmFunctionJoin( + SqmSetReturningFunction function, + String alias, + SqmJoinType joinType, + boolean lateral, + SqmRoot sqmRoot) { + this( + SqmCreationHelper.buildRootNavigablePath( "<>", alias ), + function, + lateral, + function.getType(), + alias, + validateJoinType( joinType, lateral ), + sqmRoot + ); + } + + public SqmFunctionJoin( + NavigablePath navigablePath, + SqmSetReturningFunction function, + boolean lateral, + SqmPathSource pathSource, + String alias, + SqmJoinType joinType, + SqmRoot sqmRoot) { + super( + navigablePath, + pathSource, + sqmRoot, + alias, + joinType, + sqmRoot.nodeBuilder() + ); + this.function = function; + this.lateral = lateral; + } + + private static SqmJoinType validateJoinType(SqmJoinType joinType, boolean lateral) { + if ( lateral ) { + switch ( joinType ) { + case LEFT: + case INNER: + break; + default: + throw new IllegalArgumentException( "Lateral joins can only be left or inner. Illegal join type: " + joinType ); + } + } + return joinType; + } + + @Override + public boolean isImplicitlySelectable() { + return false; + } + + @Override + public SqmFunctionJoin copy(SqmCopyContext context) { + final SqmFunctionJoin existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + //noinspection unchecked + final SqmFunctionJoin path = context.registerCopy( + this, + new SqmFunctionJoin<>( + getNavigablePath(), + function, + lateral, + getReferencedPathSource(), + getExplicitAlias(), + getSqmJoinType(), + (SqmRoot) findRoot().copy( context ) + ) + ); + copyTo( path, context ); + return path; + } + + public SqmRoot getRoot() { + return (SqmRoot) (SqmFrom) super.getLhs(); + } + + @Override + public SqmRoot findRoot() { + return getRoot(); + } + + @Override + public SqmSetReturningFunction getFunction() { + return function; + } + + @Override + public SqmPath index() { + return get( CollectionPart.Nature.INDEX.getName() ); + } + + @Override + public boolean isLateral() { + return lateral; + } + + @Override + public SqmFrom getLhs() { + // A derived-join has no LHS + return null; + } + + @Override + public SqmFunctionJoin on(JpaExpression restriction) { + return (SqmFunctionJoin) super.on( restriction ); + } + + @Override + public SqmFunctionJoin on(Expression restriction) { + return (SqmFunctionJoin) super.on( restriction ); + } + + @Override + public SqmFunctionJoin on(JpaPredicate... restrictions) { + return (SqmFunctionJoin) super.on( restrictions ); + } + + @Override + public SqmFunctionJoin on(Predicate... restrictions) { + return (SqmFunctionJoin) super.on( restrictions ); + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitQualifiedFunctionJoin( this ); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public SqmCorrelation createCorrelation() { + throw new UnsupportedOperationException(); + } + + @Override + public SqmTreatedJoin treatAs(Class treatTarget) { + throw new UnsupportedOperationException( "Function joins can not be treated" ); + } + + @Override + public SqmTreatedJoin treatAs(EntityDomainType treatTarget) { + throw new UnsupportedOperationException( "Function joins can not be treated" ); + } + + @Override + public SqmTreatedJoin treatAs(Class treatJavaType, String alias) { + throw new UnsupportedOperationException( "Function joins can not be treated" ); + } + + @Override + public SqmTreatedJoin treatAs(EntityDomainType treatTarget, String alias) { + throw new UnsupportedOperationException( "Function joins can not be treated" ); + } + + @Override + public SqmTreatedJoin treatAs(Class treatJavaType, String alias, boolean fetched) { + throw new UnsupportedOperationException( "Function joins can not be treated" ); + } + + @Override + public SqmTreatedJoin treatAs( + EntityDomainType treatTarget, + String alias, + boolean fetched) { + throw new UnsupportedOperationException( "Function joins can not be treated" ); + } + + @Override + public PersistentAttribute getAttribute() { + // none + return null; + } + + @Override + public SqmFrom getParent() { + return super.getLhs(); + } + + @Override + public JoinType getJoinType() { + return getSqmJoinType().getCorrespondingJpaJoinType(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java index 152f67c2f6..de273a430d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java @@ -25,6 +25,7 @@ import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmPositionalParameter; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.predicate.SqmBetweenPredicate; import org.hibernate.query.sqm.tree.predicate.SqmComparisonPredicate; import org.hibernate.query.sqm.tree.predicate.SqmEmptinessPredicate; @@ -102,6 +103,15 @@ public class ParameterCollector extends BaseSemanticQueryWalker { return sqmFunction; } + @Override + public Object visitSetReturningFunction(SqmSetReturningFunction sqmFunction) { + final SqmExpressibleAccessor current = inferenceBasis; + inferenceBasis = null; + super.visitSetReturningFunction( sqmFunction ); + inferenceBasis = current; + return sqmFunction; + } + private BindableType getInferredParameterType(JpaCriteriaParameter expression) { BindableType parameterType = null; if ( inferenceBasis != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java index 53ebc748bd..866732fc62 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/AbstractSqmSelectQuery.java @@ -14,8 +14,10 @@ import java.util.function.Function; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaFunctionRoot; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaSelection; +import org.hibernate.query.criteria.JpaSetReturningFunction; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.tree.AbstractSqmNode; @@ -23,6 +25,8 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.domain.SqmCteRoot; import org.hibernate.query.sqm.tree.domain.SqmDerivedRoot; +import org.hibernate.query.sqm.tree.domain.SqmFunctionRoot; +import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; @@ -255,6 +259,13 @@ public abstract class AbstractSqmSelectQuery return root; } + @Override + public JpaFunctionRoot from(JpaSetReturningFunction function) { + final SqmFunctionRoot root = new SqmFunctionRoot<>( (SqmSetReturningFunction) function, null ); + addRoot( root ); + return root; + } + private SqmRoot addRoot(SqmRoot root) { getQuerySpec().addRoot( root ); return root; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java index 76a4812a5c..72997846cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java @@ -4,10 +4,13 @@ */ package org.hibernate.sql.ast; +import java.util.List; import java.util.Set; +import org.hibernate.Incubating; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Expression; @@ -28,6 +31,14 @@ public interface SqlAstTranslator extends SqlAstWalker */ X getLiteralValue(Expression expression); + /** + * Renders a named set returning function. + * + * @since 7.0 + */ + @Incubating + void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode); + /** * Renders the given SQL AST node with the given rendering mode. */ diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/ColumnQualifierCollectorSqlAstWalker.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/ColumnQualifierCollectorSqlAstWalker.java new file mode 100644 index 0000000000..5f4e6f6162 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/ColumnQualifierCollectorSqlAstWalker.java @@ -0,0 +1,30 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.internal; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.sql.ast.spi.AbstractSqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.ColumnReference; + +public class ColumnQualifierCollectorSqlAstWalker extends AbstractSqlAstWalker { + + private final Set columnQualifiers = new HashSet<>(); + + public static Set determineColumnQualifiers(SqlAstNode node) { + final ColumnQualifierCollectorSqlAstWalker walker = new ColumnQualifierCollectorSqlAstWalker(); + node.accept( walker ); + return walker.columnQualifiers; + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + if ( columnReference.getQualifier() != null ) { + columnQualifiers.add( columnReference.getQualifier() ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index c8cb37528b..66bc1fdef7 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -17,7 +17,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.TimeZone; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -32,10 +31,8 @@ import org.hibernate.dialect.RowLockStrategy; import org.hibernate.dialect.SelectItemReferenceStrategy; import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.AbstractDelegatingWrapperOptions; +import org.hibernate.engine.spi.LazySessionWrapperOptions; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.FilterJdbcParameter; import org.hibernate.internal.util.MathHelper; import org.hibernate.internal.util.QuotingHelper; @@ -45,6 +42,7 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.internal.util.collections.StandardStack; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityAssociationMapping; @@ -61,6 +59,7 @@ import org.hibernate.persister.internal.SqlFragmentPredicate; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.ReturnableType; import org.hibernate.query.SortDirection; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; import org.hibernate.query.internal.NullPrecedenceHelper; import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.QueryOptions; @@ -402,61 +401,6 @@ public abstract class AbstractSqlAstTranslator implemen return booleanType; } - /** - * A lazy session implementation that is needed for rendering literals. - * Usually, only the {@link WrapperOptions} interface is needed, - * but for creating LOBs, it might be to have a full-blown session. - */ - private static class LazySessionWrapperOptions extends AbstractDelegatingWrapperOptions { - - private final SessionFactoryImplementor sessionFactory; - private SessionImplementor session; - - public LazySessionWrapperOptions(SessionFactoryImplementor sessionFactory) { - this.sessionFactory = sessionFactory; - } - - public void cleanup() { - if ( session != null ) { - session.close(); - session = null; - } - } - - @Override - protected SessionImplementor delegate() { - if ( session == null ) { - session = sessionFactory.openTemporarySession(); - } - return session; - } - - @Override - public SharedSessionContractImplementor getSession() { - return delegate(); - } - - @Override - public SessionFactoryImplementor getSessionFactory() { - return sessionFactory; - } - - @Override - public boolean useStreamForLobBinding() { - return sessionFactory.getFastSessionServices().useStreamForLobBinding(); - } - - @Override - public int getPreferredSqlTypeCodeForBoolean() { - return sessionFactory.getFastSessionServices().getPreferredSqlTypeCodeForBoolean(); - } - - @Override - public TimeZone getJdbcTimeZone() { - return sessionFactory.getSessionFactoryOptions().getJdbcTimeZone(); - } - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // for tests, for now public String getSql() { @@ -5850,7 +5794,7 @@ public abstract class AbstractSqlAstTranslator implemen renderCasted( jdbcParameter ); } else { - appendSql( PARAM_MARKER ); + jdbcParameter.renderToSql( this, this, sessionFactory ); } } else { @@ -6219,10 +6163,15 @@ public abstract class AbstractSqlAstTranslator implemen if ( tableReference instanceof NamedTableReference ) { return renderNamedTableReference( (NamedTableReference) tableReference, lockMode ); } - final DerivedTableReference derivedTableReference = (DerivedTableReference) tableReference; - if ( derivedTableReference.isLateral() ) { + renderDerivedTableReference( (DerivedTableReference) tableReference ); + return false; + } + + protected void renderDerivedTableReference(DerivedTableReference tableReference) { + if ( tableReference.isLateral() ) { if ( dialect.supportsLateral() ) { appendSql( "lateral " ); + tableReference.accept( this ); } else if ( tableReference instanceof QueryPartTableReference queryPartTableReference ) { final SelectStatement emulationStatement = stripToSelectClause( queryPartTableReference.getStatement() ); @@ -6261,11 +6210,15 @@ public abstract class AbstractSqlAstTranslator implemen sessionFactory ); emulationTableReference.accept( this ); - return false; + } + else { + // Assume there is no need for a lateral keyword + tableReference.accept( this ); } } - tableReference.accept( this ); - return false; + else { + tableReference.accept( this ); + } } protected void inlineCteTableGroup(TableGroup tableGroup, LockMode lockMode) { @@ -6317,7 +6270,7 @@ public abstract class AbstractSqlAstTranslator implemen append( '(' ); visitValuesList( tableReference.getValuesList() ); append( ')' ); - renderDerivedTableReference( tableReference ); + renderDerivedTableReferenceIdentificationVariable( tableReference ); } @Override @@ -6330,13 +6283,40 @@ public abstract class AbstractSqlAstTranslator implemen else { tableReference.getStatement().accept( this ); } - renderDerivedTableReference( tableReference ); + renderDerivedTableReferenceIdentificationVariable( tableReference ); } @Override public void visitFunctionTableReference(FunctionTableReference tableReference) { tableReference.getFunctionExpression().accept( this ); - renderDerivedTableReference( tableReference ); + if ( !tableReference.rendersIdentifierVariable() ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + public void renderNamedSetReturningFunction(String functionName, List sqlAstArguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstNodeRenderingMode argumentRenderingMode) { + renderSimpleNamedFunction( functionName, sqlAstArguments, argumentRenderingMode ); + + if ( tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ) != null ) { + if ( dialect.getDefaultOrdinalityColumnName() == null ) { + throw new UnsupportedOperationException( "Database does not support the 'with ordinality' syntax for custom set-returning functions" ); + } + appendSql( " with ordinality" ); + } + } + + protected final void renderSimpleNamedFunction(String functionName, List sqlAstArguments, SqlAstNodeRenderingMode argumentRenderingMode) { + appendSql( functionName ); + appendSql( '(' ); + if ( !sqlAstArguments.isEmpty() ) { + render( sqlAstArguments.get( 0 ), argumentRenderingMode ); + for ( int i = 1; i < sqlAstArguments.size(); i++ ) { + appendSql( ',' ); + render( sqlAstArguments.get( i ), argumentRenderingMode ); + } + } + appendSql( ')' ); } protected void emulateQueryPartTableReferenceColumnAliasing(QueryPartTableReference tableReference) { @@ -6391,7 +6371,7 @@ public abstract class AbstractSqlAstTranslator implemen renderTableReferenceIdentificationVariable( tableReference ); } - protected void renderDerivedTableReference(DerivedTableReference tableReference) { + protected void renderDerivedTableReferenceIdentificationVariable(DerivedTableReference tableReference) { final String identificationVariable = tableReference.getIdentificationVariable(); if ( identificationVariable != null ) { append( WHITESPACE ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/FromClauseAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/FromClauseAccess.java index f3eaa51907..46719b8e27 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/FromClauseAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/FromClauseAccess.java @@ -10,6 +10,8 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlTreeCreationException; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Access to TableGroup indexing. The indexing is defined in terms * of {@link NavigablePath} @@ -76,4 +78,6 @@ public interface FromClauseAccess { } return tableGroup; } + + @Nullable TableGroup findTableGroupByIdentificationVariable(String identificationVariable); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SimpleFromClauseAccessImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SimpleFromClauseAccessImpl.java index e54a7cc6d3..875f9e44ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SimpleFromClauseAccessImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SimpleFromClauseAccessImpl.java @@ -18,6 +18,8 @@ import org.hibernate.sql.ast.tree.from.TableGroupJoin; import org.jboss.logging.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Simple implementation of FromClauseAccess * @@ -50,6 +52,16 @@ public class SimpleFromClauseAccessImpl implements FromClauseAccess { return parent.findTableGroup( navigablePath ); } + @Override + public @Nullable TableGroup findTableGroupByIdentificationVariable(String identificationVariable) { + for ( TableGroup tableGroup : tableGroupMap.values() ) { + if ( tableGroup.findTableReference( identificationVariable ) != null ) { + return tableGroup; + } + } + return parent == null ? null : parent.findTableGroupByIdentificationVariable( identificationVariable ); + } + @Override public TableGroup findTableGroupForGetOrCreate(NavigablePath navigablePath) { final TableGroup localTableGroup = tableGroupMap.get( navigablePath ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java index b4770f8486..1be3b4e7ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/ColumnReference.java @@ -20,6 +20,8 @@ import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.update.Assignable; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.internal.util.StringHelper.replace; import static org.hibernate.sql.Template.TEMPLATE; @@ -35,7 +37,7 @@ public class ColumnReference implements Expression, Assignable { private final String columnExpression; private final SelectablePath selectablePath; private final boolean isFormula; - private final String readExpression; + private final @Nullable String readExpression; private final JdbcMapping jdbcMapping; public ColumnReference(TableReference tableReference, SelectableMapping selectableMapping) { @@ -148,7 +150,7 @@ public class ColumnReference implements Expression, Assignable { return columnExpression; } - protected String getReadExpression() { + public @Nullable String getReadExpression() { return readExpression; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/LiteralAsParameter.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/LiteralAsParameter.java index 3631a63c6b..a38f9a3196 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/LiteralAsParameter.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/LiteralAsParameter.java @@ -27,7 +27,11 @@ public class LiteralAsParameter implements SelfRenderingExpression { @Override public void renderToSql(SqlAppender sqlAppender, SqlAstTranslator walker, SessionFactoryImplementor sessionFactory) { - sqlAppender.append( parameterMarker ); + literal.getJdbcMapping().getJdbcType().appendWriteExpression( + parameterMarker, + sqlAppender, + sessionFactory.getJdbcServices().getDialect() + ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableGroup.java index 6e71a26fb6..cc61a218da 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableGroup.java @@ -6,6 +6,7 @@ package org.hibernate.sql.ast.tree.from; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Consumer; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -27,8 +28,10 @@ public class FunctionTableGroup extends AbstractTableGroup { FunctionExpression functionExpression, String sourceAlias, List columnNames, + Set compatibleTableExpressions, boolean lateral, boolean canUseInnerJoins, + boolean rendersIdentifierVariable, SessionFactoryImplementor sessionFactory) { super( canUseInnerJoins, @@ -43,6 +46,8 @@ public class FunctionTableGroup extends AbstractTableGroup { sourceAlias, columnNames, lateral, + rendersIdentifierVariable, + compatibleTableExpressions, sessionFactory ); } @@ -57,7 +62,7 @@ public class FunctionTableGroup extends AbstractTableGroup { NavigablePath navigablePath, String tableExpression, boolean resolve) { - if ( tableExpression == null ) { + if ( getPrimaryTableReference().containsAffectedTableName( tableExpression ) ) { return getPrimaryTableReference(); } for ( TableGroupJoin tableGroupJoin : getNestedTableGroupJoins() ) { @@ -81,7 +86,7 @@ public class FunctionTableGroup extends AbstractTableGroup { @Override public void applyAffectedTableNames(Consumer nameCollector) { - functionTableReference.applyAffectedTableNames( nameCollector ); + getPrimaryTableReference().applyAffectedTableNames( nameCollector ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java index 89f65a5586..3fa5570dad 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/FunctionTableReference.java @@ -5,6 +5,7 @@ package org.hibernate.sql.ast.tree.from; import java.util.List; +import java.util.Set; import java.util.function.Function; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -19,21 +20,35 @@ import org.hibernate.sql.ast.tree.expression.FunctionExpression; public class FunctionTableReference extends DerivedTableReference { private final FunctionExpression functionExpression; + private final Set compatibleTableExpressions; + private final boolean rendersIdentifierVariable; public FunctionTableReference( FunctionExpression functionExpression, String identificationVariable, List columnNames, boolean lateral, + boolean rendersIdentifierVariable, + Set compatibleTableExpressions, SessionFactoryImplementor sessionFactory) { super( identificationVariable, columnNames, lateral, sessionFactory ); this.functionExpression = functionExpression; + this.compatibleTableExpressions = compatibleTableExpressions; + this.rendersIdentifierVariable = rendersIdentifierVariable; } public FunctionExpression getFunctionExpression() { return functionExpression; } + public Set getCompatibleTableExpressions() { + return compatibleTableExpressions; + } + + public boolean rendersIdentifierVariable() { + return rendersIdentifierVariable; + } + @Override public void accept(SqlAstWalker sqlTreeWalker) { sqlTreeWalker.visitFunctionTableReference( this ); @@ -43,4 +58,9 @@ public class FunctionTableReference extends DerivedTableReference { public Boolean visitAffectedTableNames(Function nameCollector) { return null; } + + @Override + public boolean containsAffectedTableName(String requestedName) { + return compatibleTableExpressions.contains( requestedName ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java index 92abc84031..eda21c70b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroup.java @@ -226,4 +226,19 @@ public interface TableGroup extends SqlAstNode, ColumnReferenceQualifier, SqmPat default boolean isVirtual() { return false; } + + default TableReference findTableReference(String identificationVariable) { + final TableReference primaryTableReference = getPrimaryTableReference(); + if ( identificationVariable.equals( primaryTableReference.getIdentificationVariable() ) ) { + return primaryTableReference; + } + for ( TableReferenceJoin tableReferenceJoin : getTableReferenceJoins() ) { + final NamedTableReference joinedTableReference = tableReferenceJoin.getJoinedTableReference(); + if ( identificationVariable.equals( joinedTableReference.getIdentificationVariable() ) ) { + return joinedTableReference; + } + } + + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java index f5714cd6b5..c359ba704b 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java @@ -44,10 +44,11 @@ public abstract class AbstractArrayJavaType extends AbstractClassJavaType< + " (attribute is not annotated '@ElementCollection', '@OneToMany', or '@ManyToMany')"); } // Always determine the recommended type to make sure this is a valid basic java type + final JdbcType recommendedComponentJdbcType = componentJavaType.getRecommendedJdbcType( indicators ); return indicators.getTypeConfiguration().getJdbcTypeRegistry().resolveTypeConstructorDescriptor( - indicators.getPreferredSqlTypeCodeForArray(), + indicators.getPreferredSqlTypeCodeForArray( recommendedComponentJdbcType.getDefaultSqlTypeCode() ), indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( - componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), + componentJavaType, recommendedComponentJdbcType ), ColumnTypeInformation.EMPTY ); } @@ -89,7 +90,7 @@ public abstract class AbstractArrayJavaType extends AbstractClassJavaType< return new ConvertedBasicArrayType<>( elementType, typeConfiguration.getJdbcTypeRegistry().resolveTypeConstructorDescriptor( - stdIndicators.getExplicitJdbcTypeCode(), + stdIndicators.getPreferredSqlTypeCodeForArray( elementType.getJdbcType().getDefaultSqlTypeCode() ), elementType, columnTypeInformation ), @@ -106,7 +107,7 @@ public abstract class AbstractArrayJavaType extends AbstractClassJavaType< ColumnTypeInformation columnTypeInformation, JdbcTypeIndicators stdIndicators) { final JdbcType arrayJdbcType = typeConfiguration.getJdbcTypeRegistry().resolveTypeConstructorDescriptor( - stdIndicators.getExplicitJdbcTypeCode(), + stdIndicators.getPreferredSqlTypeCodeForArray( elementType.getJdbcType().getDefaultSqlTypeCode() ), elementType, columnTypeInformation ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java index db6f6ed7fc..7217fe9c62 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java @@ -73,10 +73,11 @@ public class BasicCollectionJavaType, E> extends Abstrac } // Always determine the recommended type to make sure this is a valid basic java type // (even though we only use this inside the if block, we want it to throw here if something wrong) + final JdbcType recommendedComponentJdbcType = componentJavaType.getRecommendedJdbcType( indicators ); return indicators.getTypeConfiguration().getJdbcTypeRegistry().resolveTypeConstructorDescriptor( - indicators.getPreferredSqlTypeCodeForArray(), + indicators.getPreferredSqlTypeCodeForArray( recommendedComponentJdbcType.getDefaultSqlTypeCode() ), indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( - componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), + componentJavaType, recommendedComponentJdbcType ), ColumnTypeInformation.EMPTY ); } @@ -118,7 +119,7 @@ public class BasicCollectionJavaType, E> extends Abstrac typeConfiguration.getJavaTypeRegistry().addDescriptor( collectionJavaType ); } final BasicValueConverter valueConverter = elementType.getValueConverter(); - final int arrayTypeCode = stdIndicators.getPreferredSqlTypeCodeForArray(); + final int arrayTypeCode = stdIndicators.getPreferredSqlTypeCodeForArray( elementType.getJdbcType().getDefaultSqlTypeCode() ); final JdbcType arrayJdbcType = typeConfiguration.getJdbcTypeRegistry() .resolveTypeConstructorDescriptor( arrayTypeCode, elementType, columnTypeInformation ); @@ -342,6 +343,10 @@ public class BasicCollectionJavaType, E> extends Abstrac //noinspection unchecked return (X) new BinaryStreamImpl( SerializationHelper.serialize( asArrayList( value ) ) ); } + else if ( type == Object[].class ) { + //noinspection unchecked + return (X) value.toArray(); + } else if ( Object[].class.isAssignableFrom( type ) ) { final Class preferredJavaTypeClass = type.getComponentType(); final Object[] unwrapped = (Object[]) Array.newInstance( preferredJavaTypeClass, value.size() ); @@ -403,16 +408,17 @@ public class BasicCollectionJavaType, E> extends Abstrac else if ( value instanceof byte[] ) { // When the value is a byte[], this is a deserialization request //noinspection unchecked - return fromCollection( (ArrayList) SerializationHelper.deserialize( (byte[]) value ) ); + return fromCollection( (ArrayList) SerializationHelper.deserialize( (byte[]) value ), options ); } else if ( value instanceof BinaryStream ) { // When the value is a BinaryStream, this is a deserialization request //noinspection unchecked - return fromCollection( (ArrayList) SerializationHelper.deserialize( ( (BinaryStream) value ).getBytes() ) ); + return fromCollection( (ArrayList) SerializationHelper.deserialize( ( (BinaryStream) value ).getBytes() ), + options ); } else if ( value instanceof Collection ) { //noinspection unchecked - return fromCollection( (Collection) value ); + return fromCollection( (Collection) value, options ); } else if ( value.getClass().isArray() ) { final int length = Array.getLength( value ); @@ -441,23 +447,29 @@ public class BasicCollectionJavaType, E> extends Abstrac return new ArrayList<>( value ); } - private C fromCollection(Collection value) { + private C fromCollection(Collection value, WrapperOptions options) { + final C collection; switch ( semantics.getCollectionClassification() ) { case SET: // Keep consistent with CollectionMutabilityPlan::deepCopy //noinspection unchecked - return (C) new LinkedHashSet<>( value ); + collection = (C) new LinkedHashSet<>( value.size() ); + break; case LIST: case BAG: - if ( value instanceof ArrayList ) { + if ( value instanceof ArrayList arrayList ) { + arrayList.replaceAll( e -> componentJavaType.wrap( e, options ) ); //noinspection unchecked return (C) value; } default: - final C collection = semantics.instantiateRaw( value.size(), null ); - collection.addAll( value ); - return collection; + collection = semantics.instantiateRaw( value.size(), null ); + break; } + for ( E e : value ) { + collection.add( componentJavaType.wrap( e, options ) ); + } + return collection; } private static class CollectionMutabilityPlan, E> implements MutabilityPlan { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java index c568df1bde..50ce58be92 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java @@ -148,7 +148,9 @@ public class ArrayJdbcType implements JdbcType { final T[] domainObjects = (T[]) javaType.unwrap( value, Object[].class, options ); final Object[] objects = new Object[domainObjects.length]; for ( int i = 0; i < domainObjects.length; i++ ) { - objects[i] = elementBinder.getBindValue( domainObjects[i], options ); + if ( domainObjects[i] != null ) { + objects[i] = elementBinder.getBindValue( domainObjects[i], options ); + } } return objects; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java index 40b565135f..e4dfbb7989 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcTypeIndicators.java @@ -138,6 +138,26 @@ public interface JdbcTypeIndicators { return resolveJdbcTypeCode( getCurrentBaseSqlTypeIndicators().getPreferredSqlTypeCodeForArray() ); } + /** + * When mapping a basic array or collection type to the database what is the preferred SQL type code to use, + * given the element SQL type code? + *

+ * Returns a key into the {@link JdbcTypeRegistry}. + * + * @see org.hibernate.dialect.Dialect#getPreferredSqlTypeCodeForArray() + * + * @since 7.0 + */ + default int getPreferredSqlTypeCodeForArray(int elementSqlTypeCode) { + return resolveJdbcTypeCode( + switch ( elementSqlTypeCode ) { + case SqlTypes.JSON -> SqlTypes.JSON_ARRAY; + case SqlTypes.SQLXML -> SqlTypes.XML_ARRAY; + default -> getExplicitJdbcTypeCode(); + } + ); + } + /** * Useful for resolutions based on column length. *

diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java index 9397bb2320..c856686e78 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java @@ -9,10 +9,13 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import org.hibernate.dialect.JsonHelper; +import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.JavaType; /** @@ -20,13 +23,10 @@ import org.hibernate.type.descriptor.java.JavaType; * * @author Christian Beikov */ -public class JsonArrayJdbcType implements JdbcType { - /** - * Singleton access - */ - public static final JsonArrayJdbcType INSTANCE = new JsonArrayJdbcType(); +public class JsonArrayJdbcType extends ArrayJdbcType { - protected JsonArrayJdbcType() { + public JsonArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); } @Override @@ -34,6 +34,11 @@ public class JsonArrayJdbcType implements JdbcType { return SqlTypes.VARCHAR; } + @Override + public int getDdlTypeCode() { + return SqlTypes.JSON; + } + @Override public int getDefaultSqlTypeCode() { return SqlTypes.JSON_ARRAY; @@ -54,19 +59,21 @@ public class JsonArrayJdbcType implements JdbcType { if ( string == null ) { return null; } - return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().fromString( - string, - javaType, - options - ); + return JsonHelper.arrayFromString( javaType, this, string, options ); } protected String toString(X value, JavaType javaType, WrapperOptions options) { - return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().toString( - value, - javaType, - options - ); + final JdbcType elementJdbcType = getElementJdbcType(); + final Object[] domainObjects = javaType.unwrap( value, Object[].class, options ); + if ( elementJdbcType instanceof JsonJdbcType jsonElementJdbcType ) { + final EmbeddableMappingType embeddableMappingType = jsonElementJdbcType.getEmbeddableMappingType(); + return JsonHelper.arrayToString( embeddableMappingType, domainObjects, options ); + } + else { + assert !( elementJdbcType instanceof AggregateJdbcType ); + final JavaType elementJavaType = ( (BasicPluralJavaType) javaType ).getElementJavaType(); + return JsonHelper.arrayToString( elementJavaType, elementJdbcType, domainObjects, options ); + } } @Override @@ -93,17 +100,25 @@ public class JsonArrayJdbcType implements JdbcType { return new BasicExtractor<>( javaType, this ) { @Override protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { - return fromString( rs.getString( paramIndex ), getJavaType(), options ); + return getObject( rs.getString( paramIndex ), options ); } @Override protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { - return fromString( statement.getString( index ), getJavaType(), options ); + return getObject( statement.getString( index ), options ); } @Override protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { - return fromString( statement.getString( name ), getJavaType(), options ); + return getObject( statement.getString( name ), options ); + } + + private X getObject(String json, WrapperOptions options) throws SQLException { + return ( (JsonArrayJdbcType) getJdbcType() ).fromString( + json, + getJavaType(), + options + ); } }; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..7dab625e0f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcTypeConstructor.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + + +import org.hibernate.dialect.Dialect; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link JsonArrayJdbcType}. + */ +public class JsonArrayJdbcTypeConstructor implements JdbcTypeConstructor { + public static final JsonArrayJdbcTypeConstructor INSTANCE = new JsonArrayJdbcTypeConstructor(); + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new JsonArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonAsStringArrayJdbcType.java similarity index 74% rename from hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java rename to hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonAsStringArrayJdbcType.java index 17bb5794d3..9e0f4715d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonAsStringArrayJdbcType.java @@ -22,18 +22,17 @@ import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; * * @author Christian Beikov */ -public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements AdjustableJdbcType { - /** - * Singleton access - */ - public static final JsonArrayAsStringJdbcType VARCHAR_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.LONG32VARCHAR ); - public static final JsonArrayAsStringJdbcType NVARCHAR_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.LONG32NVARCHAR ); - public static final JsonArrayAsStringJdbcType CLOB_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.CLOB ); - public static final JsonArrayAsStringJdbcType NCLOB_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.NCLOB ); +public class JsonAsStringArrayJdbcType extends JsonArrayJdbcType implements AdjustableJdbcType { private final boolean nationalized; private final int ddlTypeCode; - protected JsonArrayAsStringJdbcType(int ddlTypeCode) { + + public JsonAsStringArrayJdbcType(JdbcType elementJdbcType) { + this( elementJdbcType, SqlTypes.LONG32VARCHAR ); + } + + public JsonAsStringArrayJdbcType(JdbcType elementJdbcType, int ddlTypeCode) { + super( elementJdbcType ); this.ddlTypeCode = ddlTypeCode; this.nationalized = ddlTypeCode == SqlTypes.LONG32NVARCHAR || ddlTypeCode == SqlTypes.NCLOB; @@ -60,10 +59,14 @@ public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements Adju // In some DBMS we can compare LOBs with special functions which is handled in the SqlAstTranslators, // but that requires the correct jdbc type code to be available, which we ensure this way if ( needsLob( indicators ) ) { - return indicators.isNationalized() ? NCLOB_INSTANCE : CLOB_INSTANCE; + return indicators.isNationalized() + ? new JsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.NCLOB ) + : new JsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.CLOB ); } else { - return indicators.isNationalized() ? NVARCHAR_INSTANCE : VARCHAR_INSTANCE; + return indicators.isNationalized() + ? new JsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32NVARCHAR ) + : new JsonAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32VARCHAR ); } } @@ -90,7 +93,7 @@ public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements Adju @Override protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { - final String json = ( (JsonArrayAsStringJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + final String json = ( (JsonAsStringArrayJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); if ( options.getDialect().supportsNationalizedMethods() ) { st.setNString( index, json ); } @@ -102,7 +105,7 @@ public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements Adju @Override protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { - final String json = ( (JsonArrayAsStringJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + final String json = ( (JsonAsStringArrayJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); if ( options.getDialect().supportsNationalizedMethods() ) { st.setNString( name, json ); } @@ -124,10 +127,10 @@ public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements Adju @Override protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { if ( options.getDialect().supportsNationalizedMethods() ) { - return fromString( rs.getNString( paramIndex ), getJavaType(), options ); + return getObject( rs.getNString( paramIndex ), options ); } else { - return fromString( rs.getString( paramIndex ), getJavaType(), options ); + return getObject( rs.getString( paramIndex ), options ); } } @@ -135,10 +138,10 @@ public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements Adju protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { if ( options.getDialect().supportsNationalizedMethods() ) { - return fromString( statement.getNString( index ), getJavaType(), options ); + return getObject( statement.getNString( index ), options ); } else { - return fromString( statement.getString( index ), getJavaType(), options ); + return getObject( statement.getString( index ), options ); } } @@ -146,13 +149,21 @@ public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements Adju protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { if ( options.getDialect().supportsNationalizedMethods() ) { - return fromString( statement.getNString( name ), getJavaType(), options ); + return getObject( statement.getNString( name ), options ); } else { - return fromString( statement.getString( name ), getJavaType(), options ); + return getObject( statement.getString( name ), options ); } } + private X getObject(String json, WrapperOptions options) throws SQLException { + return ( (JsonAsStringArrayJdbcType) getJdbcType() ).fromString( + json, + getJavaType(), + options + ); + } + }; } else { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonAsStringArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonAsStringArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..a164e19dca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonAsStringArrayJdbcTypeConstructor.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + + +import org.hibernate.dialect.Dialect; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link JsonArrayJdbcType}. + */ +public class JsonAsStringArrayJdbcTypeConstructor implements JdbcTypeConstructor { + public static final JsonAsStringArrayJdbcTypeConstructor INSTANCE = new JsonAsStringArrayJdbcTypeConstructor(); + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new JsonAsStringArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java index ffcf78be35..372c7f582d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java @@ -24,12 +24,9 @@ import java.sql.SQLException; * @author Christian Beikov */ public class OracleJsonArrayBlobJdbcType extends JsonArrayJdbcType { - /** - * Singleton access - */ - public static final OracleJsonArrayBlobJdbcType INSTANCE = new OracleJsonArrayBlobJdbcType(); - protected OracleJsonArrayBlobJdbcType() { + public OracleJsonArrayBlobJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java new file mode 100644 index 0000000000..dbcb316bd8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcType.java @@ -0,0 +1,129 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLXML; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * Specialized type mapping for {@code XML_ARRAY} and the XML ARRAY SQL data type. + * + * @author Christian Beikov + */ +public class XmlArrayJdbcType extends ArrayJdbcType { + + public XmlArrayJdbcType(JdbcType elementJdbcType) { + super( elementJdbcType ); + } + + @Override + public int getJdbcTypeCode() { + return SqlTypes.SQLXML; + } + + @Override + public int getDdlTypeCode() { + return SqlTypes.SQLXML; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.XML_ARRAY; + } + + @Override + public String toString() { + return "XmlArrayJdbcType"; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + // No literal support for now + return null; + } + + protected X fromString(String string, JavaType javaType, WrapperOptions options) throws SQLException { + if ( string == null ) { + return null; + } + return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().fromString( + string, + javaType, + options + ); + } + + protected String toString(X value, JavaType javaType, WrapperOptions options) { + return options.getSessionFactory().getFastSessionServices().getXmlFormatMapper().toString( + value, + javaType, + options + ); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String xml = ( (XmlArrayJdbcType ) getJdbcType() ).toString( value, getJavaType(), options ); + SQLXML sqlxml = st.getConnection().createSQLXML(); + sqlxml.setString( xml ); + st.setSQLXML( index, sqlxml ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String xml = ( (XmlArrayJdbcType ) getJdbcType() ).toString( value, getJavaType(), options ); + SQLXML sqlxml = st.getConnection().createSQLXML(); + sqlxml.setString( xml ); + st.setSQLXML( name, sqlxml ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getObject( rs.getSQLXML( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return getObject( statement.getSQLXML( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return getObject( statement.getSQLXML( name ), options ); + } + + private X getObject(SQLXML sqlxml, WrapperOptions options) throws SQLException { + if ( sqlxml == null ) { + return null; + } + return ( (XmlArrayJdbcType ) getJdbcType() ).fromString( + sqlxml.getString(), + getJavaType(), + options + ); + } + + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..01b8a564ae --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlArrayJdbcTypeConstructor.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + + +import org.hibernate.dialect.Dialect; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link XmlArrayJdbcType}. + */ +public class XmlArrayJdbcTypeConstructor implements JdbcTypeConstructor { + public static final XmlArrayJdbcTypeConstructor INSTANCE = new XmlArrayJdbcTypeConstructor(); + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new XmlArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.XML_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcType.java new file mode 100644 index 0000000000..a2f61098c4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcType.java @@ -0,0 +1,166 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; + +/** + * Specialized type mapping for {@code XML_ARRAY} and the XML ARRAY SQL data type. + * + * @author Christian Beikov + */ +public class XmlAsStringArrayJdbcType extends XmlArrayJdbcType implements AdjustableJdbcType { + + private final boolean nationalized; + private final int ddlTypeCode; + + public XmlAsStringArrayJdbcType(JdbcType elementJdbcType) { + this( elementJdbcType, SqlTypes.LONG32VARCHAR ); + } + + protected XmlAsStringArrayJdbcType(JdbcType elementJdbcType, int ddlTypeCode) { + super( elementJdbcType ); + this.ddlTypeCode = ddlTypeCode; + this.nationalized = ddlTypeCode == SqlTypes.LONG32NVARCHAR + || ddlTypeCode == SqlTypes.NCLOB; + } + + @Override + public int getJdbcTypeCode() { + return nationalized ? SqlTypes.NVARCHAR : SqlTypes.VARCHAR; + } + + @Override + public int getDdlTypeCode() { + return ddlTypeCode; + } + + @Override + public String toString() { + return "XmlArrayAsStringJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType(JdbcTypeIndicators indicators, JavaType domainJtd) { + // Depending on the size of the column, we might have to adjust the jdbc type code for DDL. + // In some DBMS we can compare LOBs with special functions which is handled in the SqlAstTranslators, + // but that requires the correct jdbc type code to be available, which we ensure this way + if ( needsLob( indicators ) ) { + return indicators.isNationalized() + ? new XmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.NCLOB ) + : new XmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.CLOB ); + } + else { + return indicators.isNationalized() + ? new XmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32NVARCHAR ) + : new XmlAsStringArrayJdbcType( getElementJdbcType(), SqlTypes.LONG32VARCHAR ); + } + } + + protected boolean needsLob(JdbcTypeIndicators indicators) { + final Dialect dialect = indicators.getDialect(); + final long length = indicators.getColumnLength(); + final long maxLength = indicators.isNationalized() + ? dialect.getMaxNVarcharLength() + : dialect.getMaxVarcharLength(); + if ( length > maxLength ) { + return true; + } + + final DdlTypeRegistry ddlTypeRegistry = indicators.getTypeConfiguration().getDdlTypeRegistry(); + final String typeName = ddlTypeRegistry.getTypeName( getDdlTypeCode(), dialect ); + return typeName.equals( ddlTypeRegistry.getTypeName( SqlTypes.CLOB, dialect ) ) + || typeName.equals( ddlTypeRegistry.getTypeName( SqlTypes.NCLOB, dialect ) ); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final XmlAsStringArrayJdbcType jdbcType = (XmlAsStringArrayJdbcType) getJdbcType(); + final String xml = jdbcType.toString( value, getJavaType(), options ); + if ( jdbcType.nationalized && options.getDialect().supportsNationalizedMethods() ) { + st.setNString( index, xml ); + } + else { + st.setString( index, xml ); + } + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final XmlAsStringArrayJdbcType jdbcType = (XmlAsStringArrayJdbcType) getJdbcType(); + final String xml = jdbcType.toString( value, getJavaType(), options ); + if ( jdbcType.nationalized && options.getDialect().supportsNationalizedMethods() ) { + st.setNString( name, xml ); + } + else { + st.setString( name, xml ); + } + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + final XmlAsStringArrayJdbcType jdbcType = (XmlAsStringArrayJdbcType) getJdbcType(); + final String value; + if ( jdbcType.nationalized && options.getDialect().supportsNationalizedMethods() ) { + value = rs.getNString( paramIndex ); + } + else { + value = rs.getString( paramIndex ); + } + return jdbcType.fromString( value, getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + final XmlAsStringArrayJdbcType jdbcType = (XmlAsStringArrayJdbcType) getJdbcType(); + final String value; + if ( jdbcType.nationalized && options.getDialect().supportsNationalizedMethods() ) { + value = statement.getNString( index ); + } + else { + value = statement.getString( index ); + } + return jdbcType.fromString( value, getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + final XmlAsStringArrayJdbcType jdbcType = (XmlAsStringArrayJdbcType) getJdbcType(); + final String value; + if ( jdbcType.nationalized && options.getDialect().supportsNationalizedMethods() ) { + value = statement.getNString( name ); + } + else { + value = statement.getString( name ); + } + return jdbcType.fromString( value, getJavaType(), options ); + } + + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcTypeConstructor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcTypeConstructor.java new file mode 100644 index 0000000000..8085f0528d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/XmlAsStringArrayJdbcTypeConstructor.java @@ -0,0 +1,40 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.type.descriptor.jdbc; + + +import org.hibernate.dialect.Dialect; +import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Factory for {@link XmlAsStringArrayJdbcType}. + */ +public class XmlAsStringArrayJdbcTypeConstructor implements JdbcTypeConstructor { + public static final XmlAsStringArrayJdbcTypeConstructor INSTANCE = new XmlAsStringArrayJdbcTypeConstructor(); + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + BasicType elementType, + ColumnTypeInformation columnTypeInformation) { + return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation ); + } + + public JdbcType resolveType( + TypeConfiguration typeConfiguration, + Dialect dialect, + JdbcType elementType, + ColumnTypeInformation columnTypeInformation) { + return new XmlAsStringArrayJdbcType( elementType ); + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.XML_ARRAY; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java index 015fb44e2b..0f370d5d09 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java @@ -284,6 +284,14 @@ public class JdbcTypeRegistry implements JdbcTypeBaseline.BaselineTarget, Serial addTypeConstructor( jdbcTypeConstructor.getDefaultSqlTypeCode(), jdbcTypeConstructor ); } + public void addTypeConstructorIfAbsent(int jdbcTypeCode, JdbcTypeConstructor jdbcTypeConstructor) { + descriptorConstructorMap.putIfAbsent( jdbcTypeCode, jdbcTypeConstructor ); + } + + public void addTypeConstructorIfAbsent(JdbcTypeConstructor jdbcTypeConstructor) { + addTypeConstructorIfAbsent( jdbcTypeConstructor.getDefaultSqlTypeCode(), jdbcTypeConstructor ); + } + private static final class TypeConstructedJdbcTypeKey { private final int typeConstructorTypeCode; private final Object jdbcTypeOrBasicType; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentArrayTest.java index 5e579879c8..bded64d502 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentArrayTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentArrayTest.java @@ -31,7 +31,7 @@ import jakarta.persistence.Id; ) @SessionFactory @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructAggregate.class) -@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTypedArrays.class) public class StructComponentArrayTest { @BeforeEach diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentAssociationErrorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentAssociationErrorTest.java index 7fd5040737..60a5aaae96 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentAssociationErrorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentAssociationErrorTest.java @@ -33,7 +33,7 @@ import static org.hamcrest.MatcherAssert.assertThat; @JiraKey( "HHH-15831" ) @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructAggregate.class) -@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTypedArrays.class) public class StructComponentAssociationErrorTest { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructNestedComponentAssociationErrorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructNestedComponentAssociationErrorTest.java index 00e6169240..4eeb641fcf 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructNestedComponentAssociationErrorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructNestedComponentAssociationErrorTest.java @@ -33,7 +33,7 @@ import static org.hamcrest.MatcherAssert.assertThat; @JiraKey( "HHH-15831" ) @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructAggregate.class) -@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsTypedArrays.class) public class StructNestedComponentAssociationErrorTest { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java index 9ec42b82e2..f7a973956e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java @@ -56,6 +56,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(standardModels = StandardDomainModel.GAMBIT) @SessionFactory @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayAgg.class) @SkipForDialect(dialectClass = SpannerDialect.class, reason = "Doesn't support array_agg ordering yet") public class ArrayAggregateTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAppendTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAppendTest.java index e920ee7799..345cfaf11b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAppendTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAppendTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayAppend.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayAppendTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConcatTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConcatTest.java index 8e407d3112..60cb80c190 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConcatTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConcatTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayConcat.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayConcatTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java index d717d0022a..67ec4a6c8f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java @@ -33,6 +33,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; }) @SessionFactory @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayConstructor.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayConstructorInSelectClauseTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorTest.java index 109498bf14..865acf39b5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorTest.java @@ -32,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayConstructor.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayConstructorTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java index 22a03141e3..cf5c9c2536 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java @@ -32,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayContains.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayContainsTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayFillTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayFillTest.java index 72bd09212d..5bbe0457a6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayFillTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayFillTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayFill.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayFillTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayGetTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayGetTest.java index 3d0d0fba93..190d53551c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayGetTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayGetTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayGet.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayGetTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIncludesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIncludesTest.java index 8e399b1e0e..053f3ec8c7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIncludesTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIncludesTest.java @@ -32,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayIncludes.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayIncludesTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIntersectsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIntersectsTest.java index 32174b535b..9bfc3239f4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIntersectsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayIntersectsTest.java @@ -32,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayIntersects.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayIntersectsTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayLengthTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayLengthTest.java index 5c505092dc..2b6f9d7b83 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayLengthTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayLengthTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayLength.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayLengthTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java index bc8a7fde83..67ca77e307 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayPosition.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayPositionTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java index 9c319c5fe2..d2f40d17b6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayPositions.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayPositionsTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPrependTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPrependTest.java index a3e8d5953b..75fadd1996 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPrependTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPrependTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayPrepend.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayPrependTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveIndexTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveIndexTest.java index ee1fb35fbb..9ebbf3df55 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveIndexTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveIndexTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayRemoveIndex.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayRemoveIndexTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveTest.java index 1d00496e29..98b0611b89 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayRemoveTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayRemove.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayRemoveTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReplaceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReplaceTest.java index ac090f188b..a69d9db21a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReplaceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayReplaceTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayReplace.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayReplaceTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySetTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySetTest.java index e4494e9f87..2d8a12c489 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySetTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySetTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArraySet.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArraySetTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySliceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySliceTest.java index bf4abf0a8a..aa007e04f8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySliceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArraySliceTest.java @@ -35,6 +35,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArraySlice.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) @SkipForDialect(dialectClass = CockroachDialect.class, reason = "See https://github.com/cockroachdb/cockroach/issues/32551") diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringTest.java index 1d65aa2541..08b44e2d1c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayToString.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayToStringTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayTrimTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayTrimTest.java index b5292c707e..a7765067d5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayTrimTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayTrimTest.java @@ -40,6 +40,7 @@ import static org.junit.jupiter.api.Assertions.fail; @DomainModel(annotatedClasses = EntityWithArrays.class) @SessionFactory @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsArrayTrim.class) // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete @BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) public class ArrayTrimTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayUnnestStructTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayUnnestStructTest.java new file mode 100644 index 0000000000..6cd6fded45 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayUnnestStructTest.java @@ -0,0 +1,287 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.array; + +import java.util.Arrays; +import java.util.List; + +import org.hibernate.annotations.Struct; +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaFunctionJoin; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.tree.SqmJoinType; + +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.Nulls; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = { + ArrayUnnestStructTest.Book.class, + ArrayUnnestStructTest.Publisher.class, + ArrayUnnestStructTest.Label.class +}) +@SessionFactory +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsTypedArrays.class) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsUnnest.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructAggregate.class) +// Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete +@BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) +public class ArrayUnnestStructTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new Book( 1L, "book1", new Publisher[0], List.of() ) ); + em.persist( new Book( + 2L, + "book2", + new Publisher[] { new Publisher( "abc" ), null, new Publisher( "def" ) }, + Arrays.asList( new Label( "k1", "v1" ), null, new Label( "k2", "v2" ) ) + ) ); + em.persist( new Book( 3L, "book3", null, null ) ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.createMutationQuery( "delete from Book" ).executeUpdate(); + } ); + } + + @Test + public void testUnnest(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-array-unnest-aggregate-example[] + List results = em.createQuery( + "select e.id, p.name, l.name, l.value " + + "from Book e " + + "join lateral unnest(e.publishers) p " + + "join lateral unnest(e.labels) l " + + "order by e.id, p.name nulls first, l.name nulls first, l.value nulls first", + Tuple.class + ) + .getResultList(); + //end::hql-array-unnest-aggregate-example[] + + // 1 row with 3 publishers and 3 labels => 3 * 3 = 9 + assertEquals( 9, results.size() ); + assertTupleEquals( results.get( 0 ), 2L, null, null, null ); + assertTupleEquals( results.get( 1 ), 2L, null, "k1", "v1" ); + assertTupleEquals( results.get( 2 ), 2L, null, "k2", "v2" ); + assertTupleEquals( results.get( 3 ), 2L, "abc", null, null ); + assertTupleEquals( results.get( 4 ), 2L, "abc", "k1", "v1" ); + assertTupleEquals( results.get( 5 ), 2L, "abc", "k2", "v2" ); + assertTupleEquals( results.get( 6 ), 2L, "def", null, null ); + assertTupleEquals( results.get( 7 ), 2L, "def", "k1", "v1" ); + assertTupleEquals( results.get( 8 ), 2L, "def", "k2", "v2" ); + } ); + } + + @Test + @SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "xmltable can't be used with a left join") + public void testNodeBuilderUnnest(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final JpaCriteriaQuery cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( Book.class ); + final JpaFunctionJoin p = root.joinArray( "publishers", SqmJoinType.LEFT ); + final JpaFunctionJoin