From 8dd50943665430d577c204e25cf3c796c12f5e09 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 29 Oct 2024 17:44:55 +0100 Subject: [PATCH] HHH-18758 Add json_table() set-returning function --- .../chapters/query/hql/QueryLanguage.adoc | 67 +- .../query/hql/extras/json_table_bnf.txt | 32 + .../dialect/CockroachLegacyDialect.java | 1 + .../community/dialect/DB2LegacyDialect.java | 3 +- .../community/dialect/H2LegacyDialect.java | 1 + .../community/dialect/HANALegacyDialect.java | 2 +- .../dialect/MariaDBLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 3 +- .../dialect/PostgreSQLLegacyDialect.java | 2 + .../dialect/SQLServerLegacyDialect.java | 1 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 6 + .../org/hibernate/grammars/hql/HqlParser.g4 | 31 + .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/DB2Dialect.java | 3 +- .../java/org/hibernate/dialect/H2Dialect.java | 1 + .../org/hibernate/dialect/HANADialect.java | 2 +- .../org/hibernate/dialect/MariaDBDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 3 +- .../hibernate/dialect/OracleJsonJdbcType.java | 6 + .../hibernate/dialect/PostgreSQLDialect.java | 2 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../dialect/aggregate/AggregateSupport.java | 35 +- .../aggregate/AggregateSupportImpl.java | 2 +- .../aggregate/DB2AggregateSupport.java | 6 +- .../aggregate/OracleAggregateSupport.java | 39 +- .../aggregate/PostgreSQLAggregateSupport.java | 7 +- .../function/CommonFunctionFactory.java | 158 ++- .../function/CteGenerateSeriesFunction.java | 2 - .../function/H2GenerateSeriesFunction.java | 2 - .../function/HANAGenerateSeriesFunction.java | 2 - .../SQLServerGenerateSeriesFunction.java | 2 - .../SybaseASEGenerateSeriesFunction.java | 2 - .../function/array/H2UnnestFunction.java | 2 - .../function/array/HANAUnnestFunction.java | 4 +- .../array/SQLServerUnnestFunction.java | 58 +- .../json/CastTargetReturnTypeResolver.java | 12 +- .../json/CockroachDBJsonExistsFunction.java | 31 +- .../json/CockroachDBJsonQueryFunction.java | 88 +- .../json/CockroachDBJsonTableFunction.java | 218 ++++ .../json/CockroachDBJsonValueFunction.java | 30 +- .../function/json/DB2JsonTableFunction.java | 438 ++++++++ .../function/json/DB2JsonValueFunction.java | 66 ++ .../function/json/H2JsonQueryFunction.java | 33 +- .../function/json/H2JsonTableFunction.java | 761 +++++++++++++ .../function/json/H2JsonValueFunction.java | 56 + .../function/json/HANAJsonTableFunction.java | 458 ++++++++ .../function/json/HANAJsonValueFunction.java | 66 ++ .../function/json/JsonArrayFunction.java | 6 + .../dialect/function/json/JsonPathHelper.java | 22 + .../function/json/JsonTableFunction.java | 361 +++++++ ...TableSetReturningFunctionTypeResolver.java | 146 +++ .../function/json/JsonValueFunction.java | 12 +- .../json/MariaDBJsonValueFunction.java | 18 +- .../function/json/MySQLJsonTableFunction.java | 184 ++++ .../function/json/MySQLJsonValueFunction.java | 18 +- .../json/OracleJsonArrayFunction.java | 17 + .../json/OracleJsonObjectFunction.java | 4 +- .../json/OracleJsonTableFunction.java | 153 +++ .../json/OracleJsonValueFunction.java | 69 ++ .../json/PostgreSQLJsonExistsFunction.java | 16 +- .../json/PostgreSQLJsonQueryFunction.java | 20 +- .../json/PostgreSQLJsonTableFunction.java | 237 +++++ .../json/PostgreSQLJsonValueFunction.java | 27 +- .../json/SQLServerJsonTableFunction.java | 263 +++++ .../internal/EmbeddableMappingTypeImpl.java | 3 +- .../criteria/HibernateCriteriaBuilder.java | 38 + .../query/criteria/JpaCastTarget.java | 38 + .../criteria/JpaJsonExistsExpression.java | 60 +- .../query/criteria/JpaJsonExistsNode.java | 68 ++ .../criteria/JpaJsonQueryExpression.java | 182 +--- .../query/criteria/JpaJsonQueryNode.java | 193 ++++ .../criteria/JpaJsonTableColumnsNode.java | 88 ++ .../query/criteria/JpaJsonTableFunction.java | 70 ++ .../criteria/JpaJsonValueExpression.java | 128 +-- .../query/criteria/JpaJsonValueNode.java | 139 +++ .../spi/HibernateCriteriaBuilderDelegate.java | 74 +- .../hql/internal/SemanticQueryBuilder.java | 210 ++-- .../org/hibernate/query/sqm/NodeBuilder.java | 24 + ...nderingSetReturningFunctionDescriptor.java | 3 - .../SelfRenderingSqmSetReturningFunction.java | 37 +- .../sqm/internal/SqmCriteriaNodeBuilder.java | 88 +- .../sqm/tree/expression/SqmCastTarget.java | 43 +- .../tree/expression/SqmJsonTableFunction.java | 997 ++++++++++++++++++ .../expression/SqmSetReturningFunction.java | 7 +- .../expression/JsonTableColumnDefinition.java | 21 + .../expression/JsonTableColumnsClause.java | 32 + .../expression/JsonTableErrorBehavior.java | 22 + .../JsonTableExistsColumnDefinition.java | 19 + .../JsonTableNestedColumnDefinition.java | 15 + .../JsonTableOrdinalityColumnDefinition.java | 13 + .../JsonTableQueryColumnDefinition.java | 22 + .../JsonTableValueColumnDefinition.java | 20 + .../jdbc/OracleJsonBlobJdbcType.java | 5 + .../orm/test/function/json/JsonTableTest.java | 198 ++++ .../orm/test/query/hql/JsonFunctionTests.java | 14 + .../orm/junit/DialectFeatureChecks.java | 6 + release-announcement.adoc | 9 +- 99 files changed, 6450 insertions(+), 759 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_table_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableSetReturningFunctionTypeResolver.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCastTarget.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsNode.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryNode.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableColumnsNode.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueNode.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonTableFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnDefinition.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnsClause.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableErrorBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableExistsColumnDefinition.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableNestedColumnDefinition.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableOrdinalityColumnDefinition.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableQueryColumnDefinition.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableValueColumnDefinition.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonTableTest.java 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 a4e431c7516f..afd36f9a1fd2 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1677,18 +1677,19 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi |=== | Function | Purpose -| `json_object()` | Constructs a JSON object from pairs of key and value arguments -| `json_array()` | Constructs a JSON array from arguments -| `json_value()` | Extracts a value from a JSON document by JSON path -| `json_exists()` | Checks if a JSON path exists in a JSON document -| `json_query()` | Queries non-scalar values by JSON path in a JSON document -| `json_arrayagg()` | Creates a JSON array by aggregating values -| `json_objectagg()` | Creates a JSON object by aggregating values -| `json_set()` | Inserts/Replaces a value by JSON path within a JSON document -| `json_remove()` | Removes a value by JSON path within a JSON document -| `json_mergepatch()` | Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge -| `json_array_append()` | Appends to a JSON array of a JSON document by JSON path -| `json_array_insert()` | Inserts a value by JSON path to a JSON array within a JSON document +| <> | Constructs a JSON object from pairs of key and value arguments +| <> | Constructs a JSON array from arguments +| <> | Extracts a value from a JSON document by JSON path +| <> | Checks if a JSON path exists in a JSON document +| <> | Queries non-scalar values by JSON path in a JSON document +| <> | Creates a JSON array by aggregating values +| <> | Creates a JSON object by aggregating values +| <> | Inserts/Replaces a value by JSON path within a JSON document +| <> | Removes a value by JSON path within a JSON document +| <> | Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge +| <> | Appends to a JSON array of a JSON document by JSON path +| <> | Inserts a value by JSON path to a JSON array within a JSON document +| <> | Turns a JSON document into rows |=== @@ -2203,6 +2204,47 @@ include::{json-example-dir-hql}/JsonArrayInsertTest.java[tags=hql-json-array-ins WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-json-table-function]] +===== `json_table()` + +A <>, which turns a JSON document argument into rows. +Returns no rows if the document is `null` or an empty JSON array. + +[[hql-json-table-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_table_bnf.txt[] +---- + +The first argument is the JSON document. The second optional argument represents the JSON path expression to use +in order to obtain JSON nodes for further processing. The default for the optional second argument is `$[*]` +i.e. access of root array elements. + +NOTE: If the root of the JSON document is an object, it is recommended to pass `$` as JSON path for portability. + +The `passing` clause can be used to pass values for variables in the JSON path. + +Attributes/columns that ought to be accessible via the `from` node alias are defined in the `columns` clause, +which can be of varying forms: + +* Value attributes - denoted by a `castTarget` after the name, behaves like <> +* Query attributes - denoted by the `json` type after the name, behaves like <> +* Exists attributes - denoted by the `exists` keyword after the name, behaves like <> +* Ordinal attributes - denoted by the `for ordinality` syntax after the name, gives access to the 1-based index of the currently processed array element +* Nested paths - declare a JSON path for processing of a nested `columns` clause + +[[hql-json-table-simple-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonTableTest.java[tags=hql-json-table-example] +---- +==== + +The `lateral` keyword is mandatory if one of the arguments refer to a from node item of the same query level. + +WARNING: H2 support is limited and HSQLDB as well as Sybase ASE do not support this function. + [[hql-functions-xml]] ==== Functions for dealing with XML @@ -2959,6 +3001,7 @@ The following set-returning functions are available on many platforms: | <> | Turns an array into rows | <> | Creates a series of values as rows +| <> | Turns a JSON document into rows |=== To use set returning functions defined in the database, it is required to register them in a `FunctionContributor`: diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_table_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_table_bnf.txt new file mode 100644 index 000000000000..53674cc45b8f --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_table_bnf.txt @@ -0,0 +1,32 @@ +"json_table(" expression ("," expression)? passingClause? columnsClause errorClause? ")" + +passingClause + : "passing" expression "as" identifier ("," expression "as" identifier)* + +columnsClause + : "columns(" column ("," column)* ")" + +column + : "nested" "path"? STRING_LITERAL columnsClause + | attributeName "json" wrapperClause? ("path" STRING_LITERAL)? queryOnErrorClause? queryOnEmptyClause? + | attributeName "for ordinality" + | attributeName "exists" ("path" STRING_LITERAL)? existsOnErrorClause? + | attributeName castTarget ("path" STRING_LITERAL)? valueOnErrorClause? valueOnEmptyClause? + +queryOnErrorClause + : ( "error" | "null" | ( "empty" ( "array" | "object" )? ) ) "on error"; + +queryOnEmptyClause + : ( "error" | "null" | ( "empty" ( "array" | "object" )? ) ) "on empty"; + +existsOnErrorClause + : ( "error" | "true" | "false" ) "on error" + +valueOnErrorClause + : ( "error" | "null" | ( "default" expression ) ) "on error"; + +valueOnEmptyClause + : ( "error" | "null" | ( "default" expression ) ) "on empty"; + +errorClause + : ( "error" | "null" ) "on error" \ No newline at end of file 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 a8bf9e5d5a9b..c6161e95a165 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 @@ -519,6 +519,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.unnest_postgresql(); functionFactory.generateSeries( null, "ordinality", true ); + functionFactory.jsonTable_cockroachdb(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 55445acd7957..4a7a9c70c277 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 @@ -432,13 +432,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.listagg( null ); if ( getDB2Version().isSameOrAfter( 11 ) ) { - functionFactory.jsonValue_no_passing(); + functionFactory.jsonValue_db2(); functionFactory.jsonQuery_no_passing(); functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_db2(); functionFactory.jsonArrayAgg_db2(); functionFactory.jsonObjectAgg_db2(); + functionFactory.jsonTable_db2(); } } 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 8ab9c65c37d2..b254a74f5b7f 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 @@ -429,6 +429,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.unnest_h2( getMaximumArraySize() ); functionFactory.generateSeries_h2( getMaximumSeriesSize() ); + functionFactory.jsonTable_h2( getMaximumArraySize() ); } /** 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 2bb7adcb4d8e..43f6bdf4f10c 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 @@ -495,7 +495,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_hana(); functionFactory.unnest_hana(); -// functionFactory.json_table(); + functionFactory.jsonTable_hana(); functionFactory.generateSeries_hana( getMaximumSeriesSize() ); 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 4cc8d2460b3f..564f2ad33488 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 @@ -100,6 +100,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { if ( getVersion().isSameOrAfter( 10, 6 ) ) { commonFunctionFactory.unnest_emulated(); + commonFunctionFactory.jsonTable_mysql(); } commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); 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 b34f51da9f32..ba4c01f6c3f3 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 @@ -674,6 +674,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getMySQLVersion().isSameOrAfter( 8 ) ) { functionFactory.unnest_emulated(); + functionFactory.jsonTable_mysql(); } if ( supportsRecursiveCTE() ) { functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, false ); 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 6d78be011dce..3c7716cd7126 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 @@ -312,7 +312,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonValue_oracle(); functionFactory.jsonQuery_oracle(); functionFactory.jsonExists_oracle(); - functionFactory.jsonObject_oracle(); + functionFactory.jsonObject_oracle( getVersion().isSameOrAfter( 19 ) ); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); functionFactory.jsonObjectAgg_oracle(); @@ -323,6 +323,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonMergepatch_oracle(); functionFactory.jsonArrayAppend_oracle(); functionFactory.jsonArrayInsert_oracle(); + functionFactory.jsonTable_oracle(); } functionFactory.xmlelement(); 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 49c816656460..c04740f10cb3 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 @@ -639,6 +639,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray(); functionFactory.jsonArrayAgg_postgresql( true ); functionFactory.jsonObjectAgg_postgresql( true ); + functionFactory.jsonTable(); } else { functionFactory.jsonValue_postgresql(); @@ -656,6 +657,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); } + functionFactory.jsonTable_postgresql(); } functionFactory.jsonSet_postgresql(); functionFactory.jsonRemove_postgresql(); 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 cd4c835f3483..a65d974a245e 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 @@ -419,6 +419,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); + functionFactory.jsonTable_sqlserver(); } functionFactory.xmlelement_sqlserver(); functionFactory.xmlcomment_sqlserver(); diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index ea55107b2246..8220db2ad31a 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -161,6 +161,7 @@ CASE : [cC] [aA] [sS] [eE]; CAST : [cC] [aA] [sS] [tT]; COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE]; COLUMN : [cC] [oO] [lL] [uU] [mM] [nN]; +COLUMNS : [cC] [oO] [lL] [uU] [mM] [nN] [sS]; CONDITIONAL : [cC] [oO] [nN] [dD] [iI] [tT] [iI] [oO] [nN] [aA] [lL]; CONFLICT : [cC] [oO] [nN] [fF] [lL] [iI] [cC] [tT]; CONSTRAINT : [cC] [oO] [nN] [sS] [tT] [rR] [aA] [iI] [nN] [tT]; @@ -224,12 +225,14 @@ INTERSECTS : [iI] [nN] [tT] [eE] [rR] [sS] [eE] [cC] [tT] [sS]; INTO : [iI] [nN] [tT] [oO]; IS : [iI] [sS]; JOIN : [jJ] [oO] [iI] [nN]; +JSON : [jJ] [sS] [oO] [nN]; JSON_ARRAY : [jJ] [sS] [oO] [nN] '_' [aA] [rR] [rR] [aA] [yY]; JSON_ARRAYAGG : [jJ] [sS] [oO] [nN] '_' [aA] [rR] [rR] [aA] [yY] [aA] [gG] [gG]; JSON_EXISTS : [jJ] [sS] [oO] [nN] '_' [eE] [xX] [iI] [sS] [tT] [sS]; JSON_OBJECT : [jJ] [sS] [oO] [nN] '_' [oO] [bB] [jJ] [eE] [cC] [tT]; JSON_OBJECTAGG : [jJ] [sS] [oO] [nN] '_' [oO] [bB] [jJ] [eE] [cC] [tT] [aA] [gG] [gG]; JSON_QUERY : [jJ] [sS] [oO] [nN] '_' [qQ] [uU] [eE] [rR] [yY]; +JSON_TABLE : [jJ] [sS] [oO] [nN] '_' [tT] [aA] [bB] [lL] [eE]; JSON_VALUE : [jJ] [sS] [oO] [nN] '_' [vV] [aA] [lL] [uU] [eE]; KEY : [kK] [eE] [yY]; KEYS : [kK] [eE] [yY] [sS]; @@ -260,6 +263,7 @@ MINUTE : [mM] [iI] [nN] [uU] [tT] [eE]; MONTH : [mM] [oO] [nN] [tT] [hH]; NAME : [nN] [aA] [mM] [eE]; NANOSECOND : [nN] [aA] [nN] [oO] [sS] [eE] [cC] [oO] [nN] [dD]; +NESTED : [nN] [eE] [sS] [tT] [eE] [dD]; NEW : [nN] [eE] [wW]; NEXT : [nN] [eE] [xX] [tT]; NO : [nN] [oO]; @@ -274,6 +278,7 @@ ON : [oO] [nN]; ONLY : [oO] [nN] [lL] [yY]; OR : [oO] [rR]; ORDER : [oO] [rR] [dD] [eE] [rR]; +ORDINALITY : [oO] [rR] [dD] [iI] [nN] [aA] [lL] [iI] [tT] [yY]; OTHERS : [oO] [tT] [hH] [eE] [rR] [sS]; OUTER : [oO] [uU] [tT] [eE] [rR]; OVER : [oO] [vV] [eE] [rR]; @@ -282,6 +287,7 @@ OVERLAY : [oO] [vV] [eE] [rR] [lL] [aA] [yY]; PAD : [pP] [aA] [dD]; PARTITION : [pP] [aA] [rR] [tT] [iI] [tT] [iI] [oO] [nN]; PASSING : [pP] [aA] [sS] [sS] [iI] [nN] [gG]; +PATH : [pP] [aA] [tT] [hH]; PERCENT : [pP] [eE] [rR] [cC] [eE] [nN] [tT]; PLACING : [pP] [lL] [aA] [cC] [iI] [nN] [gG]; POSITION : [pP] [oO] [sS] [iI] [tT] [iI] [oO] [nN]; 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 77ea591d91b8..f6e756c5133a 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 @@ -1118,6 +1118,7 @@ function setReturningFunction : simpleSetReturningFunction + | jsonTableFunction ; /** @@ -1730,6 +1731,30 @@ jsonUniqueKeysClause : (WITH|WITHOUT) UNIQUE KEYS ; +jsonTableFunction + : JSON_TABLE LEFT_PAREN expression (COMMA expression)? jsonPassingClause? jsonTableColumnsClause jsonTableErrorClause? RIGHT_PAREN + ; + +jsonTableErrorClause + : (ERROR|NULL) ON ERROR + ; + +jsonTableColumnsClause + : COLUMNS LEFT_PAREN jsonTableColumns RIGHT_PAREN + ; + +jsonTableColumns + : jsonTableColumn (COMMA jsonTableColumn)* + ; + +jsonTableColumn + : NESTED PATH? STRING_LITERAL jsonTableColumnsClause # JsonTableNestedColumn + | identifier JSON jsonQueryWrapperClause? (PATH STRING_LITERAL)? jsonQueryOnErrorOrEmptyClause? jsonQueryOnErrorOrEmptyClause? # JsonTableQueryColumn + | identifier FOR ORDINALITY # JsonTableOrdinalityColumn + | identifier EXISTS (PATH STRING_LITERAL)? jsonExistsOnErrorClause? # JsonTableExistsColumn + | identifier castTarget (PATH STRING_LITERAL)? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? # JsonTableValueColumn + ; + xmlFunction : xmlelementFunction | xmlforestFunction @@ -1820,6 +1845,7 @@ xmlaggFunction | CAST | COLLATE | COLUMN + | COLUMNS | CONDITIONAL | CONFLICT | CONSTRAINT @@ -1885,12 +1911,14 @@ xmlaggFunction | INTO | IS | JOIN + | JSON | JSON_ARRAY | JSON_ARRAYAGG | JSON_EXISTS | JSON_OBJECT | JSON_OBJECTAGG | JSON_QUERY + | JSON_TABLE | JSON_VALUE | KEY | KEYS @@ -1921,6 +1949,7 @@ xmlaggFunction | MONTH | NAME | NANOSECOND + | NESTED | NATURALID | NEW | NEXT @@ -1936,6 +1965,7 @@ xmlaggFunction | ONLY | OR | ORDER + | ORDINALITY | OTHERS // | OUTER | OVER @@ -1944,6 +1974,7 @@ xmlaggFunction | PAD | PARTITION | PASSING + | PATH | PERCENT | PLACING | POSITION 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 5b1d39163697..6fda9b7061c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -488,6 +488,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.unnest_postgresql(); functionFactory.generateSeries( null, "ordinality", true ); + functionFactory.jsonTable_cockroachdb(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 506d9d2e0c41..b6816b389e38 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -418,13 +418,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.listagg( null ); if ( getDB2Version().isSameOrAfter( 11 ) ) { - functionFactory.jsonValue_no_passing(); + functionFactory.jsonValue_db2(); functionFactory.jsonQuery_no_passing(); functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_db2(); functionFactory.jsonArrayAgg_db2(); functionFactory.jsonObjectAgg_db2(); + functionFactory.jsonTable_db2(); } functionFactory.xmlelement(); 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 39815a60265a..f380c66a72ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -360,6 +360,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.unnest_h2( getMaximumArraySize() ); functionFactory.generateSeries_h2( getMaximumSeriesSize() ); + functionFactory.jsonTable_h2( getMaximumArraySize() ); } /** 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 477446ca3300..125bd4623cfb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -503,7 +503,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_hana(); functionFactory.unnest_hana(); -// functionFactory.json_table(); + functionFactory.jsonTable_hana(); // Introduced in 2.0 SPS 04 functionFactory.jsonObject_hana(); 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 70d7d40e9e1c..5aadd93192d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -102,6 +102,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 10, 6 ) ) { commonFunctionFactory.unnest_emulated(); + commonFunctionFactory.jsonTable_mysql(); } commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); 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 c00b6d973382..cbca90d08d1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -659,6 +659,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getMySQLVersion().isSameOrAfter( 8 ) ) { functionFactory.unnest_emulated(); + functionFactory.jsonTable_mysql(); } if ( supportsRecursiveCTE() ) { functionFactory.generateSeries_recursive( getMaximumSeriesSize(), false, false ); 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 b7a92ddaed63..2cc949d4be1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -401,7 +401,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonValue_oracle(); functionFactory.jsonQuery_oracle(); functionFactory.jsonExists_oracle(); - functionFactory.jsonObject_oracle(); + functionFactory.jsonObject_oracle( getVersion().isSameOrAfter( 19 ) ); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); functionFactory.jsonObjectAgg_oracle(); @@ -424,6 +424,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.unnest_oracle(); functionFactory.generateSeries_recursive( getMaximumSeriesSize(), true, false ); + functionFactory.jsonTable_oracle(); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonJdbcType.java index d4cbc8fb2d23..3f2e451935dd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonJdbcType.java @@ -6,6 +6,7 @@ import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; @@ -26,6 +27,11 @@ private OracleJsonJdbcType(EmbeddableMappingType embeddableMappingType) { super( embeddableMappingType ); } + @Override + public int getDdlTypeCode() { + return SqlTypes.JSON; + } + @Override public String toString() { return "OracleJsonJdbcType"; 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 3f5b0ceea6cf..4878c7093af8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -601,6 +601,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray(); functionFactory.jsonArrayAgg_postgresql( true ); functionFactory.jsonObjectAgg_postgresql( true ); + functionFactory.jsonTable(); } else { functionFactory.jsonValue_postgresql(); @@ -618,6 +619,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); } + functionFactory.jsonTable_postgresql(); } functionFactory.jsonSet_postgresql(); functionFactory.jsonRemove_postgresql(); 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 e2c6e17280fd..00445d53b7b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -436,6 +436,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); + functionFactory.jsonTable_sqlserver(); } functionFactory.xmlelement_sqlserver(); functionFactory.xmlcomment_sqlserver(); 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 69a87ae3f491..1a948106b8a4 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 @@ -18,6 +18,7 @@ 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.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -48,12 +49,15 @@ default String aggregateComponentCustomReadExpression( String columnExpression, AggregateColumn aggregateColumn, Column column) { + final int sqlTypeCode = aggregateColumn.getType().getJdbcType().getDefaultSqlTypeCode(); return aggregateComponentCustomReadExpression( template, placeholder, aggregateParentReadExpression, columnExpression, - aggregateColumn.getTypeCode(), + // We need to know what array this is STRUCT_ARRAY/JSON_ARRAY/XML_ARRAY, + // which we can easily get from the type code of the aggregate column + sqlTypeCode == SqlTypes.ARRAY ? aggregateColumn.getTypeCode() : sqlTypeCode, new SqlTypedMappingImpl( column.getTypeName(), column.getLength(), @@ -96,10 +100,37 @@ String aggregateComponentCustomReadExpression( * @param aggregateColumn The type information for the aggregate column * @param column The column within the aggregate type, for which to return the assignment expression */ - String aggregateComponentAssignmentExpression( + default String aggregateComponentAssignmentExpression( String aggregateParentAssignmentExpression, String columnExpression, AggregateColumn aggregateColumn, + Column column) { + final int sqlTypeCode = aggregateColumn.getType().getJdbcType().getDefaultSqlTypeCode(); + return aggregateComponentAssignmentExpression( + aggregateParentAssignmentExpression, + columnExpression, + // We need to know what array this is STRUCT_ARRAY/JSON_ARRAY/XML_ARRAY, + // which we can easily get from the type code of the aggregate column + sqlTypeCode == SqlTypes.ARRAY ? aggregateColumn.getTypeCode() : sqlTypeCode, + column + ); + } + + /** + * Returns the assignment expression to use for {@code column}, + * which is part of the aggregate type of {@code aggregatePath}. + * + * @param aggregateParentAssignmentExpression The expression to the aggregate column, which contains the column + * @param columnExpression The column within the aggregate type, for which to return the assignment expression + * @param aggregateColumnTypeCode The SQL type code of the aggregate column + * @param column The column within the aggregate type, for which to return the assignment expression + * + * @since 7.0 + */ + String aggregateComponentAssignmentExpression( + String aggregateParentAssignmentExpression, + String columnExpression, + int aggregateColumnTypeCode, Column 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 cf19aba14dfe..4f1c78887e85 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 @@ -28,7 +28,7 @@ public String aggregateComponentCustomReadExpression(String template, String pla public String aggregateComponentAssignmentExpression( String aggregateParentAssignmentExpression, String columnExpression, - AggregateColumn aggregateColumn, + int aggregateColumnTypeCode, Column column) { throw new UnsupportedOperationException( "Dialect does not support aggregateComponentAssignmentExpression: " + 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 6dca601d4c84..8a1a5567fc31 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 @@ -61,13 +61,13 @@ public String aggregateComponentCustomReadExpression( public String aggregateComponentAssignmentExpression( String aggregateParentAssignmentExpression, String columnExpression, - AggregateColumn aggregateColumn, + int aggregateColumnTypeCode, Column column) { - switch ( aggregateColumn.getTypeCode() ) { + switch ( aggregateColumnTypeCode ) { case STRUCT: return aggregateParentAssignmentExpression + ".." + 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 247c28357a44..6564dea9b97d 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 @@ -49,6 +49,7 @@ import static org.hibernate.type.SqlTypes.DATE; 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.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.NCLOB; import static org.hibernate.type.SqlTypes.SMALLINT; @@ -118,6 +119,7 @@ public String aggregateComponentCustomReadExpression( SqlTypedMapping column) { switch ( aggregateColumnTypeCode ) { case JSON: + case JSON_ARRAY: String jsonTypeName = "json"; switch ( jsonSupport ) { case MERGEPATCH: @@ -241,10 +243,11 @@ public String aggregateComponentCustomReadExpression( public String aggregateComponentAssignmentExpression( String aggregateParentAssignmentExpression, String columnExpression, - AggregateColumn aggregateColumn, + int aggregateColumnTypeCode, Column column) { - switch ( aggregateColumn.getTypeCode() ) { + switch ( aggregateColumnTypeCode ) { case JSON: + case JSON_ARRAY: // For JSON we always have to replace the whole object return aggregateParentAssignmentExpression; case STRUCT: @@ -252,7 +255,7 @@ public String aggregateComponentAssignmentExpression( case STRUCT_TABLE: return aggregateParentAssignmentExpression + "." + columnExpression; } - throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumn.getTypeCode() ); + throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode ); } private String jsonCustomWriteExpression( @@ -264,6 +267,8 @@ private String jsonCustomWriteExpression( switch ( jsonSupport ) { case OSON: case MERGEPATCH: + case QUERY_AND_PATH: + case QUERY: switch ( sqlTypeCode ) { case CLOB: return "to_clob(" + customWriteExpression + ")"; @@ -411,13 +416,14 @@ void append( AggregateColumnWriteExpression expression); } private static class AggregateJsonWriteExpression implements JsonWriteExpression { + private final boolean colonSyntax; private final LinkedHashMap subExpressions = new LinkedHashMap<>(); protected final EmbeddableMappingType embeddableMappingType; protected final String ddlTypeName; - public AggregateJsonWriteExpression( - SelectableMapping selectableMapping, - OracleAggregateSupport aggregateSupport) { + public AggregateJsonWriteExpression(SelectableMapping selectableMapping, OracleAggregateSupport aggregateSupport) { + this.colonSyntax = aggregateSupport.jsonSupport == JsonSupport.OSON + || aggregateSupport.jsonSupport == JsonSupport.MERGEPATCH; this.embeddableMappingType = ( (AggregateJdbcType) selectableMapping.getJdbcMapping().getJdbcType() ) .getEmbeddableMappingType(); this.ddlTypeName = aggregateSupport.determineJsonTypeName( selectableMapping ); @@ -452,7 +458,8 @@ protected void initializeSubExpressions( column.getJdbcMapping(), column, typeConfiguration - ) + ), + colonSyntax ) ); } @@ -474,7 +481,12 @@ public void append( if ( value instanceof AggregateJsonWriteExpression ) { sb.append( '\'' ); sb.append( column ); - sb.append( "':" ); + if ( colonSyntax ) { + sb.append( "':" ); + } + else { + sb.append( "' value " ); + } value.append( sb, subPath, translator, expression ); } else { @@ -538,12 +550,14 @@ public void render( private static class BasicJsonWriteExpression implements JsonWriteExpression { + private final boolean colonSyntax; private final SelectableMapping selectableMapping; private final String customWriteExpressionStart; private final String customWriteExpressionEnd; - BasicJsonWriteExpression(SelectableMapping selectableMapping, String customWriteExpression) { + BasicJsonWriteExpression(SelectableMapping selectableMapping, String customWriteExpression, boolean colonSyntax) { this.selectableMapping = selectableMapping; + this.colonSyntax = colonSyntax; if ( customWriteExpression.equals( "?" ) ) { this.customWriteExpressionStart = ""; this.customWriteExpressionEnd = ""; @@ -564,7 +578,12 @@ public void append( AggregateColumnWriteExpression expression) { sb.append( '\'' ); sb.append( selectableMapping.getSelectableName() ); - sb.append( "':" ); + if ( colonSyntax ) { + sb.append( "':" ); + } + else { + sb.append( "' value " ); + } sb.append( customWriteExpressionStart ); // We use NO_UNTYPED here so that expressions which require type inference are casted explicitly, // since we don't know how the custom write expression looks like where this is embedded, 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 061132400f4c..f177fc62d72e 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 @@ -9,7 +9,6 @@ import org.hibernate.dialect.Dialect; import org.hibernate.internal.util.StringHelper; -import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.Column; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.SelectableMapping; @@ -144,9 +143,9 @@ private static String jsonCustomWriteExpression(String customWriteExpression, Jd public String aggregateComponentAssignmentExpression( String aggregateParentAssignmentExpression, String columnExpression, - AggregateColumn aggregateColumn, + int aggregateColumnTypeCode, Column column) { - switch ( aggregateColumn.getTypeCode() ) { + switch ( aggregateColumnTypeCode ) { case JSON: case JSON_ARRAY: // For JSON we always have to replace the whole object @@ -156,7 +155,7 @@ public String aggregateComponentAssignmentExpression( case STRUCT_TABLE: return aggregateParentAssignmentExpression + "." + 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/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 9b3836517b6e..8840d1ee59cd 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 @@ -12,83 +12,7 @@ import org.hibernate.dialect.Dialect; 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; -import org.hibernate.dialect.function.json.CockroachDBJsonValueFunction; -import org.hibernate.dialect.function.json.DB2JsonArrayAggFunction; -import org.hibernate.dialect.function.json.DB2JsonArrayFunction; -import org.hibernate.dialect.function.json.DB2JsonObjectAggFunction; -import org.hibernate.dialect.function.json.DB2JsonObjectFunction; -import org.hibernate.dialect.function.json.H2JsonArrayAggFunction; -import org.hibernate.dialect.function.json.H2JsonExistsFunction; -import org.hibernate.dialect.function.json.H2JsonObjectAggFunction; -import org.hibernate.dialect.function.json.H2JsonQueryFunction; -import org.hibernate.dialect.function.json.H2JsonValueFunction; -import org.hibernate.dialect.function.json.HANAJsonArrayAggFunction; -import org.hibernate.dialect.function.json.HANAJsonArrayFunction; -import org.hibernate.dialect.function.json.HANAJsonExistsFunction; -import org.hibernate.dialect.function.json.HANAJsonObjectAggFunction; -import org.hibernate.dialect.function.json.HANAJsonObjectFunction; -import org.hibernate.dialect.function.json.HSQLJsonArrayAggFunction; -import org.hibernate.dialect.function.json.HSQLJsonArrayFunction; -import org.hibernate.dialect.function.json.HSQLJsonObjectFunction; -import org.hibernate.dialect.function.json.JsonArrayFunction; -import org.hibernate.dialect.function.json.JsonExistsFunction; -import org.hibernate.dialect.function.json.JsonObjectFunction; -import org.hibernate.dialect.function.json.JsonQueryFunction; -import org.hibernate.dialect.function.json.JsonValueFunction; -import org.hibernate.dialect.function.json.MariaDBJsonArrayAggFunction; -import org.hibernate.dialect.function.json.MariaDBJsonArrayAppendFunction; -import org.hibernate.dialect.function.json.MariaDBJsonArrayFunction; -import org.hibernate.dialect.function.json.MariaDBJsonObjectAggFunction; -import org.hibernate.dialect.function.json.MariaDBJsonQueryFunction; -import org.hibernate.dialect.function.json.MariaDBJsonValueFunction; -import org.hibernate.dialect.function.json.MySQLJsonArrayAggFunction; -import org.hibernate.dialect.function.json.MySQLJsonArrayFunction; -import org.hibernate.dialect.function.json.MySQLJsonExistsFunction; -import org.hibernate.dialect.function.json.MySQLJsonObjectAggFunction; -import org.hibernate.dialect.function.json.MySQLJsonObjectFunction; -import org.hibernate.dialect.function.json.MySQLJsonQueryFunction; -import org.hibernate.dialect.function.json.MySQLJsonValueFunction; -import org.hibernate.dialect.function.json.OracleJsonArrayAggFunction; -import org.hibernate.dialect.function.json.OracleJsonArrayAppendFunction; -import org.hibernate.dialect.function.json.OracleJsonArrayFunction; -import org.hibernate.dialect.function.json.OracleJsonArrayInsertFunction; -import org.hibernate.dialect.function.json.OracleJsonInsertFunction; -import org.hibernate.dialect.function.json.OracleJsonMergepatchFunction; -import org.hibernate.dialect.function.json.OracleJsonObjectAggFunction; -import org.hibernate.dialect.function.json.OracleJsonObjectFunction; -import org.hibernate.dialect.function.json.OracleJsonRemoveFunction; -import org.hibernate.dialect.function.json.OracleJsonReplaceFunction; -import org.hibernate.dialect.function.json.OracleJsonSetFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAppendFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonArrayInsertFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonInsertFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonMergepatchFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonObjectAggFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonRemoveFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonReplaceFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonSetFunction; -import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; -import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; -import org.hibernate.dialect.function.json.SQLServerJsonArrayAppendFunction; -import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; -import org.hibernate.dialect.function.json.SQLServerJsonArrayInsertFunction; -import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; -import org.hibernate.dialect.function.json.SQLServerJsonInsertFunction; -import org.hibernate.dialect.function.json.SQLServerJsonObjectAggFunction; -import org.hibernate.dialect.function.json.SQLServerJsonObjectFunction; -import org.hibernate.dialect.function.json.SQLServerJsonQueryFunction; -import org.hibernate.dialect.function.json.SQLServerJsonRemoveFunction; -import org.hibernate.dialect.function.json.SQLServerJsonReplaceFunction; -import org.hibernate.dialect.function.json.SQLServerJsonSetFunction; -import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; +import org.hibernate.dialect.function.json.*; import org.hibernate.dialect.function.xml.H2XmlConcatFunction; import org.hibernate.dialect.function.xml.H2XmlElementFunction; import org.hibernate.dialect.function.xml.H2XmlForestFunction; @@ -3365,17 +3289,24 @@ public void jsonValue() { } /** - * json_value() function that doesn't support the passing clause + * HANA json_value() function */ public void jsonValue_no_passing() { - functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true, false ) ); + functionRegistry.register( "json_value", new HANAJsonValueFunction( typeConfiguration ) ); } /** * Oracle json_value() function */ public void jsonValue_oracle() { - functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, false, false ) ); + functionRegistry.register( "json_value", new OracleJsonValueFunction( typeConfiguration ) ); + } + + /** + * DB2 json_value() function + */ + public void jsonValue_db2() { + functionRegistry.register( "json_value", new DB2JsonValueFunction( typeConfiguration ) ); } /** @@ -3563,8 +3494,8 @@ public void jsonObject_db2() { /** * Oracle json_object() function */ - public void jsonObject_oracle() { - functionRegistry.register( "json_object", new OracleJsonObjectFunction( typeConfiguration ) ); + public void jsonObject_oracle(boolean colonSyntax) { + functionRegistry.register( "json_object", new OracleJsonObjectFunction( colonSyntax, typeConfiguration ) ); } /** @@ -4329,4 +4260,67 @@ public void generateSeries_sybasease(int maxSeriesSize) { public void generateSeries_hana(int maxSeriesSize) { functionRegistry.register( "generate_series", new HANAGenerateSeriesFunction( maxSeriesSize, typeConfiguration ) ); } + + /** + * Standard json_table() function + */ + public void jsonTable() { + functionRegistry.register( "json_table", new JsonTableFunction( typeConfiguration ) ); + } + + /** + * Oracle json_table() function + */ + public void jsonTable_oracle() { + functionRegistry.register( "json_table", new OracleJsonTableFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL json_table() function + */ + public void jsonTable_postgresql() { + functionRegistry.register( "json_table", new PostgreSQLJsonTableFunction( typeConfiguration ) ); + } + + /** + * CockroachDB json_table() function + */ + public void jsonTable_cockroachdb() { + functionRegistry.register( "json_table", new CockroachDBJsonTableFunction( typeConfiguration ) ); + } + + /** + * MySQL json_table() function + */ + public void jsonTable_mysql() { + functionRegistry.register( "json_table", new MySQLJsonTableFunction( typeConfiguration ) ); + } + + /** + * DB2 json_table() function + */ + public void jsonTable_db2() { + functionRegistry.register( "json_table", new DB2JsonTableFunction( typeConfiguration ) ); + } + + /** + * HANA json_table() function + */ + public void jsonTable_hana() { + functionRegistry.register( "json_table", new HANAJsonTableFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_table() function + */ + public void jsonTable_sqlserver() { + functionRegistry.register( "json_table", new SQLServerJsonTableFunction( typeConfiguration ) ); + } + + /** + * H2 json_table() function + */ + public void jsonTable_h2(int maximumArraySize) { + functionRegistry.register( "json_table", new H2JsonTableFunction( maximumArraySize, typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CteGenerateSeriesFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CteGenerateSeriesFunction.java index 6ccc9dd9878a..2b1bb5435341 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CteGenerateSeriesFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CteGenerateSeriesFunction.java @@ -10,7 +10,6 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; -import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; @@ -78,7 +77,6 @@ protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFun arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), queryEngine.getCriteriaBuilder(), getName() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/H2GenerateSeriesFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/H2GenerateSeriesFunction.java index 0e7258043e18..8623318902b0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/H2GenerateSeriesFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/H2GenerateSeriesFunction.java @@ -13,7 +13,6 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; -import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; @@ -70,7 +69,6 @@ protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFun arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), queryEngine.getCriteriaBuilder(), getName() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/HANAGenerateSeriesFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/HANAGenerateSeriesFunction.java index e519262da5fb..a80ba3297930 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/HANAGenerateSeriesFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/HANAGenerateSeriesFunction.java @@ -9,7 +9,6 @@ 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.spi.QueryEngine; import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; @@ -69,7 +68,6 @@ protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFun arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), queryEngine.getCriteriaBuilder(), getName() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerGenerateSeriesFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerGenerateSeriesFunction.java index 8764e987bd80..b6c347c105e7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerGenerateSeriesFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SQLServerGenerateSeriesFunction.java @@ -13,7 +13,6 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; -import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; @@ -67,7 +66,6 @@ protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFun arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), queryEngine.getCriteriaBuilder(), getName() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseASEGenerateSeriesFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseASEGenerateSeriesFunction.java index 748a88edb1b6..e9a257e9e5a6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseASEGenerateSeriesFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SybaseASEGenerateSeriesFunction.java @@ -8,7 +8,6 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; -import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; @@ -61,7 +60,6 @@ protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFun arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), queryEngine.getCriteriaBuilder(), getName() ) { 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 index 9bdb399a8db3..b30c6af36a14 100644 --- 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 @@ -17,7 +17,6 @@ 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; @@ -71,7 +70,6 @@ protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFun arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), queryEngine.getCriteriaBuilder(), getName() ) { 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 index 6bcc43bdb786..183876859302 100644 --- 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 @@ -25,7 +25,6 @@ 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; @@ -82,7 +81,6 @@ protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFun arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( arguments, queryEngine.getTypeConfiguration() ), queryEngine.getCriteriaBuilder(), getName() ) { @@ -520,7 +518,7 @@ public void renderToSql( separator = ','; } sqlAppender.appendSql( " from sys.dummy for json('arraywrap'='no')))||" ); - sqlAppender.appendSql( "'\"v\":'||" ); + sqlAppender.appendSql( "',\"v\":'||" ); argument.accept( walker ); sqlAppender.appendSql( "||'}'" ); } 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 index c1f2f385c980..1e908e4eb2eb 100644 --- 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 @@ -4,9 +4,9 @@ */ package org.hibernate.dialect.function.array; - import org.hibernate.dialect.XmlHelper; 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; @@ -35,23 +35,29 @@ protected void renderJsonTable( AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator walker) { - sqlAppender.appendSql( "openjson(" ); + final ModelPart ordinalityPart = tupleType.findSubPart( CollectionPart.Nature.INDEX.getName(), null ); + if ( ordinalityPart != null ) { + sqlAppender.appendSql( "(select t.*,row_number() over (order by (select null)) " ); + sqlAppender.appendSql( ordinalityPart.asBasicValuedModelPart().getSelectableName() ); + sqlAppender.appendSql( " from openjson(" ); + } + else { + sqlAppender.appendSql( "openjson(" ); + } array.accept( walker ); sqlAppender.appendSql( ",'$[*]') with (" ); + boolean[] comma = new boolean[1]; 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 { + if ( !CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + if ( comma[0] ) { + sqlAppender.append( ',' ); + } + else { + sqlAppender.append( ' ' ); + comma[0] = true; + } sqlAppender.append( selectableMapping.getSelectionExpression() ); sqlAppender.append( ' ' ); sqlAppender.append( getDdlType( selectableMapping, walker ) ); @@ -63,17 +69,14 @@ protected void renderJsonTable( } 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 { + if ( !CollectionPart.Nature.INDEX.getName().equals( selectableMapping.getSelectableName() ) ) { + if ( comma[0] ) { + sqlAppender.append( ',' ); + } + else { + sqlAppender.append( ' ' ); + comma[0] = true; + } sqlAppender.append( selectableMapping.getSelectionExpression() ); sqlAppender.append( ' ' ); sqlAppender.append( getDdlType( selectableMapping, walker ) ); @@ -82,7 +85,12 @@ protected void renderJsonTable( } ); } - sqlAppender.appendSql( ')' ); + if ( ordinalityPart != null ) { + sqlAppender.appendSql( ")t)" ); + } + else { + sqlAppender.appendSql( ')' ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java index 102c6d5ea137..1f272558b177 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java @@ -40,14 +40,14 @@ public ReturnableType resolveFunctionReturnType( List> arguments, TypeConfiguration typeConfiguration) { if ( arguments.size() > 2 ) { - int castTargetIndex = -1; + int castTargetIndex = 0; for ( int i = 2; i < arguments.size(); i++ ) { if (arguments.get( i ) instanceof SqmCastTarget ) { - castTargetIndex = i; + castTargetIndex = i + 1; break; } } - if ( castTargetIndex != -1 ) { + if ( castTargetIndex != 0 ) { ReturnableType argType = extractArgumentType( arguments, castTargetIndex ); return isAssignableTo( argType, impliedType ) ? impliedType : argType; } @@ -60,14 +60,14 @@ public BasicValuedMapping resolveFunctionReturnType( Supplier impliedTypeAccess, List arguments) { if ( arguments.size() > 2 ) { - int castTargetIndex = -1; + int castTargetIndex = 0; for ( int i = 2; i < arguments.size(); i++ ) { if (arguments.get( i ) instanceof CastTarget ) { - castTargetIndex = i; + castTargetIndex = i + 1; break; } } - if ( castTargetIndex != -1 ) { + if ( castTargetIndex != 0 ) { final BasicValuedMapping specifiedArgType = extractArgumentValuedMapping( arguments, castTargetIndex ); return useImpliedTypeIfPossible( specifiedArgType, impliedTypeAccess.get() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java index 79778d362f3a..36f283f8f191 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java @@ -6,6 +6,7 @@ import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.QueryException; import org.hibernate.dialect.Dialect; import org.hibernate.query.ReturnableType; @@ -13,6 +14,7 @@ import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; import org.hibernate.type.spi.TypeConfiguration; @@ -31,6 +33,10 @@ protected void render( JsonExistsArguments arguments, ReturnableType returnType, SqlAstTranslator walker) { + // jsonb_path_exists errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonExistsErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on CockroachDB" ); + } final String jsonPath; try { jsonPath = walker.getLiteralValue( arguments.jsonPath() ); @@ -38,15 +44,31 @@ protected void render( catch (Exception ex) { throw new QueryException( "CockroachDB json_value only support literal json paths, but got " + arguments.jsonPath() ); } - final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); - final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + appendJsonExists( + sqlAppender, + arguments.jsonDocument(), + JsonPathHelper.parseJsonPathElements( jsonPath ), + arguments.isJsonType(), + arguments.passingClause(), + walker + ); + } + + static void appendJsonExists( + SqlAppender sqlAppender, + Expression jsonDocument, + List jsonPathElements, + boolean isJsonType, + @Nullable JsonPathPassingClause jsonPathPassingClause, + SqlAstTranslator walker) { + final boolean needsCast = !isJsonType && jsonDocument instanceof JdbcParameter; if ( needsCast ) { sqlAppender.appendSql( "cast(" ); } else { sqlAppender.appendSql( '(' ); } - arguments.jsonDocument().accept( walker ); + jsonDocument.accept( walker ); if ( needsCast ) { sqlAppender.appendSql( " as jsonb)" ); } @@ -62,12 +84,11 @@ protected void render( dialect.appendLiteral( sqlAppender, attribute.attribute() ); } else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { - final JsonPathPassingClause jsonPathPassingClause = arguments.passingClause(); assert jsonPathPassingClause != null; final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) jsonPathElement ).parameterName(); final Expression expression = jsonPathPassingClause.getPassingExpressions().get( parameterName ); if ( expression == null ) { - throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + throw new QueryException( "JSON path [" + JsonPathHelper.toJsonPath( jsonPathElements ) + "] uses parameter [" + parameterName + "] that is not passed" ); } sqlAppender.appendSql( "cast(" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java index c51992bf88bd..b0d537ff736a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java @@ -6,6 +6,7 @@ import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.QueryException; import org.hibernate.dialect.Dialect; import org.hibernate.query.ReturnableType; @@ -36,16 +37,17 @@ protected void render( SqlAstTranslator walker) { // jsonb_path_query functions error by default if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR ) { - throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + throw new QueryException( "Can't emulate on error clause on CockroachDB" ); } if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { - throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + throw new QueryException( "Can't emulate on empty clause on CockroachDB" ); } final JsonQueryWrapMode wrapMode = arguments.wrapMode(); if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sqlAppender.appendSql( "jsonb_build_array(" ); } + final Expression jsonDocumentExpression = arguments.jsonDocument(); final String jsonPath; try { jsonPath = walker.getLiteralValue( arguments.jsonPath() ); @@ -53,53 +55,71 @@ protected void render( catch (Exception ex) { throw new QueryException( "CockroachDB json_value only support literal json paths, but got " + arguments.jsonPath() ); } - final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); - final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + appendJsonQuery( + sqlAppender, + jsonDocumentExpression, + JsonPathHelper.parseJsonPathElements( jsonPath ), + arguments.isJsonType(), + arguments.passingClause(), + walker + ); + + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( ")" ); + } + } + + static void appendJsonQuery( + SqlAppender sqlAppender, + Expression jsonDocumentExpression, + List jsonPathElements, + boolean isJsonType, + @Nullable JsonPathPassingClause jsonPathPassingClause, + SqlAstTranslator walker) { + final boolean needsCast = !isJsonType && jsonDocumentExpression instanceof JdbcParameter; if ( needsCast ) { sqlAppender.appendSql( "cast(" ); } else { sqlAppender.appendSql( '(' ); } - arguments.jsonDocument().accept( walker ); + jsonDocumentExpression.accept( walker ); if ( needsCast ) { sqlAppender.appendSql( " as jsonb)" ); } else { sqlAppender.appendSql( ')' ); } - sqlAppender.appendSql( "#>array" ); - char separator = '['; - final Dialect dialect = walker.getSessionFactory().getJdbcServices().getDialect(); - for ( JsonPathHelper.JsonPathElement jsonPathElement : jsonPathElements ) { - sqlAppender.appendSql( separator ); - if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) { - dialect.appendLiteral( sqlAppender, attribute.attribute() ); - } - else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { - final JsonPathPassingClause jsonPathPassingClause = arguments.passingClause(); - assert jsonPathPassingClause != null; - final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) jsonPathElement ).parameterName(); - final Expression expression = jsonPathPassingClause.getPassingExpressions().get( parameterName ); - if ( expression == null ) { - throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + if ( !jsonPathElements.isEmpty() ) { + sqlAppender.appendSql( "#>array" ); + char separator = '['; + final Dialect dialect = walker.getSessionFactory().getJdbcServices().getDialect(); + for ( JsonPathHelper.JsonPathElement jsonPathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + dialect.appendLiteral( sqlAppender, attribute.attribute() ); } + else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + assert jsonPathPassingClause != null; + final String parameterName = ((JsonPathHelper.JsonParameterIndexAccess) jsonPathElement).parameterName(); + final Expression expression = jsonPathPassingClause.getPassingExpressions().get( parameterName ); + if ( expression == null ) { + throw new QueryException( + "JSON path [" + JsonPathHelper.toJsonPath( jsonPathElements ) + "] uses parameter [" + parameterName + "] that is not passed" ); + } - sqlAppender.appendSql( "cast(" ); - expression.accept( walker ); - sqlAppender.appendSql( " as text)" ); - } - else { - sqlAppender.appendSql( '\'' ); - sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() ); - sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( "cast(" ); + expression.accept( walker ); + sqlAppender.appendSql( " as text)" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ((JsonPathHelper.JsonIndexAccess) jsonPathElement).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; } - separator = ','; - } - sqlAppender.appendSql( ']' ); - - if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { - sqlAppender.appendSql( ")" ); + sqlAppender.appendSql( ']' ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonTableFunction.java new file mode 100644 index 000000000000..e02387af8358 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonTableFunction.java @@ -0,0 +1,218 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +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.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.Collections; +import java.util.List; + +/** + * CockroachDB json_table function. + */ +public class CockroachDBJsonTableFunction extends PostgreSQLJsonTableFunction { + + public CockroachDBJsonTableFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() == JsonTableErrorBehavior.NULL ) { + throw new QueryException( "Can't emulate null on error clause on CockroachDB" ); + } + final SqlAstNode jsonPathExpression = arguments.jsonPath(); + final List jsonPathElements; + final boolean isArray; + if ( jsonPathExpression == null ) { + jsonPathElements = Collections.emptyList(); + // Assume array by default + isArray = true; + } + else { + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + catch (Exception ex) { + throw new QueryException( "CockroachDB json_table only support literal json paths, but got " + arguments.jsonPath() ); + } + isArray = jsonPath.endsWith( "[*]" ); + if ( isArray ) { + jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath.substring( 0, jsonPath.length() - 3 ) ); + } + else { + jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + } + } + + sqlAppender.appendSql( "(select" ); + + renderColumns( sqlAppender, arguments.columnsClause(), 0, walker ); + + sqlAppender.appendSql( " from " ); + if ( isArray ) { + sqlAppender.appendSql( "jsonb_array_elements(" ); + } + else { + sqlAppender.appendSql( "(values (" ); + } + CockroachDBJsonQueryFunction.appendJsonQuery( + sqlAppender, + arguments.jsonDocument(), + jsonPathElements, + arguments.isJsonType(), + arguments.passingClause(), + walker + ); + if ( isArray ) { + sqlAppender.appendSql( ") with ordinality t0(d,i)" ); + } + else { + sqlAppender.appendSql( ",1)) t0(d,i)" ); + } + renderNestedColumnJoins( sqlAppender, arguments.columnsClause(), 0, walker ); + sqlAppender.appendSql( ')' ); + } + + @Override + protected int renderNestedColumnJoins(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + int nextClauseLevel = clauseLevel; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + sqlAppender.appendSql( " left join lateral " ); + final boolean isArray = nestedColumnDefinition.jsonPath().endsWith( "[*]" ); + final String jsonPath; + if ( isArray ) { + jsonPath = nestedColumnDefinition.jsonPath().substring( 0, nestedColumnDefinition.jsonPath().length() - 3 ); + sqlAppender.appendSql( "jsonb_array_elements(" ); + } + else { + jsonPath = nestedColumnDefinition.jsonPath(); + sqlAppender.appendSql( "(values (" ); + } + CockroachDBJsonQueryFunction.appendJsonQuery( + sqlAppender, + new ClauseLevelDocumentExpression( clauseLevel ), + JsonPathHelper.parseJsonPathElements( jsonPath ), + true, + null, + walker + ); + if ( isArray ) { + sqlAppender.appendSql( ") with ordinality t" ); + } + else { + sqlAppender.appendSql( ",1)) t" ); + } + sqlAppender.appendSql( clauseLevel + 1 ); + sqlAppender.appendSql( "(d,i) on true" ); + nextClauseLevel = renderNestedColumnJoins( sqlAppender, nestedColumnDefinition.columns(), clauseLevel + 1, walker ); + } + } + return nextClauseLevel; + } + + @Override + protected void renderJsonExistsColumnDefinition(SqlAppender sqlAppender, JsonTableExistsColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // jsonb_path_exists errors by default + if ( definition.errorBehavior() != null && definition.errorBehavior() != JsonExistsErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on CockroachDB" ); + } + final String jsonPath = definition.jsonPath() == null + ? "$." + definition.name() + : definition.jsonPath(); + CockroachDBJsonExistsFunction.appendJsonExists( + sqlAppender, + new ClauseLevelDocumentExpression( clauseLevel ), + JsonPathHelper.parseJsonPathElements( jsonPath ), + true, + null, + walker + ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( definition.name() ); + } + + @Override + protected void renderJsonQueryColumnDefinition(SqlAppender sqlAppender, JsonTableQueryColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // jsonb_path_query functions error by default + if ( definition.errorBehavior() != null && definition.errorBehavior() != JsonQueryErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on CockroachDB" ); + } + if ( definition.emptyBehavior() != null && definition.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on CockroachDB" ); + } + final JsonQueryWrapMode wrapMode = definition.wrapMode(); + + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "jsonb_build_array(" ); + } + final String jsonPath = definition.jsonPath() == null + ? "$." + definition.name() + : definition.jsonPath(); + CockroachDBJsonQueryFunction.appendJsonQuery( + sqlAppender, + new ClauseLevelDocumentExpression( clauseLevel ), + JsonPathHelper.parseJsonPathElements( jsonPath ), + true, + null, + walker + ); + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( ")" ); + } + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( definition.name() ); + } + + @Override + protected void renderJsonValueColumnDefinition(SqlAppender sqlAppender, JsonTableValueColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // jsonb_path_query_first errors by default + if ( definition.errorBehavior() != null && definition.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on CockroachDB" ); + } + if ( definition.emptyBehavior() != null && definition.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on CockroachDB" ); + } + final String jsonPath = definition.jsonPath() == null + ? "$." + definition.name() + : definition.jsonPath(); + CockroachDBJsonValueFunction.appendJsonValue( + sqlAppender, + new ClauseLevelDocumentExpression( clauseLevel ), + JsonPathHelper.parseJsonPathElements( jsonPath ), + true, + null, + definition.type(), + walker + ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( definition.name() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java index 4851752a370b..d5736db070fb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java @@ -11,6 +11,7 @@ import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; @@ -47,18 +48,29 @@ protected void render( catch (Exception ex) { throw new QueryException( "CockroachDB json_value only support literal json paths, but got " + arguments.jsonPath() ); } - final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); - if ( arguments.returningType() != null ) { + appendJsonValue( + sqlAppender, + arguments.jsonDocument(), + JsonPathHelper.parseJsonPathElements( jsonPath ), + arguments.isJsonType(), + arguments.passingClause(), + arguments.returningType(), + walker + ); + } + + static void appendJsonValue(SqlAppender sqlAppender, Expression jsonDocument, List jsonPathElements, boolean isJsonType, JsonPathPassingClause jsonPathPassingClause, CastTarget castTarget, SqlAstTranslator walker) { + if ( castTarget != null ) { sqlAppender.appendSql( "cast(" ); } - final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + final boolean needsCast = !isJsonType && jsonDocument instanceof JdbcParameter; if ( needsCast ) { sqlAppender.appendSql( "cast(" ); } else { sqlAppender.appendSql( '(' ); } - arguments.jsonDocument().accept( walker ); + jsonDocument.accept( walker ); if ( needsCast ) { sqlAppender.appendSql( " as jsonb)" ); } @@ -74,12 +86,11 @@ protected void render( dialect.appendLiteral( sqlAppender, attribute.attribute() ); } else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { - final JsonPathPassingClause jsonPathPassingClause = arguments.passingClause(); assert jsonPathPassingClause != null; final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) jsonPathElement ).parameterName(); final Expression expression = jsonPathPassingClause.getPassingExpressions().get( parameterName ); if ( expression == null ) { - throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + throw new QueryException( "JSON path [" + JsonPathHelper.toJsonPath( jsonPathElements ) + "] uses parameter [" + parameterName + "] that is not passed" ); } sqlAppender.appendSql( "cast(" ); @@ -93,11 +104,14 @@ else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { } separator = ','; } + if ( jsonPathElements.isEmpty() ) { + sqlAppender.appendSql( '[' ); + } sqlAppender.appendSql( ']' ); - if ( arguments.returningType() != null ) { + if ( castTarget != null ) { sqlAppender.appendSql( " as " ); - arguments.returningType().accept( walker ); + castTarget.accept( walker ); sqlAppender.appendSql( ')' ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonTableFunction.java new file mode 100644 index 000000000000..0135028cdc20 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonTableFunction.java @@ -0,0 +1,438 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.QueryException; +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.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableOrdinalityColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 json_table function. + * This implementation/emulation goes to great lengths to ensure Hibernate ORM can provide the same {@code json_table()} + * experience that other dialects provide also on DB2. + * The most notable limitation of the DB2 function is that it doesn't support JSON arrays, + * so this emulation uses a series CTE called {@code gen_} with 10_000 rows to join + * each array element queried with {@code json_query()} at the respective index via {@code json_table()} separately. + * Another notable limitation of the DB2 function is that it doesn't support nested column paths, + * which requires emulation by joining each nesting with a separate {@code json_table()}. + */ +public class DB2JsonTableFunction extends JsonTableFunction { + + public DB2JsonTableFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() == JsonTableErrorBehavior.NULL ) { + throw new QueryException( "Can't emulate null on error clause on DB2" ); + } + final Expression jsonDocument = arguments.jsonDocument(); + final Expression jsonPath = arguments.jsonPath(); + final boolean isArray = isArrayAccess( jsonPath, walker ); + sqlAppender.appendSql( "lateral(" ); + + if ( isArray || hasNestedArray( arguments.columnsClause() ) ) { + // DB2 doesn't support arrays in json_table(), so a series table to join individual elements is needed + sqlAppender.appendSql( "with gen_(v) as(select 0 from (values (0)) union all " ); + sqlAppender.appendSql( "select i.v+1 from gen_ i where i.v<10000)" ); + } + + sqlAppender.appendSql( "select" ); + renderColumnSelects( sqlAppender, arguments.columnsClause(), 0, isArray ); + + if ( isArray ) { + sqlAppender.appendSql( " from gen_ i join " ); + } + else { + sqlAppender.appendSql( " from " ); + } + sqlAppender.appendSql( "json_table(" ); + // DB2 json functions only work when passing object documents, + // which is why an array element query result is packed in shell object `{"a":...}` + if ( isArray ) { + sqlAppender.appendSql( "'{\"a\":'||" ); + } + appendJsonDocument( sqlAppender, jsonPath, jsonDocument, arguments.passingClause(), isArray, walker ); + if ( isArray ) { + sqlAppender.appendSql( "||'}'" ); + } + sqlAppender.appendSql( ",'strict $'" ); + renderColumns( sqlAppender, arguments.columnsClause(), 0, isArray ? "$.a" : null, walker ); + sqlAppender.appendSql( " error on error) t0" ); + if ( isArray ) { + sqlAppender.appendSql( " on json_exists('{\"a\":'||" ); + appendJsonDocument( sqlAppender, jsonPath, jsonDocument, arguments.passingClause(), isArray, walker ); + sqlAppender.appendSql( "||'}','$.a['||i.v||']')" ); + } + renderNestedColumnJoins( sqlAppender, arguments.columnsClause(), 0, walker ); + sqlAppender.appendSql( ')' ); + } + + private static void appendJsonDocument(SqlAppender sqlAppender, Expression jsonPath, Expression jsonDocument, JsonPathPassingClause passingClause, boolean isArray, SqlAstTranslator walker) { + if ( jsonPath != null ) { + sqlAppender.appendSql( "json_query(" ); + jsonDocument.accept( walker ); + sqlAppender.appendSql( ',' ); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + jsonPath, + passingClause, + walker + ); + } + else { + jsonPath.accept( walker ); + } + if ( isArray ) { + sqlAppender.appendSql( " with wrapper" ); + } + sqlAppender.appendSql( ')' ); + } + else { + jsonDocument.accept( walker ); + } + } + + private boolean isArrayAccess(@Nullable Expression jsonPath, SqlAstTranslator walker) { + if ( jsonPath != null ) { + try { + return isArrayAccess( walker.getLiteralValue( jsonPath ) ); + } + catch (Exception ex) { + // Ignore + } + } + // Assume array by default + return true; + } + + + private boolean isArrayAccess(String jsonPath) { + return jsonPath.endsWith( "[*]" ); + } + + private boolean hasNestedArray(JsonTableColumnsClause jsonTableColumnsClause) { + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + if ( isArrayAccess( nestedColumnDefinition.jsonPath() ) + || hasNestedArray( nestedColumnDefinition.columns() ) ) { + return true; + } + } + } + return false; + } + + private int renderNestedColumnJoins(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + int currentClauseLevel = clauseLevel; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + // DB2 doesn't support the nested path syntax, so emulate it by lateral joining json_table() + final int nextClauseLevel = currentClauseLevel + 1; + final boolean isArray = isArrayAccess( nestedColumnDefinition.jsonPath() ); + + sqlAppender.appendSql( " left join lateral (select" ); + renderColumnSelects( sqlAppender, nestedColumnDefinition.columns(), nextClauseLevel, isArray ); + sqlAppender.appendSql( " from" ); + + if ( isArray ) { + // When the JSON path indicates that the document is an array, + // join the `gen_` CTE to be able to use the respective array element in json_table(). + // DB2 json functions only work when passing object documents, + // which is why results are packed in shell object `{"a":...}` + sqlAppender.appendSql( " gen_ i join json_table('{\"a\":'||json_query('{\"a\":'||t" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( ".nested_" ); + sqlAppender.appendSql( nextClauseLevel ); + sqlAppender.appendSql( "_||'}','$.a['||i.v||']')||'}','strict $'" ); + // Since the query results are packed in a shell object `{"a":...}`, + // the JSON path for columns need to be prefixed with `$.a` + renderColumns( sqlAppender, nestedColumnDefinition.columns(), nextClauseLevel, "$.a", walker ); + sqlAppender.appendSql( " error on error) t" ); + sqlAppender.appendSql( nextClauseLevel ); + // Emulation of arrays via `gen_` sequence requires a join condition to check if an array element exists + sqlAppender.appendSql( " on json_exists('{\"a\":'||t" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( ".nested_" ); + sqlAppender.appendSql( nextClauseLevel ); + sqlAppender.appendSql( "_||'}','$.a['||i.v||']')" ); + } + else { + sqlAppender.appendSql( " json_table(t" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( ".nested_" ); + sqlAppender.appendSql( nextClauseLevel ); + sqlAppender.appendSql( "_,'strict $'" ); + renderColumns( sqlAppender, nestedColumnDefinition.columns(), nextClauseLevel, null, walker ); + sqlAppender.appendSql( " error on error) t" ); + sqlAppender.appendSql( nextClauseLevel ); + } + sqlAppender.appendSql( ") t" ); + sqlAppender.appendSql( nextClauseLevel ); + sqlAppender.appendSql( " on 1=1" ); + currentClauseLevel = renderNestedColumnJoins( sqlAppender, nestedColumnDefinition.columns(), nextClauseLevel, walker ); + } + } + return currentClauseLevel; + } + + private void renderColumnSelects(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, boolean isArray) { + int currentClauseLevel = clauseLevel; + char separator = ' '; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + sqlAppender.appendSql( separator ); + if ( columnDefinition instanceof JsonTableExistsColumnDefinition existsColumnDefinition ) { + // DB2 doesn't support the exists syntax in json_table(), + // so emulate it by selecting the json_exists() result + sqlAppender.appendSql( "json_exists(t" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( "." ); + sqlAppender.appendSql( existsColumnDefinition.name() ); + sqlAppender.appendSql( ',' ); + final String jsonPath = existsColumnDefinition.jsonPath() == null + ? "$." + existsColumnDefinition.name() + : existsColumnDefinition.jsonPath(); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + final JsonExistsErrorBehavior errorBehavior = existsColumnDefinition.errorBehavior(); + if ( errorBehavior != null && errorBehavior != JsonExistsErrorBehavior.FALSE ) { + if ( errorBehavior == JsonExistsErrorBehavior.TRUE ) { + sqlAppender.appendSql( " true on error" ); + } + else { + sqlAppender.appendSql( " error on error" ); + } + } + sqlAppender.appendSql( ") " ); + sqlAppender.appendSql( existsColumnDefinition.name() ); + } + else if ( columnDefinition instanceof JsonTableOrdinalityColumnDefinition ordinalityColumnDefinition) { + // DB2 doesn't support the for ordinality syntax in json_table() since it has no support for array either + if ( isArray ) { + // If the document is an array, a series table with alias `i` is joined to emulate array support. + // Since the value of the series is 0 based, we add 1 to obtain the ordinality value + sqlAppender.appendSql( "i.v+1 " ); + } + else { + // The ordinality for non-array documents always is trivially 1 + sqlAppender.appendSql( "1 " ); + } + sqlAppender.appendSql( ordinalityColumnDefinition.name() ); + } + else if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + // A join is created in #renderNestedColumnJoins under the alias `t` + // which holds all nested columns, so just select that directly + sqlAppender.appendSql( 't' ); + sqlAppender.appendSql( currentClauseLevel + 1 ); + sqlAppender.appendSql( ".*" ); + currentClauseLevel += 1 + countNestedColumnDefinitions( nestedColumnDefinition.columns() ); + } + else if ( columnDefinition instanceof JsonTableValueColumnDefinition valueColumnDefinition ) { + // Just pass-through value columns in the select clause + sqlAppender.appendSql( 't' ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( valueColumnDefinition.name() ); + } + else { + // Just pass-through query columns in the select clause + final JsonTableQueryColumnDefinition queryColumnDefinition = (JsonTableQueryColumnDefinition) columnDefinition; + sqlAppender.appendSql( 't' ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( queryColumnDefinition.name() ); + } + separator = ','; + } + } + + private int renderColumns(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, @Nullable String jsonPathPrefix, SqlAstTranslator walker) { + sqlAppender.appendSql( " columns" ); + int nextClauseLevel = clauseLevel + 1; + char separator = '('; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + sqlAppender.appendSql( separator ); + if ( columnDefinition instanceof JsonTableExistsColumnDefinition definition ) { + renderJsonExistsColumnDefinition( sqlAppender, definition ); + } + else if ( columnDefinition instanceof JsonTableQueryColumnDefinition definition ) { + renderJsonQueryColumnDefinition( sqlAppender, definition, jsonPathPrefix, walker ); + } + else if ( columnDefinition instanceof JsonTableValueColumnDefinition definition ) { + renderJsonValueColumnDefinition( sqlAppender, definition, jsonPathPrefix, walker ); + } + else if ( columnDefinition instanceof JsonTableOrdinalityColumnDefinition definition ) { + renderJsonOrdinalityColumnDefinition( sqlAppender, definition ); + } + else { + nextClauseLevel = renderJsonNestedColumnDefinition( sqlAppender, (JsonTableNestedColumnDefinition) columnDefinition, nextClauseLevel ); + } + separator = ','; + } + sqlAppender.appendSql( ')' ); + return nextClauseLevel; + } + + private void renderColumnPath(String name, @Nullable String jsonPath, @Nullable String jsonPathPrefix, SqlAppender sqlAppender, SqlAstTranslator walker) { + if ( jsonPath != null ) { + super.renderColumnPath( + name, + jsonPathPrefix != null + ? jsonPathPrefix + jsonPath.substring( 1 ) + : jsonPath, + sqlAppender, + walker + ); + } + else { + // We can either double quote the column name to make it case-sensitive, or use an explicit JSON path. + // Using an explicit JSON path is easier though since we don't know where the column is going to be used + sqlAppender.appendSql( " path '" ); + if ( jsonPathPrefix == null ) { + sqlAppender.appendSql( '$' ); + } + else { + sqlAppender.appendSql( jsonPathPrefix ); + } + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( name ); + sqlAppender.appendSql( '\'' ); + } + } + + @Override + protected String determineColumnType(CastTarget castTarget, SqlAstTranslator walker) { + final String columnType = super.determineColumnType( castTarget, walker ); + switch ( columnType ) { + // Boolean not supported in json_table() + case "boolean": + return "smallint"; + } + return columnType; + } + + private void renderJsonQueryColumnDefinition(SqlAppender sqlAppender, JsonTableQueryColumnDefinition definition, @Nullable String jsonPathPrefix, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( determineColumnType( new CastTarget( definition.type() ), walker ) ); + if ( definition.type().getJdbcType().getDdlTypeCode() != SqlTypes.JSON ) { + sqlAppender.appendSql( " format json" ); + } + + if ( definition.wrapMode() != null ) { + switch ( definition.wrapMode() ) { + case WITH_WRAPPER -> sqlAppender.appendSql( " with wrapper" ); + case WITHOUT_WRAPPER -> sqlAppender.appendSql( " without wrapper" ); + case WITH_CONDITIONAL_WRAPPER -> sqlAppender.appendSql( " with conditional wrapper" ); + } + } + + // Custom implementation of query rendering to pass through our path prefix + renderColumnPath( definition.name(), definition.jsonPath(), jsonPathPrefix, sqlAppender, walker ); + + if ( definition.errorBehavior() != null ) { + switch ( definition.errorBehavior() ) { + case ERROR -> sqlAppender.appendSql( " error on error" ); + case NULL -> sqlAppender.appendSql( " null on error" ); + case EMPTY_OBJECT -> sqlAppender.appendSql( " empty object on error" ); + case EMPTY_ARRAY -> sqlAppender.appendSql( " empty array on error" ); + } + } + + if ( definition.emptyBehavior() != null ) { + switch ( definition.emptyBehavior() ) { + case ERROR -> sqlAppender.appendSql( " error on empty" ); + case NULL -> sqlAppender.appendSql( " null on empty" ); + case EMPTY_OBJECT -> sqlAppender.appendSql( " empty object on empty" ); + case EMPTY_ARRAY -> sqlAppender.appendSql( " empty array on empty" ); + } + } + } + + private void renderJsonValueColumnDefinition(SqlAppender sqlAppender, JsonTableValueColumnDefinition definition, @Nullable String jsonPathPrefix, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( determineColumnType( definition.type(), walker ) ); + + // Custom implementation of value rendering to pass through our path prefix + renderColumnPath( definition.name(), definition.jsonPath(), jsonPathPrefix, sqlAppender, walker ); + + if ( definition.errorBehavior() != null ) { + if ( definition.errorBehavior() == JsonValueErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + else if ( definition.errorBehavior() != JsonValueErrorBehavior.NULL ) { + final Expression defaultExpression = definition.errorBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on error" ); + } + } + if ( definition.emptyBehavior() != null ) { + if ( definition.emptyBehavior() == JsonValueEmptyBehavior.ERROR ) { + sqlAppender.appendSql( " error on empty" ); + } + else if ( definition.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + final Expression defaultExpression = definition.emptyBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on empty" ); + } + } + } + + private void renderJsonOrdinalityColumnDefinition(SqlAppender sqlAppender, JsonTableOrdinalityColumnDefinition definition) { + // DB2 doesn't support for ordinality syntax since it also doesn't support arrays + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( " clob format json path '$'" ); + } + + private int renderJsonNestedColumnDefinition(SqlAppender sqlAppender, JsonTableNestedColumnDefinition definition, int clauseLevel) { + // DB2 doesn't support nested path syntax, so just select the nested path as json clob and join that later + sqlAppender.appendSql( "nested_" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( "_ clob format json path " ); + // Strip off array element access from JSON path to select the array as a whole for later processing + final String jsonPath = isArrayAccess( definition.jsonPath() ) + ? definition.jsonPath().substring( 0, definition.jsonPath().length() - 3 ) + : definition.jsonPath(); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + return clauseLevel + countNestedColumnDefinitions( definition.columns() ); + } + + private void renderJsonExistsColumnDefinition(SqlAppender sqlAppender, JsonTableExistsColumnDefinition definition) { + // DB2 doesn't support exists syntax, so select the whole document against which an exists check + // is made through the json_exists() function in the select clause + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( " clob format json path '$'" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonValueFunction.java new file mode 100644 index 000000000000..249d7eac9ebc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonValueFunction.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 json_value function. + */ +public class DB2JsonValueFunction extends JsonValueFunction { + + public DB2JsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final boolean encodedBoolean = arguments.returningType() != null + && isEncodedBoolean( arguments.returningType().getJdbcMapping() ); + if ( encodedBoolean ) { + sqlAppender.append( "decode(" ); + } + super.render( sqlAppender, arguments, returnType, walker ); + if ( encodedBoolean ) { + final JdbcMapping type = arguments.returningType().getJdbcMapping(); + //noinspection unchecked + final JdbcLiteralFormatter jdbcLiteralFormatter = type.getJdbcLiteralFormatter(); + final SessionFactoryImplementor sessionFactory = walker.getSessionFactory(); + final Dialect dialect = sessionFactory.getJdbcServices().getDialect(); + final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions(); + final Object trueValue = type.convertToRelationalValue( true ); + final Object falseValue = type.convertToRelationalValue( false ); + sqlAppender.append( ",'true'," ); + jdbcLiteralFormatter.appendJdbcLiteral( sqlAppender, trueValue, dialect, wrapperOptions ); + sqlAppender.append( ",'false'," ); + jdbcLiteralFormatter.appendJdbcLiteral( sqlAppender, falseValue, dialect, wrapperOptions ); + sqlAppender.append( ')' ); + } + } + + @Override + protected void renderReturningClause(SqlAppender sqlAppender, JsonValueArguments arguments, SqlAstTranslator walker) { + // No return type for booleans, this is handled via decode + if ( arguments.returningType() != null && !isEncodedBoolean( arguments.returningType().getJdbcMapping() ) ) { + super.renderReturningClause( sqlAppender, arguments, walker ); + } + } + + static boolean isEncodedBoolean(JdbcMapping type) { + return type.getJdbcType().isBoolean(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java index 6550802e0be1..298710b30c87 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java @@ -4,10 +4,13 @@ */ package org.hibernate.dialect.function.json; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.QueryException; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior; import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior; import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; @@ -35,14 +38,30 @@ protected void render( if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR ) { throw new QueryException( "Can't emulate error on empty clause on H2" ); } + appendJsonQuery( + sqlAppender, + arguments.jsonDocument(), + arguments.isJsonType(), + arguments.jsonPath(), + arguments.passingClause(), + arguments.wrapMode(), + walker + ); + } + + static void appendJsonQuery(SqlAppender sqlAppender, Expression jsonDocument, boolean isJsonType, Expression jsonPathExpression, @Nullable JsonPathPassingClause passingClause, @Nullable JsonQueryWrapMode wrapMode, SqlAstTranslator walker) { final String jsonPath; try { - jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + jsonPath = walker.getLiteralValue( jsonPathExpression ); } catch (Exception ex) { - throw new QueryException( "H2 json_query only support literal json paths, but got " + arguments.jsonPath() ); + throw new QueryException( "H2 json_query only support literal json paths, but got " + jsonPathExpression ); } - if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + appendJsonQuery( sqlAppender, jsonDocument, isJsonType, jsonPath, passingClause, wrapMode, walker ); + } + + static void appendJsonQuery(SqlAppender sqlAppender, Expression jsonDocument, boolean isJsonType, String jsonPath, @Nullable JsonPathPassingClause passingClause, @Nullable JsonQueryWrapMode wrapMode, SqlAstTranslator walker) { + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sqlAppender.appendSql( "'['||" ); } @@ -50,15 +69,15 @@ protected void render( sqlAppender.appendSql( "cast(" ); H2JsonValueFunction.renderJsonPath( sqlAppender, - arguments.jsonDocument(), - arguments.isJsonType(), + jsonDocument, + isJsonType, walker, jsonPath, - arguments.passingClause() + passingClause ); sqlAppender.appendSql( " as varchar)" ); sqlAppender.appendSql( ",'null'),'\"'))"); - if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sqlAppender.appendSql( "||']'" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java new file mode 100644 index 000000000000..c7b2ff415bc0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonTableFunction.java @@ -0,0 +1,761 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.QueryException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.NullnessUtil; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SelectablePath; +import org.hibernate.metamodel.mapping.internal.SelectableMappingImpl; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.expression.SqmJsonTableFunction; +import org.hibernate.spi.NavigablePath; +import org.hibernate.sql.ast.SqlAstJoinType; +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.cte.CteContainer; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableOrdinalityColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.QueryTransformer; +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.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * H2 json_table 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 H2JsonTableFunction extends JsonTableFunction { + + private final int maximumArraySize; + + public H2JsonTableFunction(int maximumArraySize, TypeConfiguration typeConfiguration) { + super( new H2JsonTableSetReturningFunctionTypeResolver(), typeConfiguration ); + this.maximumArraySize = maximumArraySize; + } + + @Override + protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression( + List> sqmArguments, + QueryEngine queryEngine) { + //noinspection unchecked + return new SqmJsonTableFunction<>( + this, + this, + getArgumentsValidator(), + getSetReturningTypeResolver(), + queryEngine.getCriteriaBuilder(), + (SqmExpression) sqmArguments.get( 0 ), + sqmArguments.size() > 1 ? (SqmExpression) sqmArguments.get( 1 ) : null + ) { + @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 + ); + final JsonTableArguments arguments = JsonTableArguments.extract( + functionTableGroup.getPrimaryTableReference().getFunctionExpression().getArguments() + ); + // Register a query transformer to register a join predicate + walker.registerQueryTransformer( + new JsonTableQueryTransformer( functionTableGroup, arguments, maximumArraySize ) ); + return functionTableGroup; + } + }; + } + + private static class JsonTableQueryTransformer implements QueryTransformer { + private final FunctionTableGroup functionTableGroup; + private final JsonTableArguments arguments; + private final int maximumArraySize; + + public JsonTableQueryTransformer(FunctionTableGroup functionTableGroup, JsonTableArguments arguments, int maximumArraySize) { + this.functionTableGroup = functionTableGroup; + this.arguments = arguments; + this.maximumArraySize = maximumArraySize; + } + + @Override + public QuerySpec transform(CteContainer cteContainer, QuerySpec querySpec, SqmToSqlAstConverter converter) { + final boolean isArray; + if ( arguments.jsonPath() != null ) { + if ( !( arguments.jsonPath() instanceof Literal literal) ) { + throw new QueryException( "H2 json_table() only supports literal json paths, but got " + arguments.jsonPath() ); + } + final String rawJsonPath = (String) literal.getLiteralValue(); + isArray = isArrayAccess( rawJsonPath ); + } + else { + // We have to assume this is an array + isArray = true; + } + if ( isArray ) { + final TableGroup parentTableGroup = querySpec.getFromClause().queryTableGroups( + tg -> tg.findTableGroupJoin( functionTableGroup ) == null ? null : tg + ); + final TableGroupJoin join = parentTableGroup.findTableGroupJoin( functionTableGroup ); + final BasicType integerType = converter.getCreationContext() + .getSessionFactory() + .getNodeBuilder() + .getIntegerType(); + final Expression lhs = new ArrayLengthExpression( arguments.jsonDocument(), 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 ) ); + } + final int lastArrayIndex = getLastArrayIndex( arguments.columnsClause(), 0 ); + if ( lastArrayIndex != 0 ) { + // Create a synthetic function table group which will render system_range() joins + // for every nested path for arrays + final String tableIdentifierVariable = functionTableGroup.getPrimaryTableReference() + .getIdentificationVariable(); + final TableGroup tableGroup = new FunctionTableGroup( + functionTableGroup.getNavigablePath().append( "{synthetic}" ), + null, + new SelfRenderingFunctionSqlAstExpression( + "json_table_emulation", + new NestedPathFunctionRenderer( + tableIdentifierVariable, + arguments, + maximumArraySize, + lastArrayIndex + ), + Collections.emptyList(), + null, + null + ), + tableIdentifierVariable + "_synthetic_", + Collections.emptyList(), + Set.of( "" ), + false, + false, + true, + converter.getCreationContext().getSessionFactory() + ); + final BasicType integerType = converter.getCreationContext() + .getSessionFactory() + .getNodeBuilder() + .getIntegerType(); + + // The join predicate compares the length of the last array expression against system_range() index. + // Since a table function expression can't render its own `on` clause, this split of logic is necessary + final Expression lhs = new ArrayLengthExpression( + determineLastArrayExpression( tableIdentifierVariable, arguments ), + integerType + ); + final Expression rhs = new ColumnReference( + tableIdentifierVariable + "_" + lastArrayIndex + "_", + // The default column name for the system_range function + "x", + false, + null, + integerType + ); + final Predicate predicate = new ComparisonPredicate( lhs, ComparisonOperator.GREATER_THAN_OR_EQUAL, rhs ); + functionTableGroup.addTableGroupJoin( + new TableGroupJoin( tableGroup.getNavigablePath(), SqlAstJoinType.LEFT, tableGroup, predicate ) + ); + } + return querySpec; + } + + private static Expression determineLastArrayExpression(String tableIdentifierVariable, JsonTableArguments arguments) { + final ArrayExpressionEntry arrayExpressionEntry = determineLastArrayExpression( + tableIdentifierVariable, + determineJsonElement( tableIdentifierVariable, arguments ), + arguments.columnsClause(), + new ArrayExpressionEntry( 0, null ) + ); + return NullnessUtil.castNonNull( arrayExpressionEntry.expression() ); + } + + record ArrayExpressionEntry(int arrayIndex, @Nullable Expression expression) { + } + + private static ArrayExpressionEntry determineLastArrayExpression(String tableIdentifierVariable, Expression parentJson, JsonTableColumnsClause jsonTableColumnsClause, ArrayExpressionEntry parentEntry) { + // Depth-first traversal to obtain the last nested path that refers to an array within this tree + ArrayExpressionEntry currentArrayEntry = parentEntry; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + final String rawJsonPath = nestedColumnDefinition.jsonPath(); + final boolean isArray = isArrayAccess( rawJsonPath ); + final String jsonPath = isArray ? rawJsonPath.substring( 0, rawJsonPath.length() - 3 ) : rawJsonPath; + + final Expression jsonQueryResult = new JsonValueExpression( parentJson, jsonPath, null ); + final Expression jsonElement; + final ArrayExpressionEntry nextArrayExpression; + if ( isArray ) { + final int nextArrayIndex = currentArrayEntry.arrayIndex() + 1; + jsonElement = new ArrayAccessExpression( jsonQueryResult, tableIdentifierVariable + "_" + nextArrayIndex + "_.x" ); + nextArrayExpression = new ArrayExpressionEntry( nextArrayIndex, jsonQueryResult ); + } + else { + jsonElement = jsonQueryResult; + nextArrayExpression = currentArrayEntry; + } + currentArrayEntry = determineLastArrayExpression( + tableIdentifierVariable, + jsonElement, + nestedColumnDefinition.columns(), + nextArrayExpression + ); + } + } + return currentArrayEntry; + } + + private static Expression determineJsonElement(String tableIdentifierVariable, JsonTableArguments arguments) { + // Applies the json path and array index access to obtain the "current" processing element + + final Expression jsonDocument = arguments.jsonDocument(); + final boolean isArray; + final Expression jsonQueryResult; + if ( arguments.jsonPath() != null ) { + if ( !(arguments.jsonPath() instanceof Literal literal) ) { + throw new QueryException( + "H2 json_table() only supports literal json paths, but got " + arguments.jsonPath() ); + } + final String rawJsonPath = (String) literal.getLiteralValue(); + isArray = isArrayAccess( rawJsonPath ); + final String jsonPath = isArray ? rawJsonPath.substring( 0, rawJsonPath.length() - 3 ) : rawJsonPath; + + jsonQueryResult = "$".equals( jsonPath ) + ? jsonDocument + : new JsonValueExpression( jsonDocument, arguments.isJsonType(), jsonPath, arguments.passingClause() ); + } + else { + // We have to assume this is an array + isArray = true; + jsonQueryResult = jsonDocument; + } + + final Expression jsonElement; + if ( isArray ) { + jsonElement = new ArrayAccessExpression( jsonQueryResult, tableIdentifierVariable + ".x" ); + } + else { + jsonElement = jsonQueryResult; + } + return jsonElement; + } + + private static class NestedPathFunctionRenderer implements FunctionRenderer { + private final String tableIdentifierVariable; + private final JsonTableArguments arguments; + private final int maximumArraySize; + private final int lastArrayIndex; + + public NestedPathFunctionRenderer(String tableIdentifierVariable, JsonTableArguments arguments, int maximumArraySize, int lastArrayIndex) { + this.tableIdentifierVariable = tableIdentifierVariable; + this.arguments = arguments; + this.maximumArraySize = maximumArraySize; + this.lastArrayIndex = lastArrayIndex; + } + + @Override + public void render(SqlAppender sqlAppender, List sqlAstArguments, ReturnableType returnType, SqlAstTranslator walker) { + final Expression jsonElement = determineJsonElement( tableIdentifierVariable, arguments ); + renderNestedColumnJoins( sqlAppender, tableIdentifierVariable, jsonElement, arguments.columnsClause(), 0, lastArrayIndex, walker ); + } + + private int renderNestedColumnJoins(SqlAppender sqlAppender, String tableIdentifierVariable, Expression parentJson, JsonTableColumnsClause jsonTableColumnsClause, int arrayIndex, int lastArrayIndex, SqlAstTranslator walker) { + // H2 doesn't support lateral joins, so we have to emulate array flattening by joining against a + // system_range() with a condition that checks if the array index is still within bounds + int currentArrayIndex = arrayIndex; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + final String rawJsonPath = nestedColumnDefinition.jsonPath(); + final boolean isArray = isArrayAccess( rawJsonPath ); + final String jsonPath = isArray ? rawJsonPath.substring( 0, rawJsonPath.length() - 3 ) : rawJsonPath; + final int nextArrayIndex = currentArrayIndex + ( isArray ? 1 : 0 ); + + // The left join for the first element was already rendered via TableGroupJoin + if ( isArray && currentArrayIndex != 0 ) { + sqlAppender.appendSql( " left join " ); + } + final Expression jsonQueryResult = new JsonValueExpression( parentJson, jsonPath, null ); + final Expression jsonElement; + if ( isArray ) { + // Only render system ranges for arrays + sqlAppender.append( "system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") " ); + sqlAppender.appendSql( tableIdentifierVariable ); + sqlAppender.appendSql( '_' ); + sqlAppender.appendSql( nextArrayIndex ); + sqlAppender.appendSql( '_' ); + + // The join condition for the last array will be rendered via TableGroupJoin + if ( nextArrayIndex != lastArrayIndex ) { + sqlAppender.appendSql( " on coalesce(array_length(" ); + jsonQueryResult.accept( walker ); + sqlAppender.append( "),0)>=" ); + sqlAppender.appendSql( tableIdentifierVariable ); + sqlAppender.appendSql( '_' ); + sqlAppender.appendSql( nextArrayIndex ); + sqlAppender.appendSql( "_.x" ); + } + jsonElement = new ArrayAccessExpression( jsonQueryResult, tableIdentifierVariable + "_" + nextArrayIndex + "_.x" ); + } + else { + jsonElement = jsonQueryResult; + } + currentArrayIndex = renderNestedColumnJoins( + sqlAppender, + tableIdentifierVariable, + jsonElement, + nestedColumnDefinition.columns(), + nextArrayIndex, + lastArrayIndex, + walker + ); + } + } + return currentArrayIndex; + } + } + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() == JsonTableErrorBehavior.NULL ) { + throw new QueryException( "Can't emulate null on error clause on H2" ); + } + + final Expression jsonPathExpression = arguments.jsonPath(); + final boolean isArray = isArrayAccess( jsonPathExpression, walker ); + + if ( isArray ) { + sqlAppender.append( "system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ")" ); + } + else { + sqlAppender.append( "system_range(1,1)" ); + } + } + + private static boolean isArrayAccess(@Nullable Expression jsonPath, SqlAstTranslator walker) { + if ( jsonPath != null ) { + try { + return isArrayAccess( walker.getLiteralValue( jsonPath ) ); + } + catch (Exception ex) { + // Ignore + } + } + // Assume array by default + return true; + } + + private static boolean isArrayAccess(String jsonPath) { + return jsonPath.endsWith( "[*]" ); + } + + private static int getLastArrayIndex(JsonTableColumnsClause jsonTableColumnsClause, int arrayIndex) { + int currentArrayIndex = arrayIndex; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + currentArrayIndex = getLastArrayIndex( + nestedColumnDefinition.columns(), + arrayIndex + (isArrayAccess( nestedColumnDefinition.jsonPath() ) ? 1 : 0 ) + ); + } + } + return currentArrayIndex; + } + + private static class JsonValueExpression implements SelfRenderingExpression { + private final Expression jsonDocument; + private final boolean isJsonType; + private final String jsonPath; + private final @Nullable JsonPathPassingClause passingClause; + + public JsonValueExpression(Expression jsonDocument, String jsonPath, @Nullable JsonPathPassingClause passingClause) { + this.jsonDocument = jsonDocument; + // This controls whether we put parenthesis around the document on dereference + this.isJsonType = jsonDocument instanceof JsonValueExpression + || jsonDocument instanceof ArrayAccessExpression; + this.jsonPath = jsonPath; + this.passingClause = passingClause; + } + + public JsonValueExpression(Expression jsonDocument, boolean isJsonType, String jsonPath, @Nullable JsonPathPassingClause passingClause) { + this.jsonDocument = jsonDocument; + this.isJsonType = isJsonType; + this.jsonPath = jsonPath; + this.passingClause = passingClause; + } + + @Override + public void renderToSql(SqlAppender sqlAppender, SqlAstTranslator walker, SessionFactoryImplementor sessionFactory) { + H2JsonValueFunction.renderJsonPath( + sqlAppender, + jsonDocument, + isJsonType, + walker, + jsonPath, + passingClause + ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return null; + } + } + + private static class ArrayAccessExpression implements SelfRenderingExpression { + private final Expression array; + private final String indexFragment; + + public ArrayAccessExpression(Expression array, String indexFragment) { + this.array = array; + this.indexFragment = indexFragment; + } + + @Override + public void renderToSql(SqlAppender sqlAppender, SqlAstTranslator walker, SessionFactoryImplementor sessionFactory) { + sqlAppender.appendSql( "array_get(" ); + array.accept( walker ); + sqlAppender.appendSql( ',' ); + sqlAppender.appendSql( indexFragment ); + sqlAppender.appendSql( ')' ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return null; + } + } + + private static class ArrayLengthExpression implements SelfRenderingExpression { + private final Expression arrayExpression; + private final BasicType integerType; + + public ArrayLengthExpression(Expression arrayExpression, BasicType integerType) { + this.arrayExpression = arrayExpression; + this.integerType = integerType; + } + + @Override + public void renderToSql( + SqlAppender sqlAppender, + SqlAstTranslator walker, + SessionFactoryImplementor sessionFactory) { + sqlAppender.append( "coalesce(array_length(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)" ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return integerType; + } + } + + /** + * This type resolver essentially implements all the JSON path handling and casting via column read expressions + * instead of rendering to the {@code from} clause like other {@code json_table()} implementations. + * This is necessary because H2 does not support lateral joins. + * The rendering is tightly coupled to the {@code system_range()} joins that are rendered for nested paths + * that refer to arrays. + */ + private static class H2JsonTableSetReturningFunctionTypeResolver extends JsonTableSetReturningFunctionTypeResolver { + public H2JsonTableSetReturningFunctionTypeResolver() { + } + + @Override + public SelectableMapping[] resolveFunctionReturnType( + List sqlAstNodes, + String tableIdentifierVariable, + boolean lateral, + boolean withOrdinality, + SqmToSqlAstConverter converter) { + final JsonTableArguments arguments = JsonTableArguments.extract( sqlAstNodes ); + final ColumnReference columnReference = arguments.jsonDocument().getColumnReference(); + assert columnReference != null; + + final String documentPath = columnReference.getExpressionText(); + + final String parentPath; + final boolean isArray; + if ( arguments.jsonPath() != null ) { + if ( !( arguments.jsonPath() instanceof Literal literal) ) { + throw new QueryException( "H2 json_table() only supports literal json paths, but got " + arguments.jsonPath() ); + } + final String rawJsonPath = (String) literal.getLiteralValue(); + isArray = isArrayAccess( rawJsonPath ); + final String jsonPath = isArray ? rawJsonPath.substring( 0, rawJsonPath.length() - 3 ) : rawJsonPath; + parentPath = H2JsonValueFunction.applyJsonPath( documentPath, true, arguments.isJsonType(), jsonPath, arguments.passingClause() ); + } + else { + // We have to assume this is an array + isArray = true; + parentPath = documentPath; + } + + final String parentReadExpression; + if ( isArray ) { + parentReadExpression = "array_get(" + parentPath + "," + tableIdentifierVariable + ".x)"; + } + else { + parentReadExpression = '(' + parentPath + ')'; + } + final List columnDefinitions = arguments.columnsClause().getColumnDefinitions(); + final List selectableMappings = new ArrayList<>( columnDefinitions.size() ); + addSelectableMappings( selectableMappings, tableIdentifierVariable, arguments.columnsClause(), 0, parentReadExpression, converter ); + return selectableMappings.toArray( new SelectableMapping[0] ); + } + + protected int addSelectableMappings(List selectableMappings, String tableIdentifierVariable, JsonTableColumnsClause columnsClause, int clauseLevel, String parentReadExpression, SqmToSqlAstConverter converter) { + int currentClauseLevel = clauseLevel; + for ( JsonTableColumnDefinition columnDefinition : columnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableExistsColumnDefinition definition ) { + addSelectableMappings( selectableMappings, definition, parentReadExpression, converter ); + } + else if ( columnDefinition instanceof JsonTableQueryColumnDefinition definition ) { + addSelectableMappings( selectableMappings, definition, parentReadExpression, converter ); + } + else if ( columnDefinition instanceof JsonTableValueColumnDefinition definition ) { + addSelectableMappings( selectableMappings, definition, parentReadExpression, converter ); + } + else if ( columnDefinition instanceof JsonTableOrdinalityColumnDefinition definition ) { + addSelectableMappings( selectableMappings, tableIdentifierVariable, definition, clauseLevel, converter ); + } + else { + final JsonTableNestedColumnDefinition definition = (JsonTableNestedColumnDefinition) columnDefinition; + currentClauseLevel = addSelectableMappings( + selectableMappings, + tableIdentifierVariable, + definition, + currentClauseLevel, + parentReadExpression, + converter + ); + } + } + return currentClauseLevel; + } + + protected int addSelectableMappings(List selectableMappings, String tableIdentifierVariable, JsonTableNestedColumnDefinition columnDefinition, int clauseLevel, String parentReadExpression, SqmToSqlAstConverter converter) { + final String rawJsonPath = columnDefinition.jsonPath(); + final boolean isArray = isArrayAccess( rawJsonPath ); + final String jsonPath = isArray ? rawJsonPath.substring( 0, rawJsonPath.length() - 3 ) : rawJsonPath; + final String parentPath = H2JsonValueFunction.applyJsonPath( parentReadExpression, false, true, jsonPath, null ); + + final int nextClauseLevel; + final String readExpression; + if ( isArray ) { + nextClauseLevel = clauseLevel + 1; + readExpression = "array_get(" + parentPath + "," + tableIdentifierVariable + "_" + nextClauseLevel + "_.x)"; + } + else { + nextClauseLevel = clauseLevel; + readExpression = parentPath; + } + return addSelectableMappings( selectableMappings, tableIdentifierVariable, columnDefinition.columns(), nextClauseLevel, readExpression, converter ); + } + + protected void addSelectableMappings(List selectableMappings, String tableIdentifierVariable, JsonTableOrdinalityColumnDefinition definition, int clauseLevel, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + tableIdentifierVariable + "_" + clauseLevel + "_.x", + converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Long.class ) + ); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableValueColumnDefinition definition, String parentReadExpression, SqmToSqlAstConverter converter) { + final JsonValueEmptyBehavior emptyBehavior = definition.emptyBehavior(); + final Literal defaultExpression; + if ( emptyBehavior != null && emptyBehavior.getDefaultExpression() != null ) { + if ( !( emptyBehavior.getDefaultExpression() instanceof Literal literal ) ) { + throw new QueryException( "H2 json_table() only supports literal default expressions, but got " + emptyBehavior.getDefaultExpression() ); + } + defaultExpression = literal; + } + else { + defaultExpression = null; + } + final String baseReadExpression = determineElementReadExpression( definition.name(), definition.jsonPath(), parentReadExpression ); + final String elementReadExpression = castValueExpression( baseReadExpression, definition.type(), defaultExpression, converter ); + + addSelectableMapping( + selectableMappings, + definition.name(), + elementReadExpression, + definition.type().getJdbcMapping() + ); + } + + private String castValueExpression(String baseReadExpression, CastTarget castTarget, @Nullable Literal defaultExpression, SqmToSqlAstConverter converter) { + final StringBuilder sb = new StringBuilder( baseReadExpression.length() + 200 ); + sb.append( "cast(stringdecode(btrim(nullif(" ); + if ( defaultExpression != null ) { + sb.append( "coalesce(" ); + } + sb.append( "cast(" ); + sb.append( baseReadExpression ); + sb.append( " as varchar)" ); + if ( defaultExpression != null ) { + sb.append( ",cast(" ); + //noinspection unchecked + final String sqlLiteral = defaultExpression.getJdbcMapping().getJdbcLiteralFormatter().toJdbcLiteral( + defaultExpression.getLiteralValue(), + converter.getCreationContext().getSessionFactory().getJdbcServices().getDialect(), + converter.getCreationContext().getSessionFactory().getWrapperOptions() + ); + sb.append( sqlLiteral ); + sb.append( " as varchar))" ); + } + sb.append( ",'null'),'\"'))"); + + sb.append( " as " ); + sb.append( determineColumnType( castTarget, converter.getCreationContext().getTypeConfiguration() ) ); + sb.append( ')' ); + return sb.toString(); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableQueryColumnDefinition definition, String parentReadExpression, SqmToSqlAstConverter converter) { + final String baseReadExpression = determineElementReadExpression( definition.name(), definition.jsonPath(), parentReadExpression ); + final String elementReadExpression = castQueryExpression( baseReadExpression, definition.emptyBehavior(), definition.wrapMode(), converter ); + + addSelectableMapping( + selectableMappings, + definition.name(), + elementReadExpression, + converter.getCreationContext().getTypeConfiguration().getBasicTypeRegistry() + .resolve( String.class, SqlTypes.JSON ) + ); + } + + private String castQueryExpression(String baseReadExpression, JsonQueryEmptyBehavior emptyBehavior, JsonQueryWrapMode wrapMode, SqmToSqlAstConverter converter) { + final StringBuilder sb = new StringBuilder( baseReadExpression.length() + 200 ); + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sb.append( "'['||" ); + } + + sb.append( "stringdecode(btrim(nullif(" ); + sb.append( "cast(" ); + sb.append( baseReadExpression ); + sb.append( " as varchar)" ); + sb.append( ",'null'),'\"'))"); + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sb.append( "||']'" ); + } + return sb.toString(); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableExistsColumnDefinition definition, String parentReadExpression, SqmToSqlAstConverter converter) { + final String baseReadExpression = determineElementReadExpression( definition.name(), definition.jsonPath(), parentReadExpression ); + final String elementReadExpression = parentReadExpression + " is not null and " + baseReadExpression + " is not null"; + + addSelectableMapping( + selectableMappings, + definition.name(), + elementReadExpression, + converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ) + ); + } + + protected String determineElementReadExpression(String name, @Nullable String jsonPath, String parentReadExpression) { + return jsonPath == null + ? H2JsonValueFunction.applyJsonPath( parentReadExpression, false, true, "$." + name, null ) + : H2JsonValueFunction.applyJsonPath( parentReadExpression, false, true, jsonPath, null ); + } + + protected void addSelectableMapping(List selectableMappings, String name, String elementReadExpression, JdbcMapping type) { + selectableMappings.add( new SelectableMappingImpl( + "", + name, + new SelectablePath( name ), + elementReadExpression, + null, + null, + null, + null, + null, + null, + false, + false, + false, + false, + false, + false, + type + )); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java index f704fccb212a..56a7aaec617a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -7,6 +7,7 @@ import java.util.List; import org.hibernate.QueryException; +import org.hibernate.internal.util.QuotingHelper; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; @@ -14,6 +15,7 @@ import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.type.spi.TypeConfiguration; import org.checkerframework.checker.nullness.qual.Nullable; @@ -89,6 +91,10 @@ public static void renderJsonPath( SqlAstTranslator walker, String jsonPath, @Nullable JsonPathPassingClause passingClause) { + if ( "$".equals( jsonPath ) ) { + jsonDocument.accept( walker ); + return; + } final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute && jsonDocument.getColumnReference() != null @@ -129,4 +135,54 @@ else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { } } } + + static String applyJsonPath(String parentPath, boolean isColumn, boolean isJson, String jsonPath, @Nullable JsonPathPassingClause passingClause) { + if ( "$".equals( jsonPath ) ) { + return parentPath; + } + final StringBuilder sb = new StringBuilder( parentPath.length() + jsonPath.length() ); + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute + && isColumn + || !isJson; + if ( needsWrapping ) { + sb.append( '(' ); + } + sb.append( parentPath ); + if ( needsWrapping ) { + if ( !isJson ) { + sb.append( " format json" ); + } + sb.append( ')' ); + } + for ( int i = 0; i < jsonPathElements.size(); i++ ) { + final JsonPathHelper.JsonPathElement jsonPathElement = jsonPathElements.get( i ); + if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + final String attributeName = attribute.attribute(); + sb.append( "." ); + QuotingHelper.appendDoubleQuoteEscapedString( sb, attributeName ); + } + else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + assert passingClause != null; + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) jsonPathElement ).parameterName(); + final Expression expression = passingClause.getPassingExpressions().get( parameterName ); + if ( expression == null ) { + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + if ( !( expression instanceof Literal literal) ) { + throw new QueryException( "H2 json_table() passing clause only supports literal json path passing values, but got " + expression ); + } + + sb.append( '[' ); + sb.append( literal.getLiteralValue() ); + sb.append( "+1]" ); + } + else { + sb.append( '[' ); + sb.append( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() + 1 ); + sb.append( ']' ); + } + } + return sb.toString(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java new file mode 100644 index 000000000000..8bb1b3c85e73 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonTableFunction.java @@ -0,0 +1,458 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.QueryException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +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.ValuedModelPart; +import org.hibernate.metamodel.mapping.internal.EmbeddedCollectionPart; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +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.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.expression.SqmJsonTableFunction; +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.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +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.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.results.internal.SqlSelectionImpl; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * HANA json_table function. + */ +public class HANAJsonTableFunction extends JsonTableFunction { + + public HANAJsonTableFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_table(" ); + arguments.jsonDocument().accept( walker ); + if ( arguments.jsonDocument() 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( "'," ); + } + + if ( arguments.jsonPath() != null ) { + final JsonPathPassingClause passingClause = arguments.passingClause(); + final String rawJsonPath; + if ( passingClause != null ) { + rawJsonPath = JsonPathHelper.inlinedJsonPathIncludingPassingClause( + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + rawJsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + assert rawJsonPath.charAt( 0 ) == '$'; + final String jsonPath = "$.v" + rawJsonPath.substring( 1 ); + + sqlAppender.appendSql( "nested path '" ); + sqlAppender.appendSql( jsonPath ); + sqlAppender.appendSql( "' columns" ); + } + else { + sqlAppender.appendSql( "nested path '$.v' columns" ); + } + } + else if ( arguments.jsonPath() != null ) { + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + arguments.jsonPath().accept( walker ); + } + sqlAppender.appendSql( " columns" ); + } + else { + sqlAppender.appendSql( ",'$[*]' columns" ); + } + renderColumnDefinitions( sqlAppender, arguments.columnsClause(), '(', 0, walker ); + sqlAppender.appendSql( ')' ); + + if ( arguments.jsonDocument() instanceof TableColumnReferenceExpression ) { + sqlAppender.appendSql( ')' ); + } + // Default behavior is NULL ON ERROR + if ( arguments.errorBehavior() == JsonTableErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + sqlAppender.appendSql( ')' ); + } + + @Override + protected String determineColumnType(CastTarget castTarget, SqlAstTranslator walker) { + final String columnType = super.determineColumnType( castTarget, walker ); + switch ( columnType ) { + case "boolean": + return "varchar(5)"; + case "clob": + return "varchar(5000)"; + case "nclob": + return "nvarchar(5000)"; + } + return columnType; + } + + @Override + protected void renderColumnPath(String name, @Nullable String jsonPath, SqlAppender sqlAppender, SqlAstTranslator walker) { + if ( jsonPath != null ) { + super.renderColumnPath( name, jsonPath, sqlAppender, walker ); + } + else { + // HANA requires a path + sqlAppender.appendSql( " path '$." ); + sqlAppender.appendSql( name ); + sqlAppender.appendSql( '\'' ); + } + } + + @Override + protected void renderJsonExistsColumnDefinition(SqlAppender sqlAppender, JsonTableExistsColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( " varchar(5) " ); + renderColumnPath( definition.name(), definition.jsonPath(), sqlAppender, walker ); + + sqlAppender.appendSql( " default 'false' on empty" ); + final JsonExistsErrorBehavior errorBehavior = definition.errorBehavior(); + if ( errorBehavior == JsonExistsErrorBehavior.TRUE ) { + sqlAppender.appendSql( " default 'true' on error" ); + } + else if ( errorBehavior == JsonExistsErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + else { + sqlAppender.appendSql( " default 'false' on error" ); + } + } + + @Override + protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression( + List> arguments, + QueryEngine queryEngine) { + //noinspection unchecked + return new SqmJsonTableFunction<>( + this, + this, + getArgumentsValidator(), + getSetReturningTypeResolver(), + queryEngine.getCriteriaBuilder(), + (SqmExpression) arguments.get( 0 ), + arguments.size() > 1 ? (SqmExpression) arguments.get( 1 ) : null + ) { + @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 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 = 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; + } + } + + 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/json/HANAJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonValueFunction.java new file mode 100644 index 000000000000..aaaa77a54862 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonValueFunction.java @@ -0,0 +1,66 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * HANA json_value function. + */ +public class HANAJsonValueFunction extends JsonValueFunction { + + public HANAJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final boolean encodedBoolean = arguments.returningType() != null + && isEncodedBoolean( arguments.returningType().getJdbcMapping() ); + if ( encodedBoolean ) { + sqlAppender.append( "case " ); + } + super.render( sqlAppender, arguments, returnType, walker ); + if ( encodedBoolean ) { + final JdbcMapping type = arguments.returningType().getJdbcMapping(); + //noinspection unchecked + final JdbcLiteralFormatter jdbcLiteralFormatter = type.getJdbcLiteralFormatter(); + final SessionFactoryImplementor sessionFactory = walker.getSessionFactory(); + final Dialect dialect = sessionFactory.getJdbcServices().getDialect(); + final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions(); + final Object trueValue = type.convertToRelationalValue( true ); + final Object falseValue = type.convertToRelationalValue( false ); + sqlAppender.append( " when 'true' then " ); + jdbcLiteralFormatter.appendJdbcLiteral( sqlAppender, trueValue, dialect, wrapperOptions ); + sqlAppender.append( " when 'false' then " ); + jdbcLiteralFormatter.appendJdbcLiteral( sqlAppender, falseValue, dialect, wrapperOptions ); + sqlAppender.append( " end" ); + } + } + + @Override + protected void renderReturningClause(SqlAppender sqlAppender, JsonValueArguments arguments, SqlAstTranslator walker) { + // No return type for booleans, this is handled via decode + if ( arguments.returningType() != null && !isEncodedBoolean( arguments.returningType().getJdbcMapping() ) ) { + super.renderReturningClause( sqlAppender, arguments, walker ); + } + } + + static boolean isEncodedBoolean(JdbcMapping type) { + return type.getJdbcType().isBoolean(); + } +} 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 373dbfef3a0b..9f7e20501b82 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 @@ -44,6 +44,7 @@ public void render( char separator = '('; if ( sqlAstArguments.isEmpty() ) { sqlAppender.appendSql( separator ); + renderReturningClause( sqlAppender, walker ); } else { final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); @@ -65,6 +66,7 @@ public void render( if ( nullBehavior == JsonNullBehavior.NULL ) { sqlAppender.appendSql( " null on null" ); } + renderReturningClause( sqlAppender, walker ); } sqlAppender.appendSql( ')' ); } @@ -72,4 +74,8 @@ public void render( protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { value.accept( walker ); } + + protected void renderReturningClause(SqlAppender sqlAppender, SqlAstTranslator walker) { + // No-op + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java index 1fb56877a4a1..dfb670a5a0b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.hibernate.QueryException; import org.hibernate.sql.ast.SqlAstTranslator; @@ -64,6 +65,27 @@ public static void appendJsonPathDoublePipePassingClause( appendJsonPathConcatenatedPassingClause( sqlAppender, jsonPathExpression, passingClause, walker, "", "||" ); } + public static String inlinedJsonPathIncludingPassingClause( + Expression jsonPathExpression, + JsonPathPassingClause passingClause, + SqlAstTranslator walker) { + return inlinedJsonPathIncludingPassingClause( + walker.getLiteralValue( jsonPathExpression ), + passingClause, + walker + ); + } + + public static String inlinedJsonPathIncludingPassingClause( + String jsonPath, + JsonPathPassingClause passingClause, + SqlAstTranslator walker) { + for ( Map.Entry entry : passingClause.getPassingExpressions().entrySet() ) { + jsonPath = jsonPath.replace( "$" + entry.getKey(), walker.getLiteralValue( entry.getValue() ).toString() ); + } + return jsonPath; + } + public static void appendInlinedJsonPathIncludingPassingClause( SqlAppender sqlAppender, String prefix, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java new file mode 100644 index 000000000000..4a2245069856 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableFunction.java @@ -0,0 +1,361 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.dialect.function.array.DdlTypeHelper; +import org.hibernate.query.derived.AnonymousTupleTableGroupProducer; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingSetReturningFunctionDescriptor; +import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +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.hibernate.query.sqm.tree.expression.SqmExpression; +import org.hibernate.query.sqm.tree.expression.SqmJsonTableFunction; +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.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableOrdinalityColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.JSON; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * Standard json_table function. + */ +public class JsonTableFunction extends AbstractSqmSelfRenderingSetReturningFunctionDescriptor { + + public JsonTableFunction(TypeConfiguration typeConfiguration) { + this( + new JsonTableSetReturningFunctionTypeResolver(), + typeConfiguration + ); + } + + protected JsonTableFunction(SetReturningFunctionTypeResolver setReturningFunctionTypeResolver, TypeConfiguration typeConfiguration) { + super( + "json_table", + new ArgumentTypesValidator( + StandardArgumentsValidators.between( 1, 2 ), + FunctionParameterType.JSON, + FunctionParameterType.STRING + ), + setReturningFunctionTypeResolver, + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) + ); + } + + @Override + protected SelfRenderingSqmSetReturningFunction generateSqmSetReturningFunctionExpression(List> arguments, QueryEngine queryEngine) { + //noinspection unchecked + return new SqmJsonTableFunction<>( + this, + this, + getArgumentsValidator(), + getSetReturningTypeResolver(), + queryEngine.getCriteriaBuilder(), + (SqmExpression) arguments.get( 0 ), + arguments.size() > 1 ? (SqmExpression) arguments.get( 1 ) : null + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + renderJsonTable( sqlAppender, JsonTableArguments.extract( sqlAstArguments ), tupleType, tableIdentifierVariable, walker ); + } + + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_table(" ); + arguments.jsonDocument().accept( walker ); + if ( arguments.jsonPath() != null ) { + sqlAppender.appendSql( ',' ); + arguments.jsonPath().accept( walker ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + sqlAppender.appendSql( " passing " ); + final Map passingExpressions = passingClause.getPassingExpressions(); + final Iterator> iterator = passingExpressions.entrySet().iterator(); + Map.Entry entry = iterator.next(); + entry.getValue().accept( walker ); + sqlAppender.appendSql( " as " ); + sqlAppender.appendDoubleQuoteEscapedString( entry.getKey() ); + while ( iterator.hasNext() ) { + entry = iterator.next(); + sqlAppender.appendSql( ',' ); + entry.getValue().accept( walker ); + sqlAppender.appendSql( " as " ); + sqlAppender.appendDoubleQuoteEscapedString( entry.getKey() ); + } + } + } + renderColumns( sqlAppender, arguments.columnsClause(), 0, walker ); + // Default behavior is NULL ON ERROR + if ( arguments.errorBehavior() == JsonTableErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + sqlAppender.appendSql( ')' ); + } + + protected String determineColumnType(CastTarget castTarget, SqlAstTranslator walker) { + return determineColumnType( castTarget, walker.getSessionFactory().getTypeConfiguration() ); + } + + protected static String determineColumnType(CastTarget castTarget, TypeConfiguration typeConfiguration) { + final String columnDefinition = castTarget.getColumnDefinition(); + if ( columnDefinition != null ) { + return columnDefinition; + } + else { + final String typeName = DdlTypeHelper.getTypeName( + castTarget.getJdbcMapping(), + castTarget.toSize(), + typeConfiguration + ); + final int parenthesisIndex = typeName.indexOf( '(' ); + if ( parenthesisIndex != -1 && typeName.charAt( parenthesisIndex + 1 ) == '$' ) { + // Remove length/precision and scale arguments if it contains unresolved variables + return typeName.substring( 0, parenthesisIndex ); + } + else { + return typeName; + } + } + } + + protected int renderColumns(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( " columns" ); + int nextClauseLevel = renderColumnDefinitions( sqlAppender, jsonTableColumnsClause, '(', clauseLevel, walker ); + sqlAppender.appendSql( ')' ); + return nextClauseLevel; + } + + protected int renderColumnDefinitions(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, char separator, int clauseLevel, SqlAstTranslator walker) { + int nextClauseLevel = clauseLevel + 1; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + sqlAppender.appendSql( separator ); + if ( columnDefinition instanceof JsonTableExistsColumnDefinition definition ) { + renderJsonExistsColumnDefinition( sqlAppender, definition, clauseLevel, walker ); + } + else if ( columnDefinition instanceof JsonTableQueryColumnDefinition definition ) { + renderJsonQueryColumnDefinition( sqlAppender, definition, clauseLevel, walker ); + } + else if ( columnDefinition instanceof JsonTableValueColumnDefinition definition ) { + renderJsonValueColumnDefinition( sqlAppender, definition, clauseLevel, walker ); + } + else if ( columnDefinition instanceof JsonTableOrdinalityColumnDefinition definition ) { + renderJsonOrdinalityColumnDefinition( sqlAppender, definition, clauseLevel, walker ); + } + else { + nextClauseLevel = renderJsonNestedColumnDefinition( sqlAppender, (JsonTableNestedColumnDefinition) columnDefinition, nextClauseLevel, walker ); + } + separator = ','; + } + return nextClauseLevel; + } + + protected int renderJsonNestedColumnDefinition(SqlAppender sqlAppender, JsonTableNestedColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( "nested " ); + sqlAppender.appendSingleQuoteEscapedString( definition.jsonPath() ); + return renderColumns( sqlAppender, definition.columns(), clauseLevel, walker ); + } + + protected int countNestedColumnDefinitions(JsonTableColumnsClause jsonTableColumnsClause) { + int count = 0; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + count = count + 1 + countNestedColumnDefinitions( nestedColumnDefinition.columns() ); + } + } + return count; + } + + protected void renderJsonOrdinalityColumnDefinition(SqlAppender sqlAppender, JsonTableOrdinalityColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( " for ordinality" ); + } + + protected void renderJsonValueColumnDefinition(SqlAppender sqlAppender, JsonTableValueColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( determineColumnType( definition.type(), walker ) ); + + renderColumnPath( definition.name(), definition.jsonPath(), sqlAppender, walker ); + + if ( definition.errorBehavior() != null ) { + if ( definition.errorBehavior() == JsonValueErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + else if ( definition.errorBehavior() != JsonValueErrorBehavior.NULL ) { + final Expression defaultExpression = definition.errorBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on error" ); + } + } + if ( definition.emptyBehavior() != null ) { + if ( definition.emptyBehavior() == JsonValueEmptyBehavior.ERROR ) { + sqlAppender.appendSql( " error on empty" ); + } + else if ( definition.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + final Expression defaultExpression = definition.emptyBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on empty" ); + } + } + // todo: mismatch clause? + } + + protected void renderColumnPath(String name, @Nullable String jsonPath, SqlAppender sqlAppender, SqlAstTranslator walker) { + if ( jsonPath != null ) { + sqlAppender.appendSql( " path " ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + } + } + + protected void renderJsonQueryColumnDefinition(SqlAppender sqlAppender, JsonTableQueryColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( determineColumnType( new CastTarget( definition.type() ), walker ) ); + if ( definition.type().getJdbcType().getDdlTypeCode() != SqlTypes.JSON ) { + sqlAppender.appendSql( " format json" ); + } + + if ( definition.wrapMode() != null ) { + switch ( definition.wrapMode() ) { + case WITH_WRAPPER -> sqlAppender.appendSql( " with wrapper" ); + case WITHOUT_WRAPPER -> sqlAppender.appendSql( " without wrapper" ); + case WITH_CONDITIONAL_WRAPPER -> sqlAppender.appendSql( " with conditional wrapper" ); + } + } + + renderColumnPath( definition.name(), definition.jsonPath(), sqlAppender, walker ); + + if ( definition.errorBehavior() != null ) { + switch ( definition.errorBehavior() ) { + case ERROR -> sqlAppender.appendSql( " error on error" ); + case NULL -> sqlAppender.appendSql( " null on error" ); + case EMPTY_OBJECT -> sqlAppender.appendSql( " empty object on error" ); + case EMPTY_ARRAY -> sqlAppender.appendSql( " empty array on error" ); + } + } + + if ( definition.emptyBehavior() != null ) { + switch ( definition.emptyBehavior() ) { + case ERROR -> sqlAppender.appendSql( " error on empty" ); + case NULL -> sqlAppender.appendSql( " null on empty" ); + case EMPTY_OBJECT -> sqlAppender.appendSql( " empty object on empty" ); + case EMPTY_ARRAY -> sqlAppender.appendSql( " empty array on empty" ); + } + } + } + + protected void renderJsonExistsColumnDefinition(SqlAppender sqlAppender, JsonTableExistsColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( determineColumnType( new CastTarget( definition.type() ), walker ) ); + + sqlAppender.appendSql( " exists" ); + renderColumnPath( definition.name(), definition.jsonPath(), sqlAppender, walker ); + final JsonExistsErrorBehavior errorBehavior = definition.errorBehavior(); + if ( errorBehavior != null && errorBehavior != JsonExistsErrorBehavior.FALSE ) { + if ( errorBehavior == JsonExistsErrorBehavior.TRUE ) { + sqlAppender.appendSql( " true on error" ); + } + else { + sqlAppender.appendSql( " error on error" ); + } + } + } + + protected record JsonTableArguments( + Expression jsonDocument, + @Nullable Expression jsonPath, + boolean isJsonType, + @Nullable JsonPathPassingClause passingClause, + @Nullable JsonTableErrorBehavior errorBehavior, + JsonTableColumnsClause columnsClause + ){ + public static JsonTableArguments extract(List sqlAstArguments) { + final Expression jsonDocument = (Expression) sqlAstArguments.get( 0 ); + Expression jsonPath = null; + JsonPathPassingClause passingClause = null; + JsonTableErrorBehavior errorBehavior = null; + JsonTableColumnsClause columnsClause = null; + int nextIndex = 1; + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof Expression ) { + jsonPath = (Expression) node; + nextIndex++; + } + } + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonPathPassingClause ) { + passingClause = (JsonPathPassingClause) node; + nextIndex++; + } + } + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonTableErrorBehavior ) { + errorBehavior = (JsonTableErrorBehavior) node; + nextIndex++; + } + } + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonTableColumnsClause ) { + columnsClause = (JsonTableColumnsClause) node; + } + } + return new JsonTableArguments( + jsonDocument, + jsonPath, + jsonDocument.getExpressionType() != null + && jsonDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isJson(), + passingClause, + errorBehavior, + columnsClause + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableSetReturningFunctionTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableSetReturningFunctionTypeResolver.java new file mode 100644 index 000000000000..713beca99d96 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonTableSetReturningFunctionTypeResolver.java @@ -0,0 +1,146 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +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.produce.function.SetReturningFunctionTypeResolver; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmJsonTableFunction; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableOrdinalityColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @since 7.0 + */ +public class JsonTableSetReturningFunctionTypeResolver implements SetReturningFunctionTypeResolver { + + @Override + public AnonymousTupleType resolveTupleType(List> arguments, TypeConfiguration typeConfiguration) { + final SqmJsonTableFunction.Columns columns = (SqmJsonTableFunction.Columns) arguments.get( arguments.size() - 1 ); + return columns.createTupleType(); + } + + @Override + public SelectableMapping[] resolveFunctionReturnType( + List arguments, + String tableIdentifierVariable, + boolean lateral, + boolean withOrdinality, + SqmToSqlAstConverter converter) { + JsonTableColumnsClause columnsClause = null; + for ( SqlAstNode argument : arguments ) { + if ( argument instanceof JsonTableColumnsClause ) { + columnsClause = (JsonTableColumnsClause) argument; + break; + } + } + assert columnsClause != null; + + final List columnDefinitions = columnsClause.getColumnDefinitions(); + final List selectableMappings = new ArrayList<>( columnDefinitions.size() ); + addSelectableMappings( selectableMappings, columnsClause, converter ); + return selectableMappings.toArray( new SelectableMapping[0] ); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableNestedColumnDefinition columnDefinition, SqmToSqlAstConverter converter) { + addSelectableMappings( selectableMappings, columnDefinition.columns(), converter ); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableColumnsClause columnsClause, SqmToSqlAstConverter converter) { + for ( JsonTableColumnDefinition columnDefinition : columnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableExistsColumnDefinition definition ) { + addSelectableMappings( selectableMappings, definition, converter ); + } + else if ( columnDefinition instanceof JsonTableQueryColumnDefinition definition ) { + addSelectableMappings( selectableMappings, definition, converter ); + } + else if ( columnDefinition instanceof JsonTableValueColumnDefinition definition ) { + addSelectableMappings( selectableMappings, definition, converter ); + } + else if ( columnDefinition instanceof JsonTableOrdinalityColumnDefinition definition ) { + addSelectableMappings( selectableMappings, definition, converter ); + } + else { + addSelectableMappings( selectableMappings, (JsonTableNestedColumnDefinition) columnDefinition, converter ); + } + } + } + + protected void addSelectableMappings(List selectableMappings, JsonTableOrdinalityColumnDefinition definition, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Long.class ), + converter + ); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableValueColumnDefinition definition, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + definition.type().getJdbcMapping(), + converter + ); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableQueryColumnDefinition definition, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + converter.getCreationContext().getTypeConfiguration().getBasicTypeRegistry() + .resolve( String.class, SqlTypes.JSON ), + converter + ); + } + + protected void addSelectableMappings(List selectableMappings, JsonTableExistsColumnDefinition definition, SqmToSqlAstConverter converter) { + addSelectableMapping( + selectableMappings, + definition.name(), + converter.getCreationContext().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ), + converter + ); + } + + protected void addSelectableMapping(List selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) { + selectableMappings.add( new SelectableMappingImpl( + "", + name, + new SelectablePath( name ), + null, + null, + null, + null, + null, + null, + null, + false, + false, + false, + false, + false, + false, + type + )); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java index eca1c9a58066..449751c277fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java @@ -131,10 +131,7 @@ protected void render( walker ); } - if ( arguments.returningType() != null ) { - sqlAppender.appendSql( " returning " ); - arguments.returningType().accept( walker ); - } + renderReturningClause( sqlAppender, arguments, walker ); if ( arguments.errorBehavior() != null ) { if ( arguments.errorBehavior() == JsonValueErrorBehavior.ERROR ) { sqlAppender.appendSql( " error on error" ); @@ -162,6 +159,13 @@ else if ( arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { sqlAppender.appendSql( ')' ); } + protected void renderReturningClause(SqlAppender sqlAppender, JsonValueArguments arguments, SqlAstTranslator walker) { + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( " returning " ); + arguments.returningType().accept( walker ); + } + } + protected record JsonValueArguments( Expression jsonDocument, Expression jsonPath, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java index 857450dfacd5..d1ccd0f2aedb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java @@ -36,7 +36,12 @@ protected void render( throw new QueryException( "Can't emulate on empty clause on MariaDB" ); } if ( arguments.returningType() != null ) { - sqlAppender.append( "cast(" ); + if ( arguments.returningType().getJdbcMapping().getJdbcType().isBoolean() ) { + sqlAppender.append( "case " ); + } + else { + sqlAppender.append( "cast(" ); + } } sqlAppender.appendSql( "json_unquote(nullif(json_extract(" ); arguments.jsonDocument().accept( walker ); @@ -54,9 +59,14 @@ protected void render( } sqlAppender.appendSql( "),'null'))" ); if ( arguments.returningType() != null ) { - sqlAppender.appendSql( " as " ); - arguments.returningType().accept( walker ); - sqlAppender.appendSql( ')' ); + if ( arguments.returningType().getJdbcMapping().getJdbcType().isBoolean() ) { + sqlAppender.append( " when 'true' then true when 'false' then false end " ); + } + else { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonTableFunction.java new file mode 100644 index 000000000000..f79867cdbf35 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonTableFunction.java @@ -0,0 +1,184 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.array.DdlTypeHelper; +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.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MySQL json_table function. + */ +public class MySQLJsonTableFunction extends JsonTableFunction { + + public MySQLJsonTableFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() == JsonTableErrorBehavior.NULL ) { + throw new QueryException( "Can't emulate null on error clause on MySQL" ); + } + sqlAppender.appendSql( "json_table(" ); + arguments.jsonDocument().accept( walker ); + if ( arguments.jsonPath() == null ) { + sqlAppender.appendSql( ",'$'" ); + } + else { + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + arguments.jsonPath().accept( walker ); + } + } + renderColumns( sqlAppender, arguments.columnsClause(), 0, walker ); + sqlAppender.appendSql( ')' ); + } + + @Override + protected int renderJsonNestedColumnDefinition(SqlAppender sqlAppender, JsonTableNestedColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // MySQL docs way that "path" is optional, but it isn't... + sqlAppender.appendSql( "nested path " ); + sqlAppender.appendSingleQuoteEscapedString( definition.jsonPath() ); + return renderColumns( sqlAppender, definition.columns(), clauseLevel, walker ); + } + + @Override + protected void renderJsonValueColumnDefinition(SqlAppender sqlAppender, JsonTableValueColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql(determineColumnType( definition.type(), walker ) ); + + if ( definition.jsonPath() != null ) { + sqlAppender.appendSql( " path " ); + sqlAppender.appendSingleQuoteEscapedString( definition.jsonPath() ); + } + else { + sqlAppender.appendSql( " path '$." ); + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( "'" ); + } + + if ( definition.errorBehavior() != null ) { + if ( definition.errorBehavior() == JsonValueErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + else if ( definition.errorBehavior() != JsonValueErrorBehavior.NULL ) { + final Expression defaultExpression = definition.errorBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on error" ); + } + } + if ( definition.emptyBehavior() != null ) { + if ( definition.emptyBehavior() == JsonValueEmptyBehavior.ERROR ) { + sqlAppender.appendSql( " error on empty" ); + } + else if ( definition.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + final Expression defaultExpression = definition.emptyBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on empty" ); + } + } + // todo: mismatch clause? + } + + @Override + protected void renderJsonQueryColumnDefinition(SqlAppender sqlAppender, JsonTableQueryColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // Conditional wrapper is the default behavior on MySQL + if ( definition.wrapMode() != null && definition.wrapMode() != JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER ) { + throw new QueryException( "Can't emulate wrapper clause on MySQL" ); + } + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( DdlTypeHelper.getTypeName( definition.type(), walker.getSessionFactory().getTypeConfiguration() ) ); + if ( definition.type().getJdbcType().getDdlTypeCode() != SqlTypes.JSON ) { + sqlAppender.appendSql( " format json" ); + } + + if ( definition.jsonPath() != null ) { + sqlAppender.appendSql( " path " ); + sqlAppender.appendSingleQuoteEscapedString( definition.jsonPath() ); + } + else { + sqlAppender.appendSql( " path '$." ); + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( "'" ); + } + + if ( definition.errorBehavior() != null ) { + switch ( definition.errorBehavior() ) { + case ERROR -> sqlAppender.appendSql( " error on error" ); + case NULL -> sqlAppender.appendSql( " null on error" ); + case EMPTY_OBJECT -> sqlAppender.appendSql( " default '{}' on error" ); + case EMPTY_ARRAY -> sqlAppender.appendSql( " default '[]' on error" ); + } + } + + if ( definition.emptyBehavior() != null ) { + switch ( definition.emptyBehavior() ) { + case ERROR -> sqlAppender.appendSql( " error on empty" ); + case NULL -> sqlAppender.appendSql( " null on empty" ); + case EMPTY_OBJECT -> sqlAppender.appendSql( " default '{}' on empty" ); + case EMPTY_ARRAY -> sqlAppender.appendSql( " default '[]' on empty" ); + } + } + } + + @Override + protected void renderJsonExistsColumnDefinition(SqlAppender sqlAppender, JsonTableExistsColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // jsonb_path_exists errors by default + if ( definition.errorBehavior() != null && definition.errorBehavior() != JsonExistsErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on MySQL" ); + } + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( DdlTypeHelper.getTypeName( definition.type(), walker.getSessionFactory().getTypeConfiguration() ) ); + + sqlAppender.appendSql( " exists" ); + if ( definition.jsonPath() != null ) { + sqlAppender.appendSql( " path " ); + sqlAppender.appendSingleQuoteEscapedString( definition.jsonPath() ); + } + else { + sqlAppender.appendSql( " path '$." ); + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( "'" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java index 686b04dfb71c..8f5919068573 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java @@ -36,7 +36,12 @@ protected void render( } else { if ( arguments.returningType() != null ) { - sqlAppender.append( "cast(" ); + if ( arguments.returningType().getJdbcMapping().getJdbcType().isBoolean() ) { + sqlAppender.append( "case " ); + } + else { + sqlAppender.append( "cast(" ); + } } sqlAppender.appendSql( "json_unquote(nullif(json_extract(" ); arguments.jsonDocument().accept( walker ); @@ -54,9 +59,14 @@ protected void render( } sqlAppender.appendSql( "),cast('null' as json)))" ); if ( arguments.returningType() != null ) { - sqlAppender.appendSql( " as " ); - arguments.returningType().accept( walker ); - sqlAppender.appendSql( ')' ); + if ( arguments.returningType().getJdbcMapping().getJdbcType().isBoolean() ) { + sqlAppender.append( " when 'true' then true when 'false' then false end " ); + } + else { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java index e3ea2df0817d..987c2c105908 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java @@ -6,7 +6,9 @@ import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.dialect.function.CastFunction; +import org.hibernate.engine.jdbc.Size; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; @@ -14,6 +16,7 @@ import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.BasicType; import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -23,11 +26,19 @@ public class OracleJsonArrayFunction extends JsonArrayFunction { private final CastTarget stringCastTarget; + private final @Nullable String returningType; private CastFunction castFunction; public OracleJsonArrayFunction(TypeConfiguration typeConfiguration) { super( typeConfiguration ); this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); + final BasicType jsonType = typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ); + final String jsonTypeName = typeConfiguration.getDdlTypeRegistry().getTypeName( + jsonType.getJdbcType().getDdlTypeCode(), + Size.nil(), + jsonType + ); + this.returningType = jsonTypeName; } @Override @@ -57,4 +68,10 @@ protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTran } } } + + @Override + protected void renderReturningClause(SqlAppender sqlAppender, SqlAstTranslator walker) { + sqlAppender.appendSql( " returning " ); + sqlAppender.appendSql( returningType ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java index 7ee2ef0f620f..75f9db2a5123 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java @@ -25,8 +25,8 @@ public class OracleJsonObjectFunction extends JsonObjectFunction { private final CastTarget stringCastTarget; private CastFunction castFunction; - public OracleJsonObjectFunction(TypeConfiguration typeConfiguration) { - super( typeConfiguration, true ); + public OracleJsonObjectFunction(boolean colonSyntax, TypeConfiguration typeConfiguration) { + super( typeConfiguration, colonSyntax ); this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java new file mode 100644 index 000000000000..7f4ea449b124 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonTableFunction.java @@ -0,0 +1,153 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +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.AnonymousTupleTableGroupProducer; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.sql.Template; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JsonAsStringJdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +import static org.hibernate.dialect.function.json.OracleJsonValueFunction.isEncodedBoolean; + +/** + * Oracle json_table function. + */ +public class OracleJsonTableFunction extends JsonTableFunction { + + public OracleJsonTableFunction(TypeConfiguration typeConfiguration) { + super( new OracleJsonTableSetReturningFunctionTypeResolver(), typeConfiguration ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_table(" ); + arguments.jsonDocument().accept( walker ); + if ( arguments.jsonPath() != null ) { + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + arguments.jsonPath().accept( walker ); + } + } + // Default behavior is NULL ON ERROR + if ( arguments.errorBehavior() == JsonTableErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + renderColumns( sqlAppender, arguments.columnsClause(), 0, walker ); + sqlAppender.appendSql( ')' ); + } + + @Override + protected String determineColumnType(CastTarget castTarget, SqlAstTranslator walker) { + final String typeName = super.determineColumnType( castTarget, walker ); + // The various float types are not supported in json_table() on all versions of Oracle, + // but luckily, one can use "number" to "parse" arbitrary precision numbers + return switch ( typeName ) { + case "float", "binary_float", "binary_double" -> "number"; + case "number(1,0)" -> isEncodedBoolean( castTarget.getJdbcMapping() ) ? "varchar2(5)" : typeName; + // Prefer clob over blob for JSON types for backwards compatibility + case "blob" -> isJson( castTarget.getJdbcMapping() ) ? "clob" : typeName; + default -> typeName; + }; + } + + private boolean isJson(JdbcMapping jdbcMapping) { + return jdbcMapping.getJdbcType().isJson(); + } + + private static class OracleJsonTableSetReturningFunctionTypeResolver extends JsonTableSetReturningFunctionTypeResolver { + @Override + protected void addSelectableMappings(List selectableMappings, JsonTableQueryColumnDefinition definition, SqmToSqlAstConverter converter) { + // + final TypeConfiguration typeConfiguration = converter.getCreationContext().getTypeConfiguration(); + final JdbcType jsonType = typeConfiguration.getJdbcTypeRegistry().getDescriptor( SqlTypes.JSON ); + if ( jsonType.getDdlTypeCode() == SqlTypes.BLOB ) { + // Blob is not supported on all DB versions as return type for json_table(), so we have to use clob + addSelectableMapping( + selectableMappings, + definition.name(), + typeConfiguration.getBasicTypeRegistry().resolve( + typeConfiguration.getJavaTypeRegistry().getDescriptor( String.class ), + JsonAsStringJdbcType.CLOB_INSTANCE + ), + converter + ); + } + else { + super.addSelectableMappings( selectableMappings, definition, converter ); + } + } + + @Override + protected void addSelectableMapping(List selectableMappings, String name, JdbcMapping type, SqmToSqlAstConverter converter) { + if ( isEncodedBoolean( type ) ) { + //noinspection unchecked + final JdbcLiteralFormatter jdbcLiteralFormatter = type.getJdbcLiteralFormatter(); + final SessionFactoryImplementor sessionFactory = converter.getCreationContext().getSessionFactory(); + final Dialect dialect = sessionFactory.getJdbcServices().getDialect(); + final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions(); + final Object trueValue = type.convertToRelationalValue( true ); + final Object falseValue = type.convertToRelationalValue( false ); + final String trueFragment = jdbcLiteralFormatter.toJdbcLiteral( trueValue, dialect, wrapperOptions ); + final String falseFragment = jdbcLiteralFormatter.toJdbcLiteral( falseValue, dialect, wrapperOptions ); + selectableMappings.add( new SelectableMappingImpl( + "", + name, + new SelectablePath( name ), + "decode(" + Template.TEMPLATE + "." + name + ",'true'," + trueFragment + ",'false'," + falseFragment + ")", + null, + "varchar2(5)", + null, + null, + null, + null, + false, + false, + false, + false, + false, + false, + type + )); + } + else { + super.addSelectableMapping( selectableMappings, name, type, converter ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java new file mode 100644 index 000000000000..15b215fe760b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonValueFunction.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_value function. + */ +public class OracleJsonValueFunction extends JsonValueFunction { + + public OracleJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final boolean encodedBoolean = arguments.returningType() != null + && isEncodedBoolean( arguments.returningType().getJdbcMapping() ); + if ( encodedBoolean ) { + sqlAppender.append( "decode(" ); + } + super.render( sqlAppender, arguments, returnType, walker ); + if ( encodedBoolean ) { + final JdbcMapping type = arguments.returningType().getJdbcMapping(); + //noinspection unchecked + final JdbcLiteralFormatter jdbcLiteralFormatter = type.getJdbcLiteralFormatter(); + final SessionFactoryImplementor sessionFactory = walker.getSessionFactory(); + final Dialect dialect = sessionFactory.getJdbcServices().getDialect(); + final WrapperOptions wrapperOptions = sessionFactory.getWrapperOptions(); + final Object trueValue = type.convertToRelationalValue( true ); + final Object falseValue = type.convertToRelationalValue( false ); + sqlAppender.append( ",'true'," ); + jdbcLiteralFormatter.appendJdbcLiteral( sqlAppender, trueValue, dialect, wrapperOptions ); + sqlAppender.append( ",'false'," ); + jdbcLiteralFormatter.appendJdbcLiteral( sqlAppender, falseValue, dialect, wrapperOptions ); + sqlAppender.append( ')' ); + } + } + + @Override + protected void renderReturningClause(SqlAppender sqlAppender, JsonValueArguments arguments, SqlAstTranslator walker) { + if ( arguments.returningType() != null && isEncodedBoolean( arguments.returningType().getJdbcMapping() ) ) { + sqlAppender.appendSql( " returning varchar2(5)" ); + } + else { + super.renderReturningClause( sqlAppender, arguments, walker ); + } + } + + static boolean isEncodedBoolean(JdbcMapping type) { + return type.getJdbcType().isBoolean() && type.getJdbcType().getDdlTypeCode() != SqlTypes.BOOLEAN; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java index 6d43e3eede51..29c1051d5014 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java @@ -6,10 +6,13 @@ import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.QueryException; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; import org.hibernate.type.spi.TypeConfiguration; @@ -28,11 +31,18 @@ protected void render( JsonExistsArguments arguments, ReturnableType returnType, SqlAstTranslator walker) { + // jsonb_path_exists errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonExistsErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + } + appendJsonExists( sqlAppender, walker, arguments.jsonDocument(), arguments.jsonPath(), arguments.passingClause() ); + } + + static void appendJsonExists(SqlAppender sqlAppender, SqlAstTranslator walker, Expression jsonDocument, Expression jsonPath, @Nullable JsonPathPassingClause passingClause) { sqlAppender.appendSql( "jsonb_path_exists(" ); - arguments.jsonDocument().accept( walker ); + jsonDocument.accept( walker ); sqlAppender.appendSql( ',' ); - arguments.jsonPath().accept( walker ); - final JsonPathPassingClause passingClause = arguments.passingClause(); + jsonPath.accept( walker ); if ( passingClause != null ) { sqlAppender.append( ",jsonb_build_object" ); char separator = '('; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java index 40a497777862..59237bd48418 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java @@ -6,6 +6,7 @@ import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.QueryException; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; @@ -42,8 +43,19 @@ protected void render( if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); } - final JsonQueryWrapMode wrapMode = arguments.wrapMode(); + appendJsonQuery( + sqlAppender, + arguments.jsonDocument(), + arguments.jsonPath(), + arguments.isJsonType(), + arguments.wrapMode(), + arguments.passingClause(), + walker + ); + } + + static void appendJsonQuery(SqlAppender sqlAppender, Expression jsonDocument, SqlAstNode jsonPath, boolean isJsonType, JsonQueryWrapMode wrapMode, @Nullable JsonPathPassingClause passingClause, SqlAstTranslator walker) { if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { sqlAppender.appendSql( "jsonb_path_query_array(" ); } @@ -53,16 +65,15 @@ else if ( wrapMode == JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER ) { else { sqlAppender.appendSql( "(select t.v from jsonb_path_query(" ); } - final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + final boolean needsCast = !isJsonType && jsonDocument instanceof JdbcParameter; if ( needsCast ) { sqlAppender.appendSql( "cast(" ); } - arguments.jsonDocument().accept( walker ); + jsonDocument.accept( walker ); if ( needsCast ) { sqlAppender.appendSql( " as jsonb)" ); } sqlAppender.appendSql( ',' ); - final SqlAstNode jsonPath = arguments.jsonPath(); if ( jsonPath instanceof Literal ) { jsonPath.accept( walker ); } @@ -71,7 +82,6 @@ else if ( wrapMode == JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER ) { jsonPath.accept( walker ); sqlAppender.appendSql( " as jsonpath)" ); } - final JsonPathPassingClause passingClause = arguments.passingClause(); if ( passingClause != null ) { sqlAppender.append( ",jsonb_build_object" ); char separator = '('; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonTableFunction.java new file mode 100644 index 000000000000..95555792a13c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonTableFunction.java @@ -0,0 +1,237 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +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.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableOrdinalityColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.Map; + +/** + * PostgreSQL json_table function. + */ +public class PostgreSQLJsonTableFunction extends JsonTableFunction { + + public PostgreSQLJsonTableFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderJsonTable( + SqlAppender sqlAppender, + JsonTableArguments arguments, + AnonymousTupleTableGroupProducer tupleType, + String tableIdentifierVariable, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() == JsonTableErrorBehavior.NULL ) { + throw new QueryException( "Can't emulate null on error clause on PostgreSQL" ); + } + sqlAppender.appendSql( "(select" ); + + renderColumns( sqlAppender, arguments.columnsClause(), 0, walker ); + + sqlAppender.appendSql( " from jsonb_path_query(" ); + + final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + arguments.jsonDocument().accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + final SqlAstNode jsonPath = arguments.jsonPath(); + if ( jsonPath != null ) { + sqlAppender.appendSql( ',' ); + if ( jsonPath instanceof Literal ) { + jsonPath.accept( walker ); + } + else { + sqlAppender.appendSql( "cast(" ); + jsonPath.accept( walker ); + sqlAppender.appendSql( " as jsonpath)" ); + } + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + sqlAppender.append( ",jsonb_build_object" ); + char separator = '('; + for ( Map.Entry entry : passingClause.getPassingExpressions().entrySet() ) { + sqlAppender.append( separator ); + sqlAppender.appendSingleQuoteEscapedString( entry.getKey() ); + sqlAppender.append( ',' ); + entry.getValue().accept( walker ); + separator = ','; + } + sqlAppender.append( ')' ); + } + } + else { + sqlAppender.appendSql( ",'$[*]'" ); + } + sqlAppender.appendSql( ") with ordinality t0(d,i)" ); + renderNestedColumnJoins( sqlAppender, arguments.columnsClause(), 0, walker ); + + sqlAppender.appendSql( ')' ); + } + + protected int renderNestedColumnJoins(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + int nextClauseLevel = clauseLevel; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + sqlAppender.appendSql( " left join lateral jsonb_path_query(t" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( ".d," ); + sqlAppender.appendSingleQuoteEscapedString( nestedColumnDefinition.jsonPath() ); + sqlAppender.appendSql( ") with ordinality t" ); + sqlAppender.appendSql( clauseLevel + 1 ); + sqlAppender.appendSql( "(d,i) on true" ); + nextClauseLevel = renderNestedColumnJoins( sqlAppender, nestedColumnDefinition.columns(), clauseLevel + 1, walker ); + } + } + return nextClauseLevel; + } + + @Override + protected int renderColumns(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + return renderColumnDefinitions( sqlAppender, jsonTableColumnsClause, ' ', clauseLevel, walker ); + } + + @Override + protected int renderJsonNestedColumnDefinition(SqlAppender sqlAppender, JsonTableNestedColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + return renderColumns( sqlAppender, definition.columns(), clauseLevel, walker ); + } + + @Override + protected void renderJsonOrdinalityColumnDefinition(SqlAppender sqlAppender, JsonTableOrdinalityColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( 't' ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( ".i " ); + sqlAppender.appendSql( definition.name() ); + } + + @Override + protected void renderJsonValueColumnDefinition(SqlAppender sqlAppender, JsonTableValueColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // jsonb_path_query_first errors by default + if ( definition.errorBehavior() != null && definition.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + } + if ( definition.emptyBehavior() != null && definition.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + } + final String jsonPath = definition.jsonPath() == null + ? "$." + definition.name() + : definition.jsonPath(); + PostgreSQLJsonValueFunction.appendJsonValue( + sqlAppender, + new ClauseLevelDocumentExpression( clauseLevel ), + new QueryLiteral<>( + jsonPath, + walker.getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( String.class ) + ), + true, + definition.type(), + null, + walker + ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( definition.name() ); + } + + @Override + protected void renderJsonQueryColumnDefinition(SqlAppender sqlAppender, JsonTableQueryColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // jsonb_path_query functions error by default + if ( definition.errorBehavior() != null && definition.errorBehavior() != JsonQueryErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + } + if ( definition.emptyBehavior() != null && definition.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + } + final String jsonPath = definition.jsonPath() == null + ? "$." + definition.name() + : definition.jsonPath(); + PostgreSQLJsonQueryFunction.appendJsonQuery( + sqlAppender, + new ClauseLevelDocumentExpression( clauseLevel ), + new QueryLiteral<>( + jsonPath, + walker.getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( String.class ) + ), + true, + definition.wrapMode(), + null, + walker + ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( definition.name() ); + } + + @Override + protected void renderJsonExistsColumnDefinition(SqlAppender sqlAppender, JsonTableExistsColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + // jsonb_path_exists errors by default + if ( definition.errorBehavior() != null && definition.errorBehavior() != JsonExistsErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + } + final String jsonPath = definition.jsonPath() == null + ? "$." + definition.name() + : definition.jsonPath(); + PostgreSQLJsonExistsFunction.appendJsonExists( + sqlAppender, + walker, + new ClauseLevelDocumentExpression( clauseLevel ), + new QueryLiteral<>( + jsonPath, + walker.getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( String.class ) + ), + null + ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( definition.name() ); + } + + protected static class ClauseLevelDocumentExpression implements SelfRenderingExpression { + private final int clauseLevel; + + public ClauseLevelDocumentExpression(int clauseLevel) { + this.clauseLevel = clauseLevel; + } + + @Override + public void renderToSql(SqlAppender sqlAppender, SqlAstTranslator walker, SessionFactoryImplementor sessionFactory) { + sqlAppender.appendSql( 't' ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( ".d" ); + } + + @Override + public JdbcMappingContainer getExpressionType() { + return null; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java index 9f7081625a2e..592f43daa06d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java @@ -6,11 +6,13 @@ import java.util.Map; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.QueryException; import org.hibernate.query.ReturnableType; 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.CastTarget; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; @@ -41,20 +43,32 @@ protected void render( if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); } - if ( arguments.returningType() != null ) { + + appendJsonValue( + sqlAppender, + arguments.jsonDocument(), + arguments.jsonPath(), + arguments.isJsonType(), + arguments.returningType(), + arguments.passingClause(), + walker + ); + } + + static void appendJsonValue(SqlAppender sqlAppender, Expression jsonDocument, SqlAstNode jsonPath, boolean isJsonType, @Nullable CastTarget castTarget, @Nullable JsonPathPassingClause passingClause, SqlAstTranslator walker) { + if ( castTarget != null ) { sqlAppender.appendSql( "cast(" ); } sqlAppender.appendSql( "jsonb_path_query_first(" ); - final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + final boolean needsCast = !isJsonType && jsonDocument instanceof JdbcParameter; if ( needsCast ) { sqlAppender.appendSql( "cast(" ); } - arguments.jsonDocument().accept( walker ); + jsonDocument.accept( walker ); if ( needsCast ) { sqlAppender.appendSql( " as jsonb)" ); } sqlAppender.appendSql( ',' ); - final SqlAstNode jsonPath = arguments.jsonPath(); if ( jsonPath instanceof Literal ) { jsonPath.accept( walker ); } @@ -63,7 +77,6 @@ protected void render( jsonPath.accept( walker ); sqlAppender.appendSql( " as jsonpath)" ); } - final JsonPathPassingClause passingClause = arguments.passingClause(); if ( passingClause != null ) { sqlAppender.append( ",jsonb_build_object" ); char separator = '('; @@ -78,9 +91,9 @@ protected void render( } // Unquote the value sqlAppender.appendSql( ")#>>'{}'" ); - if ( arguments.returningType() != null ) { + if ( castTarget != null ) { sqlAppender.appendSql( " as " ); - arguments.returningType().accept( walker ); + castTarget.accept( walker ); sqlAppender.appendSql( ')' ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonTableFunction.java new file mode 100644 index 000000000000..9ab5f39111bf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonTableFunction.java @@ -0,0 +1,263 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.QueryException; +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.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableOrdinalityColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.List; + +/** + * SQL Server json_table function. + */ +public class SQLServerJsonTableFunction extends JsonTableFunction { + + public SQLServerJsonTableFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderJsonTable(SqlAppender sqlAppender, JsonTableArguments arguments, AnonymousTupleTableGroupProducer tupleType, String tableIdentifierVariable, SqlAstTranslator walker) { + sqlAppender.appendSql( "(select" ); + renderColumnSelects( sqlAppender, arguments.columnsClause(), 0, walker ); + sqlAppender.appendSql( " from openjson(" ); + arguments.jsonDocument().accept( walker ); + if ( arguments.jsonPath() != null ) { + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + // Default behavior is NULL ON ERROR + arguments.errorBehavior() == JsonTableErrorBehavior.ERROR ? "strict " : "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + if ( arguments.errorBehavior() == JsonTableErrorBehavior.ERROR ) { + // Default behavior is NULL ON ERROR + sqlAppender.appendSql( "'strict '+" ); + } + arguments.jsonPath().accept( walker ); + } + } + else if ( arguments.errorBehavior() == JsonTableErrorBehavior.ERROR ) { + // Default behavior is NULL ON ERROR + sqlAppender.appendSql( ",'strict $'" ); + } + sqlAppender.appendSql( ")" ); + renderColumnDefinitions( sqlAppender, arguments.columnsClause(), 0, walker ); + sqlAppender.appendSql( " t0" ); + renderNestedColumnJoins( sqlAppender, arguments.columnsClause(), 0, walker ); + sqlAppender.appendSql( ')' ); + } + + protected int renderNestedColumnJoins(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + int currentClauseLevel = clauseLevel; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableNestedColumnDefinition nestedColumnDefinition ) { + final int nextClauseLevel = currentClauseLevel + 1; + // For every nested column, we create a lateral join that selects all the columns nested within it + sqlAppender.appendSql( " cross apply (select" ); + renderColumnSelects( sqlAppender, nestedColumnDefinition.columns(), nextClauseLevel, walker ); + // The previous table alias will have a special column ready for this join to use for openjson + sqlAppender.appendSql( " from openjson(t" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( ".nested_" ); + sqlAppender.appendSql( nextClauseLevel ); + sqlAppender.appendSql( "_)" ); + renderColumnDefinitions( sqlAppender, nestedColumnDefinition.columns(), nextClauseLevel, walker ); + sqlAppender.appendSql( " t" ); + sqlAppender.appendSql( nextClauseLevel ); + sqlAppender.appendSql( ") t" ); + sqlAppender.appendSql( nextClauseLevel ); + currentClauseLevel = renderNestedColumnJoins( sqlAppender, nestedColumnDefinition.columns(), nextClauseLevel, walker ); + } + } + return currentClauseLevel; + } + + protected void renderColumnSelects(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + int currentClauseLevel = clauseLevel; + char separator = ' '; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + sqlAppender.appendSql( separator ); + if ( columnDefinition instanceof JsonTableValueColumnDefinition valueColumnDefinition ) { + if ( valueColumnDefinition.errorBehavior() != null && valueColumnDefinition.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause for value within json_table() on SQL server" ); + } + sqlAppender.appendSql( 't' ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( valueColumnDefinition.name() ); + // todo: empty behavior? + } + else if ( columnDefinition instanceof JsonTableQueryColumnDefinition queryColumnDefinition ) { + if ( queryColumnDefinition.errorBehavior() != null && queryColumnDefinition.errorBehavior() != JsonQueryErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause for query within json_table() on SQL server" ); + } + if ( queryColumnDefinition.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_ARRAY + || queryColumnDefinition.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sqlAppender.appendSql( "coalesce(" ); + } + // SQL Server only supports no wildcard in JSON paths, so wrapper can be added statically + if ( queryColumnDefinition.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "'['+" ); + } + sqlAppender.appendSql( 't' ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( queryColumnDefinition.name() ); + if ( queryColumnDefinition.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "+']'" ); + } + if ( queryColumnDefinition.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_ARRAY ) { + sqlAppender.appendSql( ",'[]')" ); + } + else if ( queryColumnDefinition.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sqlAppender.appendSql( ",'{}')" ); + } + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( queryColumnDefinition.name() ); + } + else if ( columnDefinition instanceof JsonTableOrdinalityColumnDefinition ordinalityColumnDefinition ) { + sqlAppender.appendSql( "row_number() over (order by (select null)) " ); + sqlAppender.appendSql( ordinalityColumnDefinition.name() ); + } + else if ( columnDefinition instanceof JsonTableExistsColumnDefinition existsColumnDefinition ) { + if ( existsColumnDefinition.errorBehavior() == JsonExistsErrorBehavior.FALSE ) { + throw new QueryException( "Can't emulate exists false on error for json_table() on SQL Server" ); + } + final String jsonPath = existsColumnDefinition.jsonPath() == null + ? "$." + existsColumnDefinition.name() + : existsColumnDefinition.jsonPath(); + final List pathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + final JsonPathHelper.JsonPathElement lastPathElement = pathElements.get( pathElements.size() - 1 ); + final String prefix = JsonPathHelper.toJsonPath( pathElements, 0, pathElements.size() - 1 ); + final String terminalKey; + if ( lastPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) { + terminalKey = String.valueOf( indexAccess.index() ); + } + else { + assert lastPathElement instanceof JsonPathHelper.JsonAttribute; + terminalKey = ( (JsonPathHelper.JsonAttribute) lastPathElement ).attribute(); + } + + sqlAppender.appendSql( "coalesce((select 1 from openjson(t" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( "." ); + sqlAppender.appendSql( existsColumnDefinition.name() ); + sqlAppender.appendSql( ',' ); + sqlAppender.appendSingleQuoteEscapedString( prefix ); + sqlAppender.appendSql( ") t where t.[key]=" ); + sqlAppender.appendSingleQuoteEscapedString( terminalKey ); + sqlAppender.appendSql( "),0) " ); + sqlAppender.appendSql( existsColumnDefinition.name() ); + } + else { + final JsonTableNestedColumnDefinition nestedColumnDefinition = (JsonTableNestedColumnDefinition) columnDefinition; + // Select all the columns of a directly nested column definition + sqlAppender.appendSql( "t" ); + sqlAppender.appendSql( currentClauseLevel + 1 ); + sqlAppender.appendSql( ".*" ); + currentClauseLevel += 1 + countNestedColumnDefinitions( nestedColumnDefinition.columns() ); + } + separator = ','; + } + } + + protected void renderColumnDefinitions(SqlAppender sqlAppender, JsonTableColumnsClause jsonTableColumnsClause, int clauseLevel, SqlAstTranslator walker) { + int currentClauseLevel = clauseLevel; + String separator = " with ("; + for ( JsonTableColumnDefinition columnDefinition : jsonTableColumnsClause.getColumnDefinitions() ) { + if ( columnDefinition instanceof JsonTableOrdinalityColumnDefinition ) { + // This is implemented differently, so don't render or change the separator + continue; + } + sqlAppender.appendSql( separator ); + if ( columnDefinition instanceof JsonTableQueryColumnDefinition definition ) { + renderJsonQueryColumnDefinition( sqlAppender, definition, clauseLevel, walker ); + } + else if ( columnDefinition instanceof JsonTableValueColumnDefinition definition ) { + renderJsonValueColumnDefinition( sqlAppender, definition, clauseLevel, walker ); + } + else if ( columnDefinition instanceof JsonTableExistsColumnDefinition definition ) { + renderJsonExistsColumnDefinition( sqlAppender, definition, clauseLevel, walker ); + } + else { + final JsonTableNestedColumnDefinition nestedColumnDefinition = (JsonTableNestedColumnDefinition) columnDefinition; + currentClauseLevel = renderJsonNestedColumnDefinition( sqlAppender, nestedColumnDefinition, currentClauseLevel + 1, walker ); + } + separator = ","; + } + if ( ",".equals( separator ) ) { + sqlAppender.appendSql( ')' ); + } + } + + @Override + protected void renderColumnPath(String name, @Nullable String jsonPath, SqlAppender sqlAppender, SqlAstTranslator walker) { + if ( jsonPath != null ) { + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + } + } + + @Override + protected int renderJsonNestedColumnDefinition(SqlAppender sqlAppender, JsonTableNestedColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( "nested_" ); + sqlAppender.appendSql( clauseLevel ); + sqlAppender.appendSql( "_ nvarchar(max) " ); + // Strip off the array spread operator since SQL Server doesn't support that + final String jsonPath = definition.jsonPath().endsWith( "[*]" ) + ? definition.jsonPath().substring( 0, definition.jsonPath().length() - 3 ) + : definition.jsonPath(); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( " as json" ); + return clauseLevel + countNestedColumnDefinitions( definition.columns() ); + } + + @Override + protected void renderJsonQueryColumnDefinition(SqlAppender sqlAppender, JsonTableQueryColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( " nvarchar(max)" ); + renderColumnPath( definition.name(), definition.jsonPath(), sqlAppender, walker ); + sqlAppender.appendSql( " as json" ); + } + + @Override + protected void renderJsonValueColumnDefinition(SqlAppender sqlAppender, JsonTableValueColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( ' ' ); + sqlAppender.appendSql( determineColumnType( definition.type(), walker ) ); + renderColumnPath( definition.name(), definition.jsonPath(), sqlAppender, walker ); + } + + @Override + protected void renderJsonExistsColumnDefinition(SqlAppender sqlAppender, JsonTableExistsColumnDefinition definition, int clauseLevel, SqlAstTranslator walker) { + sqlAppender.appendSql( definition.name() ); + sqlAppender.appendSql( " nvarchar(max) '$' as json" ); + } +} 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 718d602da5c5..75cf134c9449 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 @@ -237,8 +237,9 @@ private EmbeddableMappingTypeImpl( null, creationContext ); + final int defaultSqlTypeCode = aggregateMapping.getJdbcMapping().getJdbcType().getDefaultSqlTypeCode(); final AggregateSupport aggregateSupport = dialect.getAggregateSupport(); - final int sqlTypeCode = aggregateColumn.getSqlTypeCode(); + final int sqlTypeCode = defaultSqlTypeCode == ARRAY ? aggregateColumn.getTypeCode() : defaultSqlTypeCode; this.aggregateMappingRequiresColumnWriter = aggregateSupport .requiresAggregateCustomWriteExpressionRenderer( sqlTypeCode ); this.preferSelectAggregateMapping = aggregateSupport.preferSelectAggregateMapping( sqlTypeCode ); 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 49148d28ed96..589fbba45dd1 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 @@ -95,6 +95,14 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { JpaExpression cast(JpaExpression expression, Class castTargetJavaType); + JpaExpression cast(JpaExpression expression, JpaCastTarget castTarget); + + JpaCastTarget castTarget(Class castTargetJavaType); + + JpaCastTarget castTarget(Class castTargetJavaType, long length); + + JpaCastTarget castTarget(Class castTargetJavaType, int precision, int scale); + JpaPredicate wrap(Expression expression); @SuppressWarnings("unchecked") @@ -4425,6 +4433,36 @@ JpaExpression xmlagg( @Incubating JpaSetReturningFunction generateTimeSeries(Expression start, Expression stop, Expression step); + /** + * Creates a {@code json_table} function expression to generate rows from JSON array elements. + * + * @since 7.0 + * @see JpaSelectCriteria#from(JpaSetReturningFunction) + * @see JpaFrom#join(JpaSetReturningFunction) + */ + @Incubating + JpaJsonTableFunction jsonTable(Expression jsonDocument); + + /** + * Creates a {@code json_table} function expression to generate rows from JSON array elements. + * + * @since 7.0 + * @see JpaSelectCriteria#from(JpaSetReturningFunction) + * @see JpaFrom#join(JpaSetReturningFunction) + */ + @Incubating + JpaJsonTableFunction jsonTable(Expression jsonDocument, String jsonPath); + + /** + * Creates a {@code json_table} function expression to generate rows from JSON array elements. + * + * @since 7.0 + * @see JpaSelectCriteria#from(JpaSetReturningFunction) + * @see JpaFrom#join(JpaSetReturningFunction) + */ + @Incubating + JpaJsonTableFunction jsonTable(Expression jsonDocument, Expression jsonPath); + @Override JpaPredicate and(List restrictions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCastTarget.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCastTarget.java new file mode 100644 index 000000000000..d0e91a0870b3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaCastTarget.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import jakarta.persistence.metamodel.Type; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.Incubating; + +/** + * The target for cast. + * + * @since 7.0 + */ +@Incubating +public interface JpaCastTarget { + + /** + * Returns the JPA type for this cast target. + */ + Type getType(); + + /** + * Returns the specified length of the cast target or {@code null}. + */ + @Nullable Long getLength(); + + /** + * Returns the specified precision of the cast target or {@code null}. + */ + @Nullable Integer getPrecision(); + + /** + * Returns the specified scale of the cast target or {@code null}. + */ + @Nullable Integer getScale(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java index 11bf095bcb18..22406d205f2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java @@ -13,65 +13,21 @@ * @since 7.0 */ @Incubating -public interface JpaJsonExistsExpression extends JpaExpression { - /** - * Get the {@link ErrorBehavior} of this json value expression. - * - * @return the error behavior - */ - ErrorBehavior getErrorBehavior(); +public interface JpaJsonExistsExpression extends JpaExpression, JpaJsonExistsNode { /** - * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json exists expression. + * Passes the given {@link Expression} as value for the parameter with the given name in the JSON path. * * @return {@code this} for method chaining */ + JpaJsonExistsExpression passing(String parameterName, Expression expression); + + @Override JpaJsonExistsExpression unspecifiedOnError(); - /** - * Sets the {@link ErrorBehavior#ERROR} for this json exists expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonExistsExpression errorOnError(); - /** - * Sets the {@link ErrorBehavior#TRUE} for this json exists expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonExistsExpression trueOnError(); - /** - * Sets the {@link ErrorBehavior#FALSE} for this json exists expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonExistsExpression falseOnError(); - - /** - * Passes the given {@link Expression} as value for the parameter with the given name in the JSON path. - * - * @return {@code this} for method chaining - */ - JpaJsonExistsExpression passing(String parameterName, Expression expression); - - /** - * The behavior of the json exists expression when a JSON processing error occurs. - */ - enum ErrorBehavior { - /** - * SQL/JDBC error should be raised. - */ - ERROR, - /** - * {@code true} should be returned on error. - */ - TRUE, - /** - * {@code false} should be returned on error. - */ - FALSE, - /** - * Unspecified behavior i.e. the default database behavior. - */ - UNSPECIFIED - } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsNode.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsNode.java new file mode 100644 index 000000000000..8f9db6641b19 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsNode.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +/** + * The base for {@code json_exists} function nodes. + * @since 7.0 + */ +@Incubating +public interface JpaJsonExistsNode { + /** + * Get the {@link ErrorBehavior} of this json exists expression. + * + * @return the error behavior + */ + ErrorBehavior getErrorBehavior(); + + /** + * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json exists expression. + * + * @return {@code this} for method chaining + */ + JpaJsonExistsNode unspecifiedOnError(); + /** + * Sets the {@link ErrorBehavior#ERROR} for this json exists expression. + * + * @return {@code this} for method chaining + */ + JpaJsonExistsNode errorOnError(); + /** + * Sets the {@link ErrorBehavior#TRUE} for this json exists expression. + * + * @return {@code this} for method chaining + */ + JpaJsonExistsNode trueOnError(); + /** + * Sets the {@link ErrorBehavior#FALSE} for this json exists expression. + * + * @return {@code this} for method chaining + */ + JpaJsonExistsNode falseOnError(); + + /** + * The behavior of the json exists expression when a JSON processing error occurs. + */ + enum ErrorBehavior { + /** + * SQL/JDBC error should be raised. + */ + ERROR, + /** + * {@code true} should be returned on error. + */ + TRUE, + /** + * {@code false} should be returned on error. + */ + FALSE, + /** + * Unspecified behavior i.e. the default database behavior. + */ + UNSPECIFIED + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java index 7b06de2d6672..12ab00dc73e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java @@ -13,190 +13,44 @@ * @since 7.0 */ @Incubating -public interface JpaJsonQueryExpression extends JpaExpression { - /** - * Get the {@link WrapMode} of this json query expression. - * - * @return the wrap mode - */ - WrapMode getWrapMode(); - /** - * Get the {@link ErrorBehavior} of this json query expression. - * - * @return the error behavior - */ - ErrorBehavior getErrorBehavior(); - - /** - * Get the {@link EmptyBehavior} of this json query expression. - * - * @return the empty behavior - */ - EmptyBehavior getEmptyBehavior(); +public interface JpaJsonQueryExpression extends JpaExpression, JpaJsonQueryNode { /** - * Sets the {@link WrapMode#WITHOUT_WRAPPER} for this json query expression. + * Passes the given {@link Expression} as value for the parameter with the given name in the JSON path. * * @return {@code this} for method chaining */ + JpaJsonQueryExpression passing(String parameterName, Expression expression); + + @Override JpaJsonQueryExpression withoutWrapper(); - /** - * Sets the {@link WrapMode#WITH_WRAPPER} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression withWrapper(); - /** - * Sets the {@link WrapMode#WITH_CONDITIONAL_WRAPPER} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression withConditionalWrapper(); - /** - * Sets the {@link WrapMode#UNSPECIFIED} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression unspecifiedWrapper(); - /** - * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression unspecifiedOnError(); - /** - * Sets the {@link ErrorBehavior#ERROR} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression errorOnError(); - /** - * Sets the {@link ErrorBehavior#NULL} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression nullOnError(); - /** - * Sets the {@link ErrorBehavior#EMPTY_ARRAY} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression emptyArrayOnError(); - /** - * Sets the {@link ErrorBehavior#EMPTY_OBJECT} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression emptyObjectOnError(); - /** - * Sets the {@link EmptyBehavior#UNSPECIFIED} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression unspecifiedOnEmpty(); - /** - * Sets the {@link EmptyBehavior#ERROR} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression errorOnEmpty(); - /** - * Sets the {@link EmptyBehavior#NULL} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression nullOnEmpty(); - /** - * Sets the {@link EmptyBehavior#EMPTY_ARRAY} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression emptyArrayOnEmpty(); - /** - * Sets the {@link EmptyBehavior#EMPTY_OBJECT} for this json query expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonQueryExpression emptyObjectOnEmpty(); - /** - * Passes the given {@link Expression} as value for the parameter with the given name in the JSON path. - * - * @return {@code this} for method chaining - */ - JpaJsonQueryExpression passing(String parameterName, Expression expression); - - /** - * The kind of wrapping to apply to the results of the query. - */ - enum WrapMode { - /** - * Omit the array wrapper in the result. - */ - WITHOUT_WRAPPER, - /** - * Force the array wrapper in the result. - */ - WITH_WRAPPER, - /** - * Only use an array wrapper in the result if there is more than one result. - */ - WITH_CONDITIONAL_WRAPPER, - /** - * Unspecified behavior i.e. the default database behavior. - */ - UNSPECIFIED - } - /** - * The behavior of the json query expression when a JSON processing error occurs. - */ - enum ErrorBehavior { - /** - * SQL/JDBC error should be raised. - */ - ERROR, - /** - * {@code null} should be returned. - */ - NULL, - /** - * An empty array should be returned. - */ - EMPTY_ARRAY, - /** - * An empty object should be returned. - */ - EMPTY_OBJECT, - /** - * Unspecified behavior i.e. the default database behavior. - */ - UNSPECIFIED - } - /** - * The behavior of the json query expression when a JSON path does not resolve for a JSON document. - */ - enum EmptyBehavior { - /** - * SQL/JDBC error should be raised. - */ - ERROR, - /** - * {@code null} should be returned. - */ - NULL, - /** - * An empty array should be returned. - */ - EMPTY_ARRAY, - /** - * An empty object should be returned. - */ - EMPTY_OBJECT, - /** - * Unspecified behavior i.e. the default database behavior. - */ - UNSPECIFIED - } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryNode.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryNode.java new file mode 100644 index 000000000000..27beaf946bbe --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryNode.java @@ -0,0 +1,193 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +/** + * The base for {@code json_query} function nodes. + * @since 7.0 + */ +@Incubating +public interface JpaJsonQueryNode { + /** + * Get the {@link WrapMode} of this json query expression. + * + * @return the wrap mode + */ + WrapMode getWrapMode(); + /** + * Get the {@link ErrorBehavior} of this json query expression. + * + * @return the error behavior + */ + ErrorBehavior getErrorBehavior(); + + /** + * Get the {@link EmptyBehavior} of this json query expression. + * + * @return the empty behavior + */ + EmptyBehavior getEmptyBehavior(); + + /** + * Sets the {@link WrapMode#WITHOUT_WRAPPER} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode withoutWrapper(); + /** + * Sets the {@link WrapMode#WITH_WRAPPER} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode withWrapper(); + /** + * Sets the {@link WrapMode#WITH_CONDITIONAL_WRAPPER} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode withConditionalWrapper(); + /** + * Sets the {@link WrapMode#UNSPECIFIED} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode unspecifiedWrapper(); + + /** + * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode unspecifiedOnError(); + /** + * Sets the {@link ErrorBehavior#ERROR} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode errorOnError(); + /** + * Sets the {@link ErrorBehavior#NULL} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode nullOnError(); + /** + * Sets the {@link ErrorBehavior#EMPTY_ARRAY} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode emptyArrayOnError(); + /** + * Sets the {@link ErrorBehavior#EMPTY_OBJECT} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode emptyObjectOnError(); + + /** + * Sets the {@link EmptyBehavior#UNSPECIFIED} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode unspecifiedOnEmpty(); + /** + * Sets the {@link EmptyBehavior#ERROR} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode errorOnEmpty(); + /** + * Sets the {@link EmptyBehavior#NULL} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode nullOnEmpty(); + /** + * Sets the {@link EmptyBehavior#EMPTY_ARRAY} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode emptyArrayOnEmpty(); + /** + * Sets the {@link EmptyBehavior#EMPTY_OBJECT} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryNode emptyObjectOnEmpty(); + + /** + * The kind of wrapping to apply to the results of the query. + */ + enum WrapMode { + /** + * Omit the array wrapper in the result. + */ + WITHOUT_WRAPPER, + /** + * Force the array wrapper in the result. + */ + WITH_WRAPPER, + /** + * Only use an array wrapper in the result if there is more than one result. + */ + WITH_CONDITIONAL_WRAPPER, + /** + * Unspecified behavior i.e. the default database behavior. + */ + UNSPECIFIED + } + /** + * The behavior of the json query expression when a JSON processing error occurs. + */ + enum ErrorBehavior { + /** + * SQL/JDBC error should be raised. + */ + ERROR, + /** + * {@code null} should be returned. + */ + NULL, + /** + * An empty array should be returned. + */ + EMPTY_ARRAY, + /** + * An empty object should be returned. + */ + EMPTY_OBJECT, + /** + * Unspecified behavior i.e. the default database behavior. + */ + UNSPECIFIED + } + /** + * The behavior of the json query expression when a JSON path does not resolve for a JSON document. + */ + enum EmptyBehavior { + /** + * SQL/JDBC error should be raised. + */ + ERROR, + /** + * {@code null} should be returned. + */ + NULL, + /** + * An empty array should be returned. + */ + EMPTY_ARRAY, + /** + * An empty object should be returned. + */ + EMPTY_OBJECT, + /** + * Unspecified behavior i.e. the default database behavior. + */ + UNSPECIFIED + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableColumnsNode.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableColumnsNode.java new file mode 100644 index 000000000000..024c6090909e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableColumnsNode.java @@ -0,0 +1,88 @@ +/* + * 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 special expression for the definition of columns within the {@code json_table} function. + * @since 7.0 + */ +@Incubating +public interface JpaJsonTableColumnsNode { + + /** + * Like {@link #existsColumn(String, String)}, but uses the column name as JSON path expression. + * + * @return The {@link JpaJsonExistsNode} for the column + */ + JpaJsonExistsNode existsColumn(String columnName); + + /** + * Defines a boolean column on the result type with the given name for which the value can be obtained + * by invoking {@code json_exists} with the given JSON path. + * + * @return The {@link JpaJsonExistsNode} for the column + */ + JpaJsonExistsNode existsColumn(String columnName, String jsonPath); + + /** + * Like {@link #queryColumn(String, String)}, but uses the column name as JSON path expression. + * + * @return The {@link JpaJsonQueryNode} for the column + */ + JpaJsonQueryNode queryColumn(String columnName); + + /** + * Defines a string column on the result type with the given name for which the value can be obtained + * by invoking {@code json_query} with the given JSON path. + * + * @return The {@link JpaJsonQueryNode} for the column + */ + JpaJsonQueryNode queryColumn(String columnName, String jsonPath); + + /** + * Like {@link #valueColumn(String, Class, String)} but uses the column name as JSON path expression. + * + * @return The {@link JpaJsonValueNode} for the column + */ + JpaJsonValueNode valueColumn(String columnName, Class type); + + /** + * Defines a column on the result type with the given name and type for which the value can be obtained by the given JSON path expression. + * + * @return The {@link JpaJsonValueNode} for the column + */ + JpaJsonValueNode valueColumn(String columnName, Class type, String jsonPath); + + /** + * Like {@link #valueColumn(String, Class, String)} but uses the column name as JSON path expression. + * + * @return The {@link JpaJsonValueNode} for the column + */ + JpaJsonValueNode valueColumn(String columnName, JpaCastTarget type); + + /** + * Defines a column on the result type with the given name and type for which the value can be obtained by the given JSON path expression. + * + * @return The {@link JpaJsonValueNode} for the column + */ + JpaJsonValueNode valueColumn(String columnName, JpaCastTarget type, String jsonPath); + + /** + * Defines nested columns that are accessible by the given JSON path. + * + * @return a new columns node for the nested JSON path + */ + JpaJsonTableColumnsNode nested(String jsonPath); + + /** + * Defines a long typed column on the result type with the given name which is set to the ordinality i.e. + * the 1-based position of the processed element. Ordinality starts again at 1 within nested paths. + * + * @return {@code this} for method chaining + */ + JpaJsonTableColumnsNode ordinalityColumn(String columnName); +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableFunction.java new file mode 100644 index 000000000000..a482a44525f9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonTableFunction.java @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import jakarta.persistence.criteria.Expression; +import org.hibernate.Incubating; + +/** + * A special expression for the {@code json_table} function. + * @since 7.0 + */ +@Incubating +public interface JpaJsonTableFunction extends JpaJsonTableColumnsNode { + + /** + * Passes the given {@link Expression} as value for the parameter with the given name in the JSON path. + * + * @return {@code this} for method chaining + */ + JpaJsonTableFunction passing(String parameterName, Expression expression); + + /** + * Get the {@link ErrorBehavior} of this json table expression. + * + * @return the error behavior + */ + ErrorBehavior getErrorBehavior(); + + /** + * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json table expression. + * + * @return {@code this} for method chaining + */ + JpaJsonTableFunction unspecifiedOnError(); + /** + * Sets the {@link ErrorBehavior#ERROR} for this json table expression. + * + * @return {@code this} for method chaining + */ + JpaJsonTableFunction errorOnError(); + /** + * Sets the {@link ErrorBehavior#NULL} for this json table expression. + * + * @return {@code this} for method chaining + */ + JpaJsonTableFunction nullOnError(); + + @Override + JpaJsonTableFunction ordinalityColumn(String columnName); + + /** + * The behavior of the json exists expression when a JSON processing error occurs. + */ + enum ErrorBehavior { + /** + * SQL/JDBC error should be raised. + */ + ERROR, + /** + * {@code null} should be returned on error. + */ + NULL, + /** + * Unspecified behavior i.e. the default database behavior. + */ + UNSPECIFIED + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java index 74e836c95f15..fbad91959744 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java @@ -7,141 +7,37 @@ import org.hibernate.Incubating; import jakarta.persistence.criteria.Expression; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A special expression for the {@code json_value} function. * @since 7.0 */ @Incubating -public interface JpaJsonValueExpression extends JpaExpression { - /** - * Get the {@link ErrorBehavior} of this json value expression. - * - * @return the error behavior - */ - ErrorBehavior getErrorBehavior(); - - /** - * Get the {@link EmptyBehavior} of this json value expression. - * - * @return the empty behavior - */ - EmptyBehavior getEmptyBehavior(); - - /** - * Get the {@link JpaExpression} that is returned on a json processing error. - * Returns {@code null} if {@link #getErrorBehavior()} is not {@link ErrorBehavior#DEFAULT}. - * - * @return the value to return on a json processing error - */ - @Nullable JpaExpression getErrorDefault(); - - /** - * Get the {@link JpaExpression} that is returned when the JSON path does not resolve for a JSON document. - * Returns {@code null} if {@link #getEmptyBehavior()} is not {@link EmptyBehavior#DEFAULT}. - * - * @return the value to return on a json processing error - */ - @Nullable JpaExpression getEmptyDefault(); +public interface JpaJsonValueExpression extends JpaExpression, JpaJsonValueNode { /** - * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json value expression. + * Passes the given {@link Expression} as value for the parameter with the given name in the JSON path. * * @return {@code this} for method chaining */ + JpaJsonValueExpression passing(String parameterName, Expression expression); + + @Override JpaJsonValueExpression unspecifiedOnError(); - /** - * Sets the {@link ErrorBehavior#ERROR} for this json value expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonValueExpression errorOnError(); - /** - * Sets the {@link ErrorBehavior#NULL} for this json value expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonValueExpression nullOnError(); - /** - * Sets the {@link ErrorBehavior#DEFAULT} for this json value expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonValueExpression defaultOnError(Expression expression); - /** - * Sets the {@link EmptyBehavior#UNSPECIFIED} for this json value expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonValueExpression unspecifiedOnEmpty(); - /** - * Sets the {@link EmptyBehavior#ERROR} for this json value expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonValueExpression errorOnEmpty(); - /** - * Sets the {@link EmptyBehavior#NULL} for this json value expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonValueExpression nullOnEmpty(); - /** - * Sets the {@link EmptyBehavior#DEFAULT} for this json value expression. - * - * @return {@code this} for method chaining - */ + @Override JpaJsonValueExpression defaultOnEmpty(Expression expression); - /** - * Passes the given {@link Expression} as value for the parameter with the given name in the JSON path. - * - * @return {@code this} for method chaining - */ - JpaJsonValueExpression passing(String parameterName, Expression expression); - - /** - * The behavior of the json value expression when a JSON processing error occurs. - */ - enum ErrorBehavior { - /** - * SQL/JDBC error should be raised. - */ - ERROR, - /** - * {@code null} should be returned. - */ - NULL, - /** - * The {@link JpaJsonValueExpression#getErrorDefault()} value should be returned. - */ - DEFAULT, - /** - * Unspecified behavior i.e. the default database behavior. - */ - UNSPECIFIED - } - /** - * The behavior of the json value expression when a JSON path does not resolve for a JSON document. - */ - enum EmptyBehavior { - /** - * SQL/JDBC error should be raised. - */ - ERROR, - /** - * {@code null} should be returned. - */ - NULL, - /** - * The {@link JpaJsonValueExpression#getEmptyDefault()} value should be returned. - */ - DEFAULT, - /** - * Unspecified behavior i.e. the default database behavior. - */ - UNSPECIFIED - } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueNode.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueNode.java new file mode 100644 index 000000000000..d010b601afca --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueNode.java @@ -0,0 +1,139 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.criteria; + +import jakarta.persistence.criteria.Expression; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.Incubating; + +/** + * The base for {@code json_value} function nodes. + * @since 7.0 + */ +@Incubating +public interface JpaJsonValueNode { + /** + * Get the {@link ErrorBehavior} of this json value expression. + * + * @return the error behavior + */ + ErrorBehavior getErrorBehavior(); + + /** + * Get the {@link EmptyBehavior} of this json value expression. + * + * @return the empty behavior + */ + EmptyBehavior getEmptyBehavior(); + + /** + * Get the {@link JpaExpression} that is returned on a json processing error. + * Returns {@code null} if {@link #getErrorBehavior()} is not {@link ErrorBehavior#DEFAULT}. + * + * @return the value to return on a json processing error + */ + @Nullable JpaExpression getErrorDefault(); + + /** + * Get the {@link JpaExpression} that is returned when the JSON path does not resolve for a JSON document. + * Returns {@code null} if {@link #getEmptyBehavior()} is not {@link EmptyBehavior#DEFAULT}. + * + * @return the value to return on a json processing error + */ + @Nullable JpaExpression getEmptyDefault(); + + /** + * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode unspecifiedOnError(); + /** + * Sets the {@link ErrorBehavior#ERROR} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode errorOnError(); + /** + * Sets the {@link ErrorBehavior#NULL} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode nullOnError(); + /** + * Sets the {@link ErrorBehavior#DEFAULT} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode defaultOnError(Expression expression); + + /** + * Sets the {@link EmptyBehavior#UNSPECIFIED} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode unspecifiedOnEmpty(); + /** + * Sets the {@link EmptyBehavior#ERROR} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode errorOnEmpty(); + /** + * Sets the {@link EmptyBehavior#NULL} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode nullOnEmpty(); + /** + * Sets the {@link EmptyBehavior#DEFAULT} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueNode defaultOnEmpty(Expression expression); + + /** + * The behavior of the json value expression when a JSON processing error occurs. + */ + enum ErrorBehavior { + /** + * SQL/JDBC error should be raised. + */ + ERROR, + /** + * {@code null} should be returned. + */ + NULL, + /** + * The {@link JpaJsonValueNode#getErrorDefault()} value should be returned. + */ + DEFAULT, + /** + * Unspecified behavior i.e. the default database behavior. + */ + UNSPECIFIED + } + /** + * The behavior of the json value expression when a JSON path does not resolve for a JSON document. + */ + enum EmptyBehavior { + /** + * SQL/JDBC error should be raised. + */ + ERROR, + /** + * {@code null} should be returned. + */ + NULL, + /** + * The {@link JpaJsonValueNode#getEmptyDefault()} value should be returned. + */ + DEFAULT, + /** + * Unspecified behavior i.e. the default database behavior. + */ + UNSPECIFIED + } +} 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 ed4f6da64570..f76a7d531fd4 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 @@ -25,41 +25,7 @@ import org.hibernate.Incubating; import org.hibernate.query.NullPrecedence; import org.hibernate.query.SortDirection; -import org.hibernate.query.criteria.HibernateCriteriaBuilder; -import org.hibernate.query.criteria.JpaCoalesce; -import org.hibernate.query.criteria.JpaCollectionJoin; -import org.hibernate.query.criteria.JpaCompoundSelection; -import org.hibernate.query.criteria.JpaCriteriaDelete; -import org.hibernate.query.criteria.JpaCriteriaInsertSelect; -import org.hibernate.query.criteria.JpaCriteriaInsertValues; -import org.hibernate.query.criteria.JpaCriteriaQuery; -import org.hibernate.query.criteria.JpaCriteriaUpdate; -import org.hibernate.query.criteria.JpaCteCriteriaAttribute; -import org.hibernate.query.criteria.JpaExpression; -import org.hibernate.query.criteria.JpaFunction; -import org.hibernate.query.criteria.JpaInPredicate; -import org.hibernate.query.criteria.JpaJoin; -import org.hibernate.query.criteria.JpaJsonExistsExpression; -import org.hibernate.query.criteria.JpaJsonQueryExpression; -import org.hibernate.query.criteria.JpaJsonValueExpression; -import org.hibernate.query.criteria.JpaListJoin; -import org.hibernate.query.criteria.JpaMapJoin; -import org.hibernate.query.criteria.JpaOrder; -import org.hibernate.query.criteria.JpaParameterExpression; -import org.hibernate.query.criteria.JpaPath; -import org.hibernate.query.criteria.JpaPredicate; -import org.hibernate.query.criteria.JpaRoot; -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; -import org.hibernate.query.criteria.JpaWindow; -import org.hibernate.query.criteria.JpaWindowFrame; -import org.hibernate.query.criteria.JpaXmlElementExpression; +import org.hibernate.query.criteria.*; import org.hibernate.query.common.TemporalUnit; import jakarta.persistence.Tuple; @@ -101,6 +67,26 @@ public JpaExpression cast(JpaExpression expression, Class castTa return criteriaBuilder.cast( expression, castTargetJavaType ); } + @Override + public JpaExpression cast(JpaExpression expression, JpaCastTarget castTarget) { + return criteriaBuilder.cast( expression, castTarget ); + } + + @Override + public JpaCastTarget castTarget(Class castTargetJavaType) { + return criteriaBuilder.castTarget( castTargetJavaType ); + } + + @Override + public JpaCastTarget castTarget(Class castTargetJavaType, long length) { + return criteriaBuilder.castTarget( castTargetJavaType, length ); + } + + @Override + public JpaCastTarget castTarget(Class castTargetJavaType, int precision, int scale) { + return criteriaBuilder.castTarget( castTargetJavaType, precision, scale ); + } + @Override public JpaPredicate wrap(Expression expression) { return criteriaBuilder.wrap( expression ); @@ -3874,4 +3860,22 @@ public JpaSetReturningFunction generateTimeSeries(E star public JpaSetReturningFunction generateTimeSeries(Expression start, Expression stop, Expression step) { return criteriaBuilder.generateTimeSeries( start, stop, step ); } + + @Incubating + @Override + public JpaJsonTableFunction jsonTable(Expression jsonDocument) { + return criteriaBuilder.jsonTable( jsonDocument ); + } + + @Incubating + @Override + public JpaJsonTableFunction jsonTable(Expression jsonDocument, String jsonPath) { + return criteriaBuilder.jsonTable( jsonDocument, jsonPath ); + } + + @Incubating + @Override + public JpaJsonTableFunction jsonTable(Expression jsonDocument, Expression jsonPath) { + return criteriaBuilder.jsonTable( jsonDocument, jsonPath ); + } } 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 414b15e49e70..fffe6c51b379 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 @@ -63,6 +63,10 @@ import org.hibernate.query.criteria.JpaCteCriteria; import org.hibernate.query.criteria.JpaCteCriteriaAttribute; import org.hibernate.query.criteria.JpaCteCriteriaType; +import org.hibernate.query.criteria.JpaJsonExistsNode; +import org.hibernate.query.criteria.JpaJsonQueryNode; +import org.hibernate.query.criteria.JpaJsonTableColumnsNode; +import org.hibernate.query.criteria.JpaJsonValueNode; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.criteria.JpaSearchOrder; import org.hibernate.query.derived.AnonymousTupleType; @@ -129,48 +133,7 @@ import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPluralValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmPolymorphicRootDescriptor; -import org.hibernate.query.sqm.tree.expression.AbstractSqmParameter; -import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; -import org.hibernate.query.sqm.tree.expression.SqmAny; -import org.hibernate.query.sqm.tree.expression.SqmAnyDiscriminatorValue; -import org.hibernate.query.sqm.tree.expression.SqmBinaryArithmetic; -import org.hibernate.query.sqm.tree.expression.SqmByUnit; -import org.hibernate.query.sqm.tree.expression.SqmCaseSearched; -import org.hibernate.query.sqm.tree.expression.SqmCaseSimple; -import org.hibernate.query.sqm.tree.expression.SqmCastTarget; -import org.hibernate.query.sqm.tree.expression.SqmCollation; -import org.hibernate.query.sqm.tree.expression.SqmCollectionSize; -import org.hibernate.query.sqm.tree.expression.SqmDistinct; -import org.hibernate.query.sqm.tree.expression.SqmDurationUnit; -import org.hibernate.query.sqm.tree.expression.SqmEvery; -import org.hibernate.query.sqm.tree.expression.SqmExpression; -import org.hibernate.query.sqm.tree.expression.SqmExtractUnit; -import org.hibernate.query.sqm.tree.expression.SqmFormat; -import org.hibernate.query.sqm.tree.expression.SqmFunction; -import org.hibernate.query.sqm.tree.expression.SqmHqlNumericLiteral; -import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression; -import org.hibernate.query.sqm.tree.expression.SqmJsonNullBehavior; -import org.hibernate.query.sqm.tree.expression.SqmJsonObjectAggUniqueKeysBehavior; -import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; -import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; -import org.hibernate.query.sqm.tree.expression.SqmLiteral; -import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; -import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; -import org.hibernate.query.sqm.tree.expression.SqmNamedExpression; -import org.hibernate.query.sqm.tree.expression.SqmNamedParameter; -import org.hibernate.query.sqm.tree.expression.SqmOver; -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; -import org.hibernate.query.sqm.tree.expression.SqmTrimSpecification; -import org.hibernate.query.sqm.tree.expression.SqmTuple; -import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; -import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; +import org.hibernate.query.sqm.tree.expression.*; import org.hibernate.query.sqm.tree.from.SqmAttributeJoin; import org.hibernate.query.sqm.tree.from.SqmCrossJoin; import org.hibernate.query.sqm.tree.from.SqmCteJoin; @@ -2771,7 +2734,23 @@ public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContex null, creationContext.getQueryEngine() ); - for ( HqlParser.JsonValueOnErrorOrEmptyClauseContext subCtx : ctx.jsonValueOnErrorOrEmptyClause() ) { + visitJsonValueOnErrorOrEmptyClause( jsonValue, ctx.jsonValueOnErrorOrEmptyClause() ); + final HqlParser.JsonPassingClauseContext passingClause = ctx.jsonPassingClause(); + if ( passingClause != null ) { + final List expressionContexts = passingClause.expressionOrPredicate(); + final List identifierContexts = passingClause.identifier(); + for ( int i = 0; i < expressionContexts.size(); i++ ) { + jsonValue.passing( + visitIdentifier( identifierContexts.get( i ) ), + (SqmExpression) expressionContexts.get( i ).accept( this ) + ); + } + } + return jsonValue; + } + + private void visitJsonValueOnErrorOrEmptyClause(JpaJsonValueNode jsonValue, List errorOrEmptyClauseContexts) { + for ( HqlParser.JsonValueOnErrorOrEmptyClauseContext subCtx : errorOrEmptyClauseContexts ) { final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 ); final TerminalNode lastToken = (TerminalNode) subCtx.getChild( subCtx.getChildCount() - 1 ); if ( lastToken.getSymbol().getType() == HqlParser.ERROR ) { @@ -2791,18 +2770,6 @@ public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContex } } } - final HqlParser.JsonPassingClauseContext passingClause = ctx.jsonPassingClause(); - if ( passingClause != null ) { - final List expressionContexts = passingClause.expressionOrPredicate(); - final List identifierContexts = passingClause.identifier(); - for ( int i = 0; i < expressionContexts.size(); i++ ) { - jsonValue.passing( - visitIdentifier( identifierContexts.get( i ) ), - (SqmExpression) expressionContexts.get( i ).accept( this ) - ); - } - } - return jsonValue; } @Override @@ -2815,7 +2782,23 @@ public SqmExpression visitJsonQueryFunction(HqlParser.JsonQueryFunctionContex null, creationContext.getQueryEngine() ); - final HqlParser.JsonQueryWrapperClauseContext wrapperClause = ctx.jsonQueryWrapperClause(); + visitJsonQueryWrapperClause( jsonQuery, ctx.jsonQueryWrapperClause() ); + visitJsonQueryOnErrorOrEmptyClause( jsonQuery, ctx.jsonQueryOnErrorOrEmptyClause() ); + final HqlParser.JsonPassingClauseContext passingClause = ctx.jsonPassingClause(); + if ( passingClause != null ) { + final List expressionContexts = passingClause.expressionOrPredicate(); + final List identifierContexts = passingClause.identifier(); + for ( int i = 0; i < expressionContexts.size(); i++ ) { + jsonQuery.passing( + visitIdentifier( identifierContexts.get( i ) ), + (SqmExpression) expressionContexts.get( i ).accept( this ) + ); + } + } + return jsonQuery; + } + + private static void visitJsonQueryWrapperClause(JpaJsonQueryNode jsonQuery, HqlParser.JsonQueryWrapperClauseContext wrapperClause) { if ( wrapperClause != null ) { final TerminalNode firstToken = (TerminalNode) wrapperClause.getChild( 0 ); if ( firstToken.getSymbol().getType() == HqlParser.WITH ) { @@ -2831,7 +2814,10 @@ public SqmExpression visitJsonQueryFunction(HqlParser.JsonQueryFunctionContex jsonQuery.withoutWrapper(); } } - for ( HqlParser.JsonQueryOnErrorOrEmptyClauseContext subCtx : ctx.jsonQueryOnErrorOrEmptyClause() ) { + } + + private static void visitJsonQueryOnErrorOrEmptyClause(JpaJsonQueryNode jsonQuery, List jsonQueryOnErrorOrEmptyClauseContexts) { + for ( HqlParser.JsonQueryOnErrorOrEmptyClauseContext subCtx : jsonQueryOnErrorOrEmptyClauseContexts ) { final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 ); final TerminalNode lastToken = (TerminalNode) subCtx.getChild( subCtx.getChildCount() - 1 ); if ( lastToken.getSymbol().getType() == HqlParser.ERROR ) { @@ -2865,18 +2851,6 @@ public SqmExpression visitJsonQueryFunction(HqlParser.JsonQueryFunctionContex } } } - final HqlParser.JsonPassingClauseContext passingClause = ctx.jsonPassingClause(); - if ( passingClause != null ) { - final List expressionContexts = passingClause.expressionOrPredicate(); - final List identifierContexts = passingClause.identifier(); - for ( int i = 0; i < expressionContexts.size(); i++ ) { - jsonQuery.passing( - visitIdentifier( identifierContexts.get( i ) ), - (SqmExpression) expressionContexts.get( i ).accept( this ) - ); - } - } - return jsonQuery; } @Override @@ -3028,6 +3002,104 @@ public Object visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext ); } + @Override + public Object visitJsonTableFunction(HqlParser.JsonTableFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final List argumentsContexts = ctx.expression(); + final SqmExpression jsonDocument = (SqmExpression) argumentsContexts.get( 0 ).accept( this ); + final SqmJsonTableFunction jsonTable; + if ( argumentsContexts.size() == 1 ) { + jsonTable = creationContext.getNodeBuilder().jsonTable( jsonDocument ); + } + else { + //noinspection unchecked + final SqmExpression jsonPath = (SqmExpression) argumentsContexts.get( 1 ).accept( this ); + jsonTable = creationContext.getNodeBuilder().jsonTable( jsonDocument, jsonPath ); + } + final HqlParser.JsonPassingClauseContext passingClauseContext = ctx.jsonPassingClause(); + if ( passingClauseContext != null ) { + final List expressionContexts = passingClauseContext.expressionOrPredicate(); + final List identifierContexts = passingClauseContext.identifier(); + for ( int i = 0; i < expressionContexts.size(); i++ ) { + jsonTable.passing( + visitIdentifier( identifierContexts.get( i ) ), + (SqmExpression) expressionContexts.get( i ).accept( this ) + ); + } + } + visitColumns( jsonTable, ctx.jsonTableColumnsClause().jsonTableColumns().jsonTableColumn() ); + + final HqlParser.JsonTableErrorClauseContext errorClauseContext = ctx.jsonTableErrorClause(); + if ( errorClauseContext != null ) { + if ( ( (TerminalNode) errorClauseContext.getChild( 0 ) ).getSymbol().getType() == HqlParser.ERROR ) { + jsonTable.errorOnError(); + } + else { + jsonTable.nullOnError(); + } + } + return jsonTable; + } + + private void visitColumns(JpaJsonTableColumnsNode columnsNode, List columnContexts) { + for ( HqlParser.JsonTableColumnContext columnContext : columnContexts ) { + if ( columnContext instanceof HqlParser.JsonTableQueryColumnContext queryContext ) { + final String attributeName = visitIdentifier( queryContext.identifier() ); + final TerminalNode jsonPath = queryContext.STRING_LITERAL(); + final JpaJsonQueryNode queryNode; + if ( jsonPath == null ) { + queryNode = columnsNode.queryColumn( attributeName ); + } + else { + queryNode = columnsNode.queryColumn( attributeName, unquoteStringLiteral( jsonPath.getText() ) ); + } + visitJsonQueryOnErrorOrEmptyClause( queryNode, queryContext.jsonQueryOnErrorOrEmptyClause() ); + } + else if ( columnContext instanceof HqlParser.JsonTableValueColumnContext valueContext ) { + final String attributeName = visitIdentifier( valueContext.identifier() ); + final SqmCastTarget sqmCastTarget = visitCastTarget( valueContext.castTarget() ); + final TerminalNode jsonPath = valueContext.STRING_LITERAL(); + final JpaJsonValueNode valueNode; + if ( jsonPath == null ) { + valueNode = columnsNode.valueColumn( attributeName, sqmCastTarget ); + } + else { + valueNode = columnsNode.valueColumn( attributeName, sqmCastTarget, unquoteStringLiteral( jsonPath.getText() ) ); + } + visitJsonValueOnErrorOrEmptyClause( valueNode, valueContext.jsonValueOnErrorOrEmptyClause() ); + } + else if ( columnContext instanceof HqlParser.JsonTableOrdinalityColumnContext ordinalityContext ) { + columnsNode.ordinalityColumn( visitIdentifier( ordinalityContext.identifier() ) ); + } + else if ( columnContext instanceof HqlParser.JsonTableExistsColumnContext existsContext ) { + final String attributeName = visitIdentifier( existsContext.identifier() ); + final TerminalNode jsonPath = existsContext.STRING_LITERAL(); + final JpaJsonExistsNode existsNode; + if ( jsonPath == null ) { + existsNode = columnsNode.existsColumn( attributeName ); + } + else { + existsNode = columnsNode.existsColumn( attributeName, unquoteStringLiteral( jsonPath.getText() ) ); + } + final HqlParser.JsonExistsOnErrorClauseContext errorClauseContext = existsContext.jsonExistsOnErrorClause(); + if ( errorClauseContext != null ) { + switch ( ( (TerminalNode) errorClauseContext.getChild( 0 ) ).getSymbol().getType() ) { + case HqlParser.ERROR -> existsNode.errorOnError(); + case HqlParser.TRUE -> existsNode.trueOnError(); + case HqlParser.FALSE -> existsNode.falseOnError(); + } + } + } + else { + final HqlParser.JsonTableNestedColumnContext nestedColumnContext = (HqlParser.JsonTableNestedColumnContext) columnContext; + visitColumns( + columnsNode.nested( unquoteStringLiteral( nestedColumnContext.STRING_LITERAL().getText() ) ), + nestedColumnContext.jsonTableColumnsClause().jsonTableColumns().jsonTableColumn() + ); + } + } + } + private void checkJsonFunctionsEnabled(ParserRuleContext ctx) { if ( !creationOptions.isJsonFunctionsEnabled() ) { throw new SemanticException( 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 1ad30cc2fe8b..960ab2241d6f 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 @@ -24,6 +24,7 @@ import org.hibernate.query.BindingContext; import org.hibernate.query.SortDirection; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCastTarget; import org.hibernate.query.criteria.JpaCoalesce; import org.hibernate.query.criteria.JpaCompoundSelection; import org.hibernate.query.criteria.JpaExpression; @@ -42,10 +43,12 @@ import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmSetJoin; import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; +import org.hibernate.query.sqm.tree.expression.SqmCastTarget; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.expression.SqmFunction; import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; +import org.hibernate.query.sqm.tree.expression.SqmJsonTableFunction; import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; @@ -873,6 +876,15 @@ SqmJsonValueExpression jsonValue( @Override SqmSetReturningFunction generateSeries(E start, E stop); + @Override + SqmJsonTableFunction jsonTable(Expression jsonDocument); + + @Override + SqmJsonTableFunction jsonTable(Expression jsonDocument, String jsonPath); + + @Override + SqmJsonTableFunction jsonTable(Expression jsonDocument, Expression jsonPath); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides @@ -930,6 +942,18 @@ SqmJsonValueExpression jsonValue( @Override SqmExpression cast(JpaExpression expression, Class castTargetJavaType); + @Override + SqmExpression cast(JpaExpression expression, JpaCastTarget castTarget); + + @Override + SqmCastTarget castTarget(Class castTargetJavaType); + + @Override + SqmCastTarget castTarget(Class castTargetJavaType, long length); + + @Override + SqmCastTarget castTarget(Class castTargetJavaType, int precision, int scale); + @Override SqmPredicate wrap(Expression expression); 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 index c4be3e89cb41..c9801b2c330f 100644 --- 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 @@ -7,7 +7,6 @@ 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; @@ -35,14 +34,12 @@ public AbstractSqmSelfRenderingSetReturningFunctionDescriptor( 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/SelfRenderingSqmSetReturningFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmSetReturningFunction.java index 978d8f0e964d..c0c269f8ca0e 100644 --- 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 @@ -25,6 +25,7 @@ import org.hibernate.query.sqm.tree.expression.SqmSetReturningFunction; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlTreeCreationException; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.FunctionExpression; @@ -44,6 +45,7 @@ public class SelfRenderingSqmSetReturningFunction extends SqmSetReturningFunc private final @Nullable ArgumentsValidator argumentsValidator; private final SetReturningFunctionTypeResolver setReturningTypeResolver; private final SetReturningFunctionRenderer renderer; + private @Nullable AnonymousTupleType type; public SelfRenderingSqmSetReturningFunction( SqmSetReturningFunctionDescriptor descriptor, @@ -51,10 +53,9 @@ public SelfRenderingSqmSetReturningFunction( List> arguments, @Nullable ArgumentsValidator argumentsValidator, SetReturningFunctionTypeResolver setReturningTypeResolver, - AnonymousTupleType type, NodeBuilder nodeBuilder, String name) { - super( name, descriptor, type, arguments, nodeBuilder ); + super( name, descriptor, arguments, nodeBuilder ); this.renderer = renderer; this.argumentsValidator = argumentsValidator; this.setReturningTypeResolver = setReturningTypeResolver; @@ -78,13 +79,32 @@ public SelfRenderingSqmSetReturningFunction copy(SqmCopyContext context) { arguments, getArgumentsValidator(), getSetReturningTypeResolver(), - getType(), nodeBuilder(), getFunctionName() ) ); } + @Override + public AnonymousTupleType getType() { + AnonymousTupleType type = this.type; + if ( type == null ) { + //noinspection unchecked + type = this.type = (AnonymousTupleType) getSetReturningTypeResolver().resolveTupleType( + getArguments(), + nodeBuilder().getTypeConfiguration() + ); + if ( type == null ) { + throw new IllegalStateException( "SetReturningFunctionTypeResolver returned a null tuple type" ); + } + } + return type; + } + + protected boolean isTypeResolved() { + return type != null; + } + public SetReturningFunctionRenderer getFunctionRenderer() { return renderer; } @@ -164,7 +184,16 @@ public TableGroup convertToSqlAst( boolean canUseInnerJoins, boolean withOrdinality, SqmToSqlAstConverter walker) { - final List arguments = resolveSqlAstArguments( getArguments(), walker ); + final List arguments; + try { + arguments = resolveSqlAstArguments( getArguments(), walker ); + } + catch ( SqlTreeCreationException ex ) { + if ( !lateral && ex.getMessage().contains( "Could not locate TableGroup" ) ) { + throw new IllegalArgumentException( "Could not construct set-returning function. Maybe you forgot to use 'lateral'?", ex ); + } + throw ex; + } final ArgumentsValidator validator = argumentsValidator; if ( validator != null ) { validator.validateSqlTypes( arguments, getFunctionName() ); 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 6798eec8a05f..099740265daf 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 @@ -55,6 +55,7 @@ import org.hibernate.query.SemanticException; import org.hibernate.query.SortDirection; import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCastTarget; import org.hibernate.query.criteria.JpaCoalesce; import org.hibernate.query.criteria.JpaCompoundSelection; import org.hibernate.query.criteria.JpaCriteriaQuery; @@ -108,41 +109,7 @@ import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; import org.hibernate.query.sqm.tree.domain.SqmTreatedRoot; import org.hibernate.query.sqm.tree.domain.SqmTreatedSingularJoin; -import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; -import org.hibernate.query.sqm.tree.expression.SqmBinaryArithmetic; -import org.hibernate.query.sqm.tree.expression.SqmByUnit; -import org.hibernate.query.sqm.tree.expression.SqmCaseSearched; -import org.hibernate.query.sqm.tree.expression.SqmCaseSimple; -import org.hibernate.query.sqm.tree.expression.SqmCastTarget; -import org.hibernate.query.sqm.tree.expression.SqmCoalesce; -import org.hibernate.query.sqm.tree.expression.SqmCollation; -import org.hibernate.query.sqm.tree.expression.SqmCollectionSize; -import org.hibernate.query.sqm.tree.expression.SqmDistinct; -import org.hibernate.query.sqm.tree.expression.SqmDurationUnit; -import org.hibernate.query.sqm.tree.expression.SqmExpression; -import org.hibernate.query.sqm.tree.expression.SqmExtractUnit; -import org.hibernate.query.sqm.tree.expression.SqmFormat; -import org.hibernate.query.sqm.tree.expression.SqmFunction; -import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression; -import org.hibernate.query.sqm.tree.expression.SqmJsonNullBehavior; -import org.hibernate.query.sqm.tree.expression.SqmJsonObjectAggUniqueKeysBehavior; -import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; -import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; -import org.hibernate.query.sqm.tree.expression.SqmLiteral; -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; -import org.hibernate.query.sqm.tree.expression.SqmTuple; -import org.hibernate.query.sqm.tree.expression.SqmUnaryOperation; -import org.hibernate.query.sqm.tree.expression.SqmWindow; -import org.hibernate.query.sqm.tree.expression.SqmWindowFrame; -import org.hibernate.query.sqm.tree.expression.SqmXmlElementExpression; -import org.hibernate.query.sqm.tree.expression.ValueBindJpaCriteriaParameter; +import org.hibernate.query.sqm.tree.expression.*; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; @@ -561,14 +528,39 @@ private void collectQueryPartsAndCtes( @Override public SqmExpression cast(JpaExpression expression, Class castTargetJavaType) { - final BasicType type = getTypeConfiguration().standardBasicTypeForJavaType( castTargetJavaType ); + return cast( expression, castTarget( castTargetJavaType ) ); + } + + @Override + public SqmExpression cast(JpaExpression expression, JpaCastTarget castTarget) { + final SqmCastTarget sqmCastTarget = (SqmCastTarget) castTarget; return getFunctionDescriptor( "cast" ).generateSqmExpression( - asList( (SqmTypedNode) expression, new SqmCastTarget<>( type, this ) ), - type, + asList( (SqmTypedNode) expression, sqmCastTarget ), + sqmCastTarget.getType(), queryEngine ); } + @Override + public SqmCastTarget castTarget(Class castTargetJavaType) { + return castTarget( castTargetJavaType, null, null, null ); + } + + @Override + public SqmCastTarget castTarget(Class castTargetJavaType, long length) { + return castTarget( castTargetJavaType, length, null, null ); + } + + @Override + public SqmCastTarget castTarget(Class castTargetJavaType, int precision, int scale) { + return castTarget( castTargetJavaType, null, precision, scale ); + } + + private SqmCastTarget castTarget(Class castTargetJavaType, @Nullable Long length, @Nullable Integer precision, @Nullable Integer scale) { + final BasicType type = getTypeConfiguration().standardBasicTypeForJavaType( castTargetJavaType ); + return new SqmCastTarget<>( type, length, precision, scale, this ); + } + @Override public SqmPredicate wrap(Expression expression) { return expression instanceof SqmPredicate @@ -5952,4 +5944,24 @@ public SqmSetReturningFunction generateSeries(E start, Exp public SqmSetReturningFunction generateSeries(E start, E stop) { return generateSeries( value( start ), value( stop ) ); } + + @Override + public SqmJsonTableFunction jsonTable(Expression jsonDocument) { + return jsonTable( jsonDocument, (Expression) null ); + } + + @Override + public SqmJsonTableFunction jsonTable(Expression jsonDocument, String jsonPath) { + return jsonTable( jsonDocument, value( jsonPath ) ); + } + + @Override + public SqmJsonTableFunction jsonTable(Expression jsonDocument, Expression jsonPath) { + return (SqmJsonTableFunction) getSetReturningFunctionDescriptor( "json_table" ).generateSqmExpression( + jsonPath == null + ? asList( (SqmTypedNode) jsonDocument ) + : asList( (SqmTypedNode) jsonDocument, (SqmTypedNode) jsonPath ), + queryEngine + ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmCastTarget.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmCastTarget.java index 5b2accedf235..4dfbe6011999 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmCastTarget.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmCastTarget.java @@ -4,7 +4,9 @@ */ package org.hibernate.query.sqm.tree.expression; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.query.ReturnableType; +import org.hibernate.query.criteria.JpaCastTarget; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.SqmExpressible; @@ -16,24 +18,12 @@ /** * @author Gavin King */ -public class SqmCastTarget extends AbstractSqmNode implements SqmTypedNode { +public class SqmCastTarget extends AbstractSqmNode implements SqmTypedNode, JpaCastTarget { private final ReturnableType type; private final Long length; private final Integer precision; private final Integer scale; - public Long getLength() { - return length; - } - - public Integer getPrecision() { - return precision; - } - - public Integer getScale() { - return scale; - } - public SqmCastTarget( ReturnableType type, NodeBuilder nodeBuilder) { @@ -68,6 +58,21 @@ public SqmCastTarget( this.scale = scale; } + @Override + public @Nullable Long getLength() { + return length; + } + + @Override + public @Nullable Integer getPrecision() { + return precision; + } + + @Override + public @Nullable Integer getScale() { + return scale; + } + @Override public SqmCastTarget copy(SqmCopyContext context) { return this; @@ -90,12 +95,7 @@ public SqmExpressible getNodeType() { @Override public void appendHqlString(StringBuilder sb) { sb.append( type.getTypeName() ); - if ( length != null ) { - sb.append( '(' ); - sb.append( length ); - sb.append( ')' ); - } - else if ( precision != null ) { + if ( precision != null ) { sb.append( '(' ); sb.append( precision ); if ( scale != null ) { @@ -104,5 +104,10 @@ else if ( precision != null ) { } sb.append( ')' ); } + else if ( length != null ) { + sb.append( '(' ); + sb.append( length ); + sb.append( ')' ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonTableFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonTableFunction.java new file mode 100644 index 000000000000..f01ac33bbdd4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonTableFunction.java @@ -0,0 +1,997 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.query.sqm.tree.expression; + +import jakarta.persistence.criteria.Expression; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.internal.util.QuotingHelper; +import org.hibernate.query.criteria.JpaCastTarget; +import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaJsonExistsNode; +import org.hibernate.query.criteria.JpaJsonQueryNode; +import org.hibernate.query.criteria.JpaJsonTableColumnsNode; +import org.hibernate.query.criteria.JpaJsonTableFunction; +import org.hibernate.query.criteria.JpaJsonValueNode; +import org.hibernate.query.derived.AnonymousTupleType; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.function.SelfRenderingSqmSetReturningFunction; +import org.hibernate.query.sqm.function.SetReturningFunctionRenderer; +import org.hibernate.query.sqm.function.SqmSetReturningFunctionDescriptor; +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.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableColumnsClause; +import org.hibernate.sql.ast.tree.expression.JsonTableErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonTableExistsColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableNestedColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableOrdinalityColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableQueryColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonTableValueColumnDefinition; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.hibernate.internal.util.NullnessUtil.castNonNull; + +/** + * @since 7.0 + */ +public class SqmJsonTableFunction extends SelfRenderingSqmSetReturningFunction implements JpaJsonTableFunction { + + private final Set columnNames = new HashSet<>(); + private final Columns columns; + private @Nullable Map> passingExpressions; + private ErrorBehavior errorBehavior; + + public SqmJsonTableFunction( + SqmSetReturningFunctionDescriptor descriptor, + SetReturningFunctionRenderer renderer, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver setReturningTypeResolver, + NodeBuilder nodeBuilder, + SqmExpression document, + @Nullable SqmExpression jsonPath) { + this( + descriptor, + renderer, + jsonPath == null ? Arrays.asList( document, null ) : Arrays.asList( document, jsonPath, null ), + argumentsValidator, + setReturningTypeResolver, + nodeBuilder, + null, + ErrorBehavior.UNSPECIFIED + ); + } + + private SqmJsonTableFunction( + SqmSetReturningFunctionDescriptor descriptor, + SetReturningFunctionRenderer renderer, + List> arguments, + @Nullable ArgumentsValidator argumentsValidator, + SetReturningFunctionTypeResolver setReturningTypeResolver, + NodeBuilder nodeBuilder, + @Nullable Map> passingExpressions, + ErrorBehavior errorBehavior) { + super( descriptor, renderer, arguments, argumentsValidator, setReturningTypeResolver, nodeBuilder, "json_table" ); + this.columns = new Columns( this ); + this.passingExpressions = passingExpressions; + this.errorBehavior = errorBehavior; + arguments.set( arguments.size() - 1, this.columns ); + } + + public Map> getPassingExpressions() { + return passingExpressions == null ? Collections.emptyMap() : Collections.unmodifiableMap( passingExpressions ); + } + + @Override + public JpaJsonTableFunction passing(String parameterName, Expression expression) { + if ( columns.jsonPath == null ) { + throw new IllegalStateException( "Can't pass parameter '" + parameterName + "', because json_table has no JSON path" ); + } + if ( passingExpressions == null ) { + passingExpressions = new HashMap<>(); + } + passingExpressions.put( parameterName, (SqmExpression) expression ); + return this; + } + + @Override + public SqmJsonTableFunction copy(SqmCopyContext context) { + final SqmJsonTableFunction existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = getArguments(); + final List> argumentsCopy = new ArrayList<>( arguments.size() ); + for ( int i = 0; i < arguments.size() - 1; i++ ) { + argumentsCopy.add( arguments.get( i ).copy( context ) ); + } + final Map> passingExpressions; + if ( this.passingExpressions == null ) { + passingExpressions = null; + } + else { + passingExpressions = new HashMap<>( this.passingExpressions.size() ); + for ( Map.Entry> entry : this.passingExpressions.entrySet() ) { + passingExpressions.put( entry.getKey(), entry.getValue().copy( context ) ); + } + } + final SqmJsonTableFunction tableFunction = new SqmJsonTableFunction<>( + getFunctionDescriptor(), + getFunctionRenderer(), + argumentsCopy, + getArgumentsValidator(), + getSetReturningTypeResolver(), + nodeBuilder(), + passingExpressions, + errorBehavior + ); + context.registerCopy( this, tableFunction ); + tableFunction.columnNames.addAll( columnNames ); + tableFunction.columns.columnDefinitions.ensureCapacity( columns.columnDefinitions.size() ); + for ( ColumnDefinition columnDefinition : columns.columnDefinitions ) { + tableFunction.columns.columnDefinitions.add( columnDefinition.copy( context ) ); + } + return tableFunction; + } + + @Override + protected List resolveSqlAstArguments(List> sqmArguments, SqmToSqlAstConverter walker) { + final List sqlAstNodes = super.resolveSqlAstArguments( sqmArguments, walker ); + // The last argument is the SqmJsonTableFunction.Columns which will convert to null, so remove that + sqlAstNodes.remove( sqlAstNodes.size() - 1 ); + + final JsonPathPassingClause jsonPathPassingClause = createJsonPathPassingClause( walker ); + if ( jsonPathPassingClause != null ) { + sqlAstNodes.add( jsonPathPassingClause ); + } + switch ( errorBehavior ) { + case NULL -> sqlAstNodes.add( JsonTableErrorBehavior.NULL ); + case ERROR -> sqlAstNodes.add( JsonTableErrorBehavior.ERROR ); + case UNSPECIFIED -> { + } + } + final List definitions = new ArrayList<>( columns.columnDefinitions.size() ); + for ( ColumnDefinition columnDefinition : columns.columnDefinitions ) { + definitions.add( columnDefinition.convertToSqlAst( walker ) ); + } + sqlAstNodes.add( new JsonTableColumnsClause( definitions ) ); + return sqlAstNodes; + } + + protected @Nullable JsonPathPassingClause createJsonPathPassingClause(SqmToSqlAstConverter walker) { + if ( passingExpressions == null || passingExpressions.isEmpty() ) { + return null; + } + final HashMap converted = new HashMap<>( passingExpressions.size() ); + for ( Map.Entry> entry : passingExpressions.entrySet() ) { + converted.put( entry.getKey(), (org.hibernate.sql.ast.tree.expression.Expression) entry.getValue().accept( walker ) ); + } + return new JsonPathPassingClause( converted ); + } + + @Override + public ErrorBehavior getErrorBehavior() { + return errorBehavior; + } + + @Override + public JpaJsonTableFunction unspecifiedOnError() { + checkTypeResolved(); + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + return this; + } + + @Override + public SqmJsonTableFunction nullOnError() { + checkTypeResolved(); + this.errorBehavior = ErrorBehavior.NULL; + return this; + } + + @Override + public SqmJsonTableFunction errorOnError() { + checkTypeResolved(); + this.errorBehavior = ErrorBehavior.ERROR; + return this; + } + + @Override + public JpaJsonExistsNode existsColumn(String columnName) { + return existsColumn( columnName, null ); + } + + @Override + public JpaJsonExistsNode existsColumn(String columnName, @Nullable String jsonPath) { + return columns.existsColumn( columnName, jsonPath ); + } + + @Override + public JpaJsonQueryNode queryColumn(String columnName) { + return queryColumn( columnName, null ); + } + + @Override + public JpaJsonQueryNode queryColumn(String columnName, @Nullable String jsonPath) { + return columns.queryColumn( columnName, jsonPath ); + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, Class type) { + return valueColumn( columnName, type, null ); + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, Class type, String jsonPath) { + return columns.valueColumn( columnName, type, jsonPath ); + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, JpaCastTarget type, String jsonPath) { + return columns.valueColumn( columnName, type, jsonPath ); + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, JpaCastTarget type) { + return columns.valueColumn( columnName, type ); + } + + @Override + public JpaJsonTableColumnsNode nested(String jsonPath) { + return columns.nested( jsonPath ); + } + + @Override + public JpaJsonTableFunction ordinalityColumn(String columnName) { + columns.ordinalityColumn( columnName ); + return this; + } + + private void addColumn(String columnName) { + checkTypeResolved(); + if ( !columnNames.add( columnName ) ) { + throw new IllegalStateException( "Duplicate column: " + columnName ); + } + } + + private void checkTypeResolved() { + if ( isTypeResolved() ) { + throw new IllegalStateException( + "Type for json_table function is already resolved. Mutation is not allowed anymore" ); + } + } + + sealed interface ColumnDefinition { + ColumnDefinition copy(SqmCopyContext context); + + JsonTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker); + + void appendHqlString(StringBuilder sb); + + int populateTupleType(int offset, String[] componentNames, SqmExpressible[] componentTypes); + } + + static final class ExistsColumnDefinition implements ColumnDefinition, JpaJsonExistsNode { + private final String name; + private final BasicType type; + private final @Nullable String jsonPath; + private JpaJsonExistsNode.ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; + + ExistsColumnDefinition(String name, BasicType type, @Nullable String jsonPath) { + this.name = name; + this.type = type; + this.jsonPath = jsonPath; + } + + private ExistsColumnDefinition(String name, BasicType type, @Nullable String jsonPath, ErrorBehavior errorBehavior) { + this.name = name; + this.type = type; + this.jsonPath = jsonPath; + this.errorBehavior = errorBehavior; + } + + @Override + public ColumnDefinition copy(SqmCopyContext context) { + return new ExistsColumnDefinition( name, type, jsonPath, errorBehavior ); + } + + @Override + public JsonTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) { + return new JsonTableExistsColumnDefinition( + name, + type, + jsonPath, + switch ( errorBehavior ) { + case ERROR -> JsonExistsErrorBehavior.ERROR; + case FALSE -> JsonExistsErrorBehavior.FALSE; + case TRUE -> JsonExistsErrorBehavior.TRUE; + case UNSPECIFIED -> null; + } + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( name ); + sb.append( " exists" ); + + if ( jsonPath != null ) { + sb.append( " path " ); + QuotingHelper.appendSingleQuoteEscapedString( sb, jsonPath ); + } + + switch ( errorBehavior ) { + case ERROR -> sb.append( " error on error" ); + case TRUE -> sb.append( " true on error" ); + case FALSE -> sb.append( " false on error" ); + } + sb.append( ')' ); + } + + @Override + public int populateTupleType(int offset, String[] componentNames, SqmExpressible[] componentTypes) { + componentNames[offset] = name; + componentTypes[offset] = type; + return 1; + } + + @Override + public JpaJsonExistsNode.ErrorBehavior getErrorBehavior() { + return errorBehavior; + } + + @Override + public JpaJsonExistsNode unspecifiedOnError() { + errorBehavior = ErrorBehavior.UNSPECIFIED; + return this; + } + + @Override + public JpaJsonExistsNode errorOnError() { + errorBehavior = ErrorBehavior.ERROR; + return this; + } + + @Override + public JpaJsonExistsNode trueOnError() { + errorBehavior = ErrorBehavior.TRUE; + return this; + } + + @Override + public JpaJsonExistsNode falseOnError() { + errorBehavior = ErrorBehavior.FALSE; + return this; + } + } + + static final class QueryColumnDefinition implements ColumnDefinition, JpaJsonQueryNode { + private final String name; + private final BasicType type; + private final @Nullable String jsonPath; + private JpaJsonQueryNode.WrapMode wrapMode = WrapMode.UNSPECIFIED; + private JpaJsonQueryNode.ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; + private JpaJsonQueryNode.EmptyBehavior emptyBehavior = EmptyBehavior.UNSPECIFIED; + + QueryColumnDefinition(String name, BasicType type, @Nullable String jsonPath) { + this.name = name; + this.type = type; + this.jsonPath = jsonPath; + } + + private QueryColumnDefinition(String name, BasicType type, @Nullable String jsonPath, WrapMode wrapMode, ErrorBehavior errorBehavior, EmptyBehavior emptyBehavior) { + this.name = name; + this.type = type; + this.jsonPath = jsonPath; + this.wrapMode = wrapMode; + this.errorBehavior = errorBehavior; + this.emptyBehavior = emptyBehavior; + } + + @Override + public ColumnDefinition copy(SqmCopyContext context) { + return new QueryColumnDefinition( name, type, jsonPath, wrapMode, errorBehavior, emptyBehavior ); + } + + @Override + public JsonTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) { + return new JsonTableQueryColumnDefinition( + name, + type, + jsonPath, + switch ( wrapMode ) { + case WITH_WRAPPER -> JsonQueryWrapMode.WITH_WRAPPER; + case WITHOUT_WRAPPER -> JsonQueryWrapMode.WITHOUT_WRAPPER; + case WITH_CONDITIONAL_WRAPPER -> JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER; + case UNSPECIFIED -> null; + }, + switch ( errorBehavior ) { + case ERROR -> JsonQueryErrorBehavior.ERROR; + case NULL -> JsonQueryErrorBehavior.NULL; + case EMPTY_ARRAY -> JsonQueryErrorBehavior.EMPTY_ARRAY; + case EMPTY_OBJECT -> JsonQueryErrorBehavior.EMPTY_OBJECT; + case UNSPECIFIED -> null; + }, + switch ( emptyBehavior ) { + case ERROR -> JsonQueryEmptyBehavior.ERROR; + case NULL -> JsonQueryEmptyBehavior.NULL; + case EMPTY_ARRAY -> JsonQueryEmptyBehavior.EMPTY_ARRAY; + case EMPTY_OBJECT -> JsonQueryEmptyBehavior.EMPTY_OBJECT; + case UNSPECIFIED -> null; + } + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( name ); + sb.append( " json" ); + switch ( wrapMode ) { + case WITH_WRAPPER -> sb.append( " with wrapper" ); + case WITHOUT_WRAPPER -> sb.append( " without wrapper" ); + case WITH_CONDITIONAL_WRAPPER -> sb.append( " with conditional wrapper" ); + } + if ( jsonPath != null ) { + sb.append( " path " ); + QuotingHelper.appendSingleQuoteEscapedString( sb, jsonPath ); + } + switch ( errorBehavior ) { + case NULL -> sb.append( " null on error" ); + case ERROR -> sb.append( " error on error" ); + case EMPTY_ARRAY -> sb.append( " empty array on error" ); + case EMPTY_OBJECT -> sb.append( " empty object on error" ); + } + switch ( emptyBehavior ) { + case NULL -> sb.append( " null on empty" ); + case ERROR -> sb.append( " error on empty" ); + case EMPTY_ARRAY -> sb.append( " empty array on empty" ); + case EMPTY_OBJECT -> sb.append( " empty object on empty" ); + } + } + + @Override + public int populateTupleType(int offset, String[] componentNames, SqmExpressible[] componentTypes) { + componentNames[offset] = name; + componentTypes[offset] = type; + return 1; + } + + @Override + public WrapMode getWrapMode() { + return wrapMode; + } + + @Override + public ErrorBehavior getErrorBehavior() { + return errorBehavior; + } + + @Override + public EmptyBehavior getEmptyBehavior() { + return emptyBehavior; + } + + @Override + public JpaJsonQueryNode withoutWrapper() { + wrapMode = WrapMode.WITHOUT_WRAPPER; + return this; + } + + @Override + public JpaJsonQueryNode withWrapper() { + wrapMode = WrapMode.WITH_WRAPPER; + return this; + } + + @Override + public JpaJsonQueryNode withConditionalWrapper() { + wrapMode = WrapMode.WITH_CONDITIONAL_WRAPPER; + return this; + } + + @Override + public JpaJsonQueryNode unspecifiedWrapper() { + wrapMode = WrapMode.UNSPECIFIED; + return this; + } + + @Override + public JpaJsonQueryNode unspecifiedOnError() { + errorBehavior = ErrorBehavior.UNSPECIFIED; + return this; + } + + @Override + public JpaJsonQueryNode errorOnError() { + errorBehavior = ErrorBehavior.ERROR; + return this; + } + + @Override + public JpaJsonQueryNode nullOnError() { + errorBehavior = ErrorBehavior.NULL; + return this; + } + + @Override + public JpaJsonQueryNode emptyArrayOnError() { + errorBehavior = ErrorBehavior.EMPTY_ARRAY; + return this; + } + + @Override + public JpaJsonQueryNode emptyObjectOnError() { + errorBehavior = ErrorBehavior.EMPTY_OBJECT; + return this; + } + + @Override + public JpaJsonQueryNode unspecifiedOnEmpty() { + emptyBehavior = EmptyBehavior.UNSPECIFIED; + return this; + } + + @Override + public JpaJsonQueryNode errorOnEmpty() { + emptyBehavior = EmptyBehavior.ERROR; + return this; + } + + @Override + public JpaJsonQueryNode nullOnEmpty() { + emptyBehavior = EmptyBehavior.NULL; + return this; + } + + @Override + public JpaJsonQueryNode emptyArrayOnEmpty() { + emptyBehavior = EmptyBehavior.EMPTY_ARRAY; + return this; + } + + @Override + public JpaJsonQueryNode emptyObjectOnEmpty() { + emptyBehavior = EmptyBehavior.EMPTY_OBJECT; + return this; + } + } + + static final class ValueColumnDefinition implements ColumnDefinition, JpaJsonValueNode { + private final String name; + private final SqmCastTarget type; + private final @Nullable String jsonPath; + private JpaJsonValueNode.ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; + private JpaJsonValueNode.EmptyBehavior emptyBehavior = EmptyBehavior.UNSPECIFIED; + private @Nullable SqmExpression errorDefaultExpression; + private @Nullable SqmExpression emptyDefaultExpression; + + ValueColumnDefinition(String name, SqmCastTarget type, @Nullable String jsonPath) { + this.name = name; + this.type = type; + this.jsonPath = jsonPath; + } + + private ValueColumnDefinition( + String name, + SqmCastTarget type, + @Nullable String jsonPath, + ErrorBehavior errorBehavior, + EmptyBehavior emptyBehavior, + @Nullable SqmExpression errorDefaultExpression, + @Nullable SqmExpression emptyDefaultExpression) { + this.name = name; + this.type = type; + this.jsonPath = jsonPath; + this.errorBehavior = errorBehavior; + this.emptyBehavior = emptyBehavior; + this.errorDefaultExpression = errorDefaultExpression; + this.emptyDefaultExpression = emptyDefaultExpression; + } + + @Override + public ColumnDefinition copy(SqmCopyContext context) { + return new ValueColumnDefinition<>( + name, + type, + jsonPath, + errorBehavior, + emptyBehavior, + errorDefaultExpression == null ? null : errorDefaultExpression.copy( context ), + emptyDefaultExpression == null ? null : emptyDefaultExpression.copy( context ) + ); + } + + @Override + public JsonTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) { + return new JsonTableValueColumnDefinition( + name, + (org.hibernate.sql.ast.tree.expression.CastTarget) type.accept( walker ), + jsonPath, + switch ( errorBehavior ) { + case UNSPECIFIED -> null; + case NULL -> JsonValueErrorBehavior.NULL; + case ERROR -> JsonValueErrorBehavior.ERROR; + case DEFAULT -> JsonValueErrorBehavior.defaultOnError( + (org.hibernate.sql.ast.tree.expression.Expression) + castNonNull( errorDefaultExpression ).accept( walker ) + ); + }, + switch ( emptyBehavior ) { + case UNSPECIFIED -> null; + case NULL -> JsonValueEmptyBehavior.NULL; + case ERROR -> JsonValueEmptyBehavior.ERROR; + case DEFAULT -> JsonValueEmptyBehavior.defaultOnEmpty( + (org.hibernate.sql.ast.tree.expression.Expression) + castNonNull( emptyDefaultExpression ).accept( walker ) + ); + } + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( name ); + sb.append( ' ' ); + type.appendHqlString( sb ); + if ( jsonPath != null ) { + sb.append( " path " ); + QuotingHelper.appendSingleQuoteEscapedString( sb, jsonPath ); + } + switch ( errorBehavior ) { + case NULL -> sb.append( " null on error" ); + case ERROR -> sb.append( " error on error" ); + case DEFAULT -> { + assert errorDefaultExpression != null; + sb.append( " default " ); + errorDefaultExpression.appendHqlString( sb ); + sb.append( " on error" ); + } + } + switch ( emptyBehavior ) { + case NULL -> sb.append( " null on empty" ); + case ERROR -> sb.append( " error on empty" ); + case DEFAULT -> { + assert emptyDefaultExpression != null; + sb.append( " default " ); + emptyDefaultExpression.appendHqlString( sb ); + sb.append( " on empty" ); + } + } + } + + @Override + public int populateTupleType(int offset, String[] componentNames, SqmExpressible[] componentTypes) { + componentNames[offset] = name; + componentTypes[offset] = type.getNodeType(); + return 1; + } + + @Override + public ErrorBehavior getErrorBehavior() { + return errorBehavior; + } + + @Override + public EmptyBehavior getEmptyBehavior() { + return emptyBehavior; + } + + @Override + public @Nullable JpaExpression getErrorDefault() { + return errorDefaultExpression; + } + + @Override + public @Nullable JpaExpression getEmptyDefault() { + return emptyDefaultExpression; + } + + @Override + public JpaJsonValueNode unspecifiedOnError() { + this.errorDefaultExpression = null; + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + return this; + } + + @Override + public JpaJsonValueNode errorOnError() { + this.errorDefaultExpression = null; + this.errorBehavior = ErrorBehavior.ERROR; + return this; + } + + @Override + public JpaJsonValueNode nullOnError() { + this.errorDefaultExpression = null; + this.errorBehavior = ErrorBehavior.NULL; + return this; + } + + @Override + public JpaJsonValueNode defaultOnError(Expression expression) { + //noinspection unchecked + this.errorDefaultExpression = (SqmExpression) expression; + this.errorBehavior = ErrorBehavior.DEFAULT; + return this; + } + + @Override + public JpaJsonValueNode unspecifiedOnEmpty() { + this.emptyDefaultExpression = null; + this.emptyBehavior = EmptyBehavior.UNSPECIFIED; + return this; + } + + @Override + public JpaJsonValueNode errorOnEmpty() { + this.emptyDefaultExpression = null; + this.emptyBehavior = EmptyBehavior.ERROR; + return this; + } + + @Override + public JpaJsonValueNode nullOnEmpty() { + this.emptyDefaultExpression = null; + this.emptyBehavior = EmptyBehavior.NULL; + return this; + } + + @Override + public JpaJsonValueNode defaultOnEmpty(Expression expression) { + //noinspection unchecked + this.emptyDefaultExpression = (SqmExpression) expression; + this.emptyBehavior = EmptyBehavior.DEFAULT; + return this; + } + } + + record OrdinalityColumnDefinition(String name, BasicType type) implements ColumnDefinition { + @Override + public ColumnDefinition copy(SqmCopyContext context) { + return this; + } + + @Override + public JsonTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) { + return new JsonTableOrdinalityColumnDefinition( name ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( name ); + sb.append( " for ordinality" ); + } + + @Override + public int populateTupleType(int offset, String[] componentNames, SqmExpressible[] componentTypes) { + componentNames[offset] = name; + componentTypes[offset] = type; + return 1; + } + } + + static sealed class NestedColumns implements ColumnDefinition, JpaJsonTableColumnsNode { + protected final String jsonPath; + protected final SqmJsonTableFunction table; + protected final ArrayList columnDefinitions; + + NestedColumns(String jsonPath, SqmJsonTableFunction table) { + this( jsonPath, table, new ArrayList<>() ); + } + + private NestedColumns(String jsonPath, SqmJsonTableFunction table, ArrayList columnDefinitions) { + this.jsonPath = jsonPath; + this.table = table; + this.columnDefinitions = columnDefinitions; + } + + @Override + public int populateTupleType(int offset, String[] componentNames, SqmExpressible[] componentTypes) { + int i = 0; + for ( ColumnDefinition columnDefinition : columnDefinitions ) { + i += columnDefinition.populateTupleType(offset + i, componentNames, componentTypes ); + } + return i; + } + + @Override + public NestedColumns copy(SqmCopyContext context) { + final ArrayList definitions = new ArrayList<>( columnDefinitions.size() ); + for ( ColumnDefinition columnDefinition : columnDefinitions ) { + definitions.add( columnDefinition.copy( context ) ); + } + return new NestedColumns( jsonPath, context.getCopy( table ), definitions ); + } + + @Override + public JsonTableColumnDefinition convertToSqlAst(SqmToSqlAstConverter walker) { + final List nestedColumns = new ArrayList<>( columnDefinitions.size() ); + for ( ColumnDefinition column : columnDefinitions ) { + nestedColumns.add( column.convertToSqlAst( walker ) ); + } + return new JsonTableNestedColumnDefinition( jsonPath, new JsonTableColumnsClause( nestedColumns ) ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( "nested " ); + QuotingHelper.appendSingleQuoteEscapedString( sb, jsonPath ); + appendColumnsToHqlString( sb ); + } + + void appendColumnsToHqlString(StringBuilder sb) { + String separator = " columns ("; + for ( ColumnDefinition columnDefinition : columnDefinitions ) { + sb.append( separator ); + columnDefinition.appendHqlString( sb ); + separator = ", "; + } + sb.append( ')' ); + } + + + @Override + public JpaJsonExistsNode existsColumn(String columnName) { + return existsColumn( columnName, null ); + } + + @Override + public JpaJsonExistsNode existsColumn(String columnName, @Nullable String jsonPath) { + final BasicType type = table.nodeBuilder().getBooleanType(); + table.addColumn( columnName ); + final ExistsColumnDefinition existsColumnDefinition = new ExistsColumnDefinition( columnName, type, jsonPath ); + columnDefinitions.add( existsColumnDefinition ); + return existsColumnDefinition; + } + + @Override + public JpaJsonQueryNode queryColumn(String columnName) { + return queryColumn( columnName, null ); + } + + @Override + public JpaJsonQueryNode queryColumn(String columnName, @Nullable String jsonPath) { + final BasicType type = table.nodeBuilder().getTypeConfiguration().getBasicTypeRegistry() + .resolve( String.class, SqlTypes.JSON ); + table.addColumn( columnName ); + final QueryColumnDefinition queryColumnDefinition = new QueryColumnDefinition( columnName, type, jsonPath ); + columnDefinitions.add( queryColumnDefinition ); + return queryColumnDefinition; + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, Class type) { + return valueColumn( columnName, type, null ); + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, Class type, String jsonPath) { + return valueColumn( columnName, table.nodeBuilder().castTarget( type ), jsonPath ); + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, JpaCastTarget type) { + return valueColumn( columnName, type, null ); + } + + @Override + public JpaJsonValueNode valueColumn(String columnName, JpaCastTarget type, String jsonPath) { + final SqmCastTarget sqmCastTarget = (SqmCastTarget) type; + table.addColumn( columnName ); + final ValueColumnDefinition valueColumnDefinition = new ValueColumnDefinition<>( + columnName, + sqmCastTarget, + jsonPath + ); + columnDefinitions.add( valueColumnDefinition ); + return valueColumnDefinition; + } + + @Override + public JpaJsonTableColumnsNode nested(String jsonPath) { + table.checkTypeResolved(); + final NestedColumns nestedColumnDefinition = new NestedColumns( jsonPath, table ); + columnDefinitions.add( nestedColumnDefinition ); + return nestedColumnDefinition; + } + + @Override + public JpaJsonTableColumnsNode ordinalityColumn(String columnName) { + final BasicType type = table.nodeBuilder().getLongType(); + table.addColumn( columnName ); + columnDefinitions.add( new OrdinalityColumnDefinition( columnName, type ) ); + return this; + } + + public X accept(SemanticQueryWalker walker) { + for ( ColumnDefinition columnDefinition : columnDefinitions ) { + if ( columnDefinition instanceof SqmJsonTableFunction.ValueColumnDefinition definition ) { + if ( definition.emptyDefaultExpression != null ) { + definition.emptyDefaultExpression.accept( walker ); + } + if ( definition.errorDefaultExpression != null ) { + definition.errorDefaultExpression.accept( walker ); + } + } + else if ( columnDefinition instanceof NestedColumns nestedColumns ) { + nestedColumns.accept( walker ); + } + } + + // No-op since this object is going to be visible as function argument + return null; + } + } + + public static final class Columns extends NestedColumns implements SqmTypedNode { + + public Columns(SqmJsonTableFunction table) { + super( "", table ); + } + + private Columns(SqmJsonTableFunction table, ArrayList columnDefinitions) { + super( "", table, columnDefinitions ); + } + + public AnonymousTupleType createTupleType() { + if ( table.columnNames.isEmpty() ) { + throw new IllegalArgumentException( "Couldn't determine types of columns of function 'json_table'" ); + } + final SqmExpressible[] componentTypes = new SqmExpressible[table.columnNames.size()]; + final String[] componentNames = new String[table.columnNames.size()]; + int result = populateTupleType( 0, componentNames, componentTypes ); + + // Sanity check + assert result == componentTypes.length; + + return new AnonymousTupleType<>( componentTypes, componentNames ); + } + + @Override + public Columns copy(SqmCopyContext context) { + final ArrayList definitions = new ArrayList<>( columnDefinitions.size() ); + for ( ColumnDefinition columnDefinition : columnDefinitions ) { + definitions.add( columnDefinition.copy( context ) ); + } + return new Columns( context.getCopy( table ), definitions ); + } + + @Override + public @Nullable SqmExpressible getNodeType() { + return null; + } + + @Override + public NodeBuilder nodeBuilder() { + return table.nodeBuilder(); + } + + @Override + public void appendHqlString(StringBuilder sb) { + appendColumnsToHqlString( sb ); + } + } +} 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 index d71b01b632d0..15e81f878e6d 100644 --- 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 @@ -33,19 +33,16 @@ public abstract class SqmSetReturningFunction extends AbstractSqmNode impleme 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; } @@ -61,9 +58,7 @@ public String getFunctionName() { return functionName; } - public AnonymousTupleType getType() { - return type; - } + public abstract AnonymousTupleType getType(); public List> getArguments() { return arguments; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnDefinition.java new file mode 100644 index 000000000000..0c1d42a338fe --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnDefinition.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * @since 7.0 + */ +public sealed interface JsonTableColumnDefinition extends SqlAstNode + permits JsonTableExistsColumnDefinition, JsonTableNestedColumnDefinition, JsonTableOrdinalityColumnDefinition, JsonTableQueryColumnDefinition, JsonTableValueColumnDefinition { + + @Override + default void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonTableColumnDefinition doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnsClause.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnsClause.java new file mode 100644 index 000000000000..8d2f0ea2babe --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableColumnsClause.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +import java.util.List; + +/** + * @since 7.0 + */ +public class JsonTableColumnsClause implements SqlAstNode { + + private final List columnDefinitions; + + public JsonTableColumnsClause(List columnDefinitions) { + this.columnDefinitions = columnDefinitions; + } + + public List getColumnDefinitions() { + return columnDefinitions; + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonPathPassingClause doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableErrorBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableErrorBehavior.java new file mode 100644 index 000000000000..0a68f4767479 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableErrorBehavior.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * @since 7.0 + */ +public enum JsonTableErrorBehavior implements SqlAstNode { + ERROR, + NULL; + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonTableErrorBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableExistsColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableExistsColumnDefinition.java new file mode 100644 index 000000000000..26645969566b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableExistsColumnDefinition.java @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.type.BasicType; + +/** + * @since 7.0 + */ +public record JsonTableExistsColumnDefinition( + String name, + BasicType type, + @Nullable String jsonPath, + @Nullable JsonExistsErrorBehavior errorBehavior +) implements JsonTableColumnDefinition { +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableNestedColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableNestedColumnDefinition.java new file mode 100644 index 000000000000..ff0095d5b142 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableNestedColumnDefinition.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +/** + * @since 7.0 + */ +public record JsonTableNestedColumnDefinition( + String jsonPath, + JsonTableColumnsClause columns +) implements JsonTableColumnDefinition { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableOrdinalityColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableOrdinalityColumnDefinition.java new file mode 100644 index 000000000000..3acc98b31778 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableOrdinalityColumnDefinition.java @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +/** + * @since 7.0 + */ +public record JsonTableOrdinalityColumnDefinition( + String name +) implements JsonTableColumnDefinition { +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableQueryColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableQueryColumnDefinition.java new file mode 100644 index 000000000000..8999eb01adaf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableQueryColumnDefinition.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.type.BasicType; + +/** + * @since 7.0 + */ +public record JsonTableQueryColumnDefinition( + String name, + BasicType type, + @Nullable String jsonPath, + @Nullable JsonQueryWrapMode wrapMode, + @Nullable JsonQueryErrorBehavior errorBehavior, + @Nullable JsonQueryEmptyBehavior emptyBehavior +) implements JsonTableColumnDefinition { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableValueColumnDefinition.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableValueColumnDefinition.java new file mode 100644 index 000000000000..d022e7c86533 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonTableValueColumnDefinition.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.sql.ast.tree.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * @since 7.0 + */ +public record JsonTableValueColumnDefinition( + String name, + CastTarget type, + @Nullable String jsonPath, + @Nullable JsonValueErrorBehavior errorBehavior, + @Nullable JsonValueEmptyBehavior emptyBehavior +) implements JsonTableColumnDefinition { + +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonBlobJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonBlobJdbcType.java index b481d20314cc..4f4263a6b6b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonBlobJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonBlobJdbcType.java @@ -40,6 +40,11 @@ public int getJdbcTypeCode() { return SqlTypes.BLOB; } + @Override + public int getDdlTypeCode() { + return SqlTypes.BLOB; + } + @Override public String toString() { return "JsonBlobJdbcType"; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonTableTest.java new file mode 100644 index 000000000000..ee56cb7854a8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonTableTest.java @@ -0,0 +1,198 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.Tuple; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.query.criteria.JpaFunctionJoin; +import org.hibernate.query.criteria.JpaJsonTableColumnsNode; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.tree.expression.SqmJsonTableFunction; +import org.hibernate.query.sqm.tree.select.SqmSelectStatement; +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.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = EntityWithJson.class) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonTable.class) +public class JsonTableTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( em -> { + EntityWithJson entity = new EntityWithJson(); + entity.setId( 1L ); + entity.getJson().put( "theInt", 1 ); + entity.getJson().put( "theFloat", 0.1 ); + entity.getJson().put( "theString", "abc" ); + entity.getJson().put( "theBoolean", true ); + entity.getJson().put( "theNull", null ); + entity.getJson().put( "theArray", new String[] { "a", "b", "c" } ); + entity.getJson().put( "theObject", new HashMap<>( entity.getJson() ) ); + em.persist(entity); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.createMutationQuery( "delete from EntityWithJson" ).executeUpdate(); + } ); + } + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-table-example[] + final String query = """ + select + t.theInt, + t.theFloat, + t.theString, + t.theBoolean, + t.theNull, + t.theObject, + t.theNestedInt, + t.theNestedFloat, + t.theNestedString, + t.arrayIndex, + t.arrayValue, + t.nonExisting + from EntityWithJson e + join lateral json_table(e.json,'$' columns( + theInt Integer, + theFloat Float, + theString String, + theBoolean Boolean, + theNull String, + theObject JSON, + theNestedInt Integer path '$.theObject.theInt', + theNestedFloat Float path '$.theObject.theFloat', + theNestedString String path '$.theObject.theString', + nested '$.theArray[*]' columns( + arrayIndex for ordinality, + arrayValue String path '$' + ), + nonExisting exists + )) t + order by e.id, t.arrayIndex + """; + List resultList = em.createQuery( query, Tuple.class ).getResultList(); + //end::hql-json-table-example[] + + assertEquals( 3, resultList.size() ); + + assertTupleEquals( resultList.get( 0 ), 1L, "a" ); + assertTupleEquals( resultList.get( 1 ), 2L, "b" ); + assertTupleEquals( resultList.get( 2 ), 3L, "c" ); + } ); + } + + @Test + public void testNodeBuilderJsonTableObject(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final SqmSelectStatement cq = cb.createTupleQuery(); + final JpaRoot root = cq.from( EntityWithJson.class ); + final SqmJsonTableFunction jsonTable = cb.jsonTable( root.get( "json" ), cb.literal( "$" ) ); + + jsonTable.valueColumn( "theInt", Integer.class ); + jsonTable.valueColumn( "theFloat", Float.class ); + jsonTable.valueColumn( "theString", String.class ); + jsonTable.valueColumn( "theBoolean", Boolean.class ); + jsonTable.valueColumn( "theNull", String.class ); + jsonTable.queryColumn( "theObject" ); + jsonTable.valueColumn( "theNestedInt", Integer.class, "$.theObject.theInt" ); + jsonTable.valueColumn( "theNestedFloat", Float.class, "$.theObject.theFloat" ); + jsonTable.valueColumn( "theNestedString", String.class, "$.theObject.theString" ); + final JpaJsonTableColumnsNode theArray = jsonTable.nested( "$.theArray[*]" ); + theArray.ordinalityColumn( "arrayIndex" ); + theArray.valueColumn( "arrayValue", String.class, "$" ); + jsonTable.existsColumn( "nonExisting" ); + + final JpaFunctionJoin join = root.joinLateral( jsonTable ); + cq.multiselect( + join.get( "theInt" ), + join.get( "theFloat" ), + join.get( "theString" ), + join.get( "theBoolean" ), + join.get( "theNull" ), + join.get( "theObject" ), + join.get( "theNestedInt" ), + join.get( "theNestedFloat" ), + join.get( "theNestedString" ), + join.get( "arrayIndex" ), + join.get( "arrayValue" ), + join.get( "nonExisting" ) + ); + cq.orderBy( cb.asc( root.get( "id" ) ), cb.asc( join.get( "arrayIndex" ) ) ); + List resultList = em.createQuery( cq ).getResultList(); + + assertEquals( 3, resultList.size() ); + + assertTupleEquals( resultList.get( 0 ), 1L, "a" ); + assertTupleEquals( resultList.get( 1 ), 2L, "b" ); + assertTupleEquals( resultList.get( 2 ), 3L, "c" ); + } ); + } + + private static void assertTupleEquals(Tuple tuple, long arrayIndex, String arrayValue) { + assertEquals( 1, tuple.get( 0 ) ); + assertEquals( 0.1F, tuple.get( 1 ) ); + assertEquals( "abc", tuple.get( 2 ) ); + assertEquals( true, tuple.get( 3 ) ); + assertNull( tuple.get( 4 ) ); + + Map jsonMap = parseObject( tuple.get( 5, String.class ) ); + assertEquals( 1, jsonMap.get( "theInt" ) ); + assertEquals( 0.1D, jsonMap.get( "theFloat" ) ); + assertEquals( "abc", jsonMap.get( "theString" ) ); + assertEquals( true, jsonMap.get( "theBoolean" ) ); + assertNull( jsonMap.get( "theNull" ) ); + assertEquals( Arrays.asList( "a", "b", "c" ), jsonMap.get( "theArray" ) ); + + assertEquals( 1, tuple.get( 6 ) ); + assertEquals( 0.1F, tuple.get( 7 ) ); + assertEquals( "abc", tuple.get( 8 ) ); + assertEquals( arrayIndex, tuple.get( 9 ) ); + assertEquals( arrayValue, tuple.get( 10 ) ); + } + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static Map parseObject(String json) { + try { + //noinspection unchecked + return MAPPER.readValue( json, Map.class ); + } + catch (JsonProcessingException e) { + throw new RuntimeException( e ); + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index cb5e2299daca..510b4b7afa24 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -176,6 +176,20 @@ public void testJsonValueExpression(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonValue.class) + public void testJsonValueBoolean(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select json_value(e.json, '$.theBoolean' returning boolean) from JsonHolder e where json_value(e.json, '$.theBoolean' returning boolean) = true", + Tuple.class + ).getSingleResult(); + assertEquals( true, tuple.get( 0 ) ); + } + ); + } + @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonQuery.class) public void testJsonQuery(SessionFactoryScope scope) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 50cac248bb57..1db67ccca356 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -841,6 +841,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonTable implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesSetReturningFunction( dialect, "json_table" ); + } + } + public static class SupportsArrayAgg implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return definesFunction( dialect, "array_agg" ); diff --git a/release-announcement.adoc b/release-announcement.adoc index bce4bebd9ca0..daeaa2337562 100644 --- a/release-announcement.adoc +++ b/release-announcement.adoc @@ -82,9 +82,12 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` and ` A set-returning function is a new type of function that can return rows and is exclusive to the `from` clause. The concept is known in many different database SQL dialects and is sometimes referred to as table valued function or table function. -Custom set-returning functions can be registered via a `FunctionContributor` and Hibernate ORM -also comes with out-of-the-box support for the set-returning functions `unnest()`, which allows to turn an array into rows, -and `generate_series()`, which can be used to create a series of values as rows. +Custom set-returning functions can be registered via a `FunctionContributor`. +Out-of-the-box, some common set-returning functions are already supported or emulated + +* `unnest()` - allows to turn an array into rows +* `generate_series()` - can be used to create a series of values as rows +* `json_table()` - turns a JSON document into rows [[cleanup]] == Clean-up