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 eee71c608528..40f67107c5aa 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -6,6 +6,7 @@ :core-project-dir: {root-project-dir}/hibernate-core :example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/hql :array-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/array +:json-example-dir-hql: {core-project-dir}/src/test/java/org/hibernate/orm/test/function/json :extrasdir: extras This chapter describes Hibernate Query Language (HQL) and Jakarta Persistence Query Language (JPQL). @@ -1619,6 +1620,399 @@ include::{array-example-dir-hql}/ArrayToStringTest.java[tags=hql-array-to-string ---- ==== +[[hql-functions-json]] +==== Functions for dealing with JSON + +The following functions deal with SQL JSON types, which are not supported on every database. + +NOTE: The following functions are incubating/tech-preview and to use them in HQL, +it is necessary to enable the `hibernate.query.hql.json_functions_enabled` configuration setting. + +[[hql-json-functions]] +|=== +| 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 +|=== + + +[[hql-json-object-function]] +===== `json_object()` + +Constructs a JSON object from pairs of key and value arguments. + +[[hql-json-object-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_object_bnf.txt[] +---- + +Argument count must be even and expressions alternate between keys and values i.e. `key1, value1, key2, value2, ...`. +Alternatively, it is also possible to use a `:` (colon) to separate keys from values or the `value` keyword. + +[[hql-json-object-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonObjectTest.java[tags=hql-json-object-example] +---- +==== + +Although database dependent, usually `null` values are present in the resulting JSON object. +To remove `null` value entries, use the `absent on null` clause. + +[[hql-json-object-on-null-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonObjectTest.java[tags=hql-json-object-on-null-example] +---- +==== + +[[hql-json-array-function]] +===== `json_array()` + +Constructs a JSON array from arguments. + +[[hql-json-array-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_array_bnf.txt[] +---- + +[[hql-json-array-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayTest.java[tags=hql-json-array-example] +---- +==== + +Although database dependent, usually `null` values are `absent` in the resulting JSON array. +To retain `null` elements, use the `null on null` clause. + +[[hql-json-array-on-null-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayTest.java[tags=hql-json-array-on-null-example] +---- +==== + +[[hql-json-value-function]] +===== `json_value()` + +Extracts a scalar value by https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path] from a JSON document. + +[[hql-json-value-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_value_bnf.txt[] +---- + +The first argument is an expression to a JSON document. The second argument is a JSON path as String expression. + +WARNING: Some databases might also allow extracting non-scalar values. Beware that this behavior is not portable. + +NOTE: It is recommended to only us the dot notation for JSON paths instead of the bracket notation, +since most databases support only that. + +[[hql-json-value-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-example] +---- +==== + +The `passing` clause allows to reuse the same JSON path but pass different values for evaluation. + +[[hql-json-value-passing-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-passing-example] +---- +==== + +The `returning` clause allows to specify the <> i.e. the type of value to extract. + +[[hql-json-value-returning-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-returning-example] +---- +==== + +The `on error` clause defines the behavior when an error occurs while resolving the value for the JSON path. +Conditions that classify as errors are database dependent, but usual errors which can be handled with this clause are: + +* First argument is not a valid JSON document +* Second argument is not a valid JSON path +* JSON path does not resolve to a scalar value + +The default behavior of `on error` is database specific, but usually, `null` is returned on an error. +It is recommended to specify this clause when the exact error behavior is important. + +[[hql-json-value-on-error-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-on-error-example] +---- +==== + +The `on empty` clause defines the behavior when the JSON path does not match the JSON document. +By default, `null` is returned on empty. + +[[hql-json-value-on-empty-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-on-empty-example] +---- +==== + +To actually receive an error `on empty`, it is necessary to also specify `error on error`. +Depending on the database, an error might still be thrown even without that, but that is not portable. + +NOTE: The H2 emulation only supports absolute JSON paths using the dot notation. + +[[hql-json-exists-function]] +===== `json_exists()` + +Checks if a JSON document contains a https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path]. + +[[hql-json-exists-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_exists_bnf.txt[] +---- + +The first argument is an expression to a JSON document. The second argument is a JSON path as String expression. + +NOTE: It is recommended to only us the dot notation for JSON paths instead of the bracket notation, +since most databases support only that. + +[[hql-json-exists-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-example] +---- +==== + +The `passing` clause allows to reuse the same JSON path but pass different values for evaluation. + +[[hql-json-exists-passing-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-passing-example] +---- +==== + +The `on error` clause defines the behavior when an error occurs while checking for existence with the JSON path. +Conditions that classify as errors are database dependent, but usual errors which can be handled with this clause are: + +* First argument is not a valid JSON document +* Second argument is not a valid JSON path + +The default behavior of `on error` is database specific, but usually, `false` is returned on an error. +It is recommended to specify this clause when the exact error behavior is important. + +[[hql-json-exists-on-error-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-on-error-example] +---- +==== + +NOTE: The H2 emulation only supports absolute JSON paths using the dot notation. + +[[hql-json-query-function]] +===== `json_query()` + +Queries non-scalar values from a JSON document by a https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path]. + +[[hql-json-query-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_query_bnf.txt[] +---- + +The first argument is an expression to a JSON document. The second argument is a JSON path as String expression. + +WARNING: Some databases might also allow querying scalar values. Beware that this behavior is not portable. + +NOTE: It is recommended to only us the dot notation for JSON paths instead of the bracket notation, +since most databases support only that. + +[[hql-json-query-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-example] +---- +==== + +The `passing` clause allows to reuse the same JSON path but pass different values for evaluation. + +[[hql-json-query-passing-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-passing-example] +---- +==== + +The `wrapper` clause allows to specify whether results of a query should be wrapped in brackets `[]` i.e. an array. +The default behavior is to omit an array wrapper i.e. `without wrapper`. +It is an error when a `json_query` returns more than a single result and `without wrapper` is used. +How an error like this should be handled can be controlled with the `on error` clause. + +WARNING: Since the default behavior of `on error` is database dependent, +some databases might return a comma separated list of values even when using `without wrapper`. This is not portable. + +[[hql-json-query-wrapper-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-with-wrapper-example] +---- +==== + +The `on error` clause defines the behavior when an error occurs while querying with the JSON path. +Conditions that classify as errors are database dependent, but usual errors which can be handled with this clause are: + +* First argument is not a valid JSON document +* Second argument is not a valid JSON path +* Multiple `json_query` results when `without wrapper` is used + +The default behavior of `on error` is database specific, but usually, `null` is returned on an error. +It is recommended to specify this clause when the exact error behavior is important. + +[[hql-json-query-on-error-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-on-error-example] +---- +==== + +The `on empty` clause defines the behavior when the JSON path does not match the JSON document. +By default, `null` is returned on empty. + +[[hql-json-query-on-empty-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonQueryTest.java[tags=hql-json-query-on-empty-example] +---- +==== + +To actually receive an error `on empty`, it is necessary to also specify `error on error`. +Depending on the database, an error might still be thrown even without that, but that is not portable. + +NOTE: The H2 emulation only supports absolute JSON paths using the dot notation. + +[[hql-json-arrayagg-function]] +===== `json_arrayagg()` + +Creates a JSON array by aggregating values. + +[[hql-json-arrayagg-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_arrayagg_bnf.txt[] +---- + +This aggregate function is similar to an <> +since it allows to specify the order in which elements are aggregated, but uses a special syntax. + +[[hql-json-arrayagg-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayAggregateTest.java[tags=hql-json-arrayagg-example] +---- +==== + +Although database dependent, usually `null` values are `absent` in the resulting JSON array. +To retain `null` elements, use the `null on null` clause. + +[[hql-json-arrayagg-null-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayAggregateTest.java[tags=hql-json-arrayagg-null-example] +---- +==== + +The order in which elements are aggregated can be defined by specifying an order by clause. + +[[hql-json-arrayagg-order-by-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayAggregateTest.java[tags=hql-json-arrayagg-order-by-example] +---- +==== + +[[hql-json-objectagg-function]] +===== `json_objectagg()` + +Creates a JSON object by aggregating values. + +[[hql-json-arrayagg-bnf]] +[source, antlrv4, indent=0] +---- +include::{extrasdir}/json_objectagg_bnf.txt[] +---- + +The arguments represent the key and the value to be aggregated to the JSON object, +separated by the `value` keyword or a `:` (colon). + +[[hql-json-objectagg-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonObjectAggregateTest.java[tags=hql-json-objectagg-example] +---- +==== + +Although database dependent, usually `null` values are `absent` in the resulting JSON array. +To retain `null` elements, use the `null on null` clause. + +[[hql-json-objectagg-null-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonObjectAggregateTest.java[tags=hql-json-objectagg-null-example] +---- +==== + +Duplicate keys usually are retained in the resulting string. +Use `with unique keys` to specify that the encounter of a duplicate key should cause an error. + +[[hql-json-objectagg-unique-keys-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonObjectAggregateTest.java[tags=hql-json-objectagg-unique-keys-example] +---- +==== + +WARNING: Some databases like e.g. MySQL, SAP HANA, DB2 and SQL Server do not support raising an error on duplicate keys. + [[hql-user-defined-functions]] ==== Native and user-defined functions diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_array_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_array_bnf.txt new file mode 100644 index 000000000000..2b0bc034edbd --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_array_bnf.txt @@ -0,0 +1,5 @@ +"json_array(" (expressionOrPredicate ("," expressionOrPredicate)* jsonNullClause?)? ")" + +jsonNullClause + : ("absent"|"null") "on null" + ; \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_arrayagg_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_arrayagg_bnf.txt new file mode 100644 index 000000000000..33e4e7e62ffc --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_arrayagg_bnf.txt @@ -0,0 +1,5 @@ +"json_arrayagg(" expressionOrPredicate jsonNullClause? orderByClause? ")" filterClause? + +jsonNullClause + : ("absent"|"null") "on null" + ; \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt new file mode 100644 index 000000000000..3921a527788b --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt @@ -0,0 +1,7 @@ +"json_exists(" expression "," expression passingClause? onErrorClause? ")" + +passingClause + : "passing" expression "as" identifier ("," expression "as" identifier)* + +onErrorClause + : ( "error" | "true" | "false" ) "on error"; diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_object_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_object_bnf.txt new file mode 100644 index 000000000000..4d8682f209d4 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_object_bnf.txt @@ -0,0 +1,15 @@ +"json_object(" (jsonObjectFunctionEntries jsonNullClause?)? ")" + +jsonObjectFunctionEntries + : expressionOrPredicate "," expressionOrPredicate ("," expressionOrPredicate "," expressionOrPredicate)* + | jsonObjectFunctionEntry ("," jsonObjectFunctionEntry)* + ; + +jsonObjectFunctionEntry + : "key"? expressionOrPredicate "value" expressionOrPredicate + | expressionOrPredicate ":" expressionOrPredicate + ; + +jsonNullClause + : ("absent"|"null") "on null" + ; \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_objectagg_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_objectagg_bnf.txt new file mode 100644 index 000000000000..3cf2ca6b6291 --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_objectagg_bnf.txt @@ -0,0 +1,9 @@ +"json_objectagg(" expressionOrPredicate ("value"|":") expressionOrPredicate jsonNullClause? uniqueKeysClause? ")" filterClause? + +jsonNullClause + : ("absent"|"null") "on null" + ; + +uniqueKeysClause + : ("with"|"without") "unique keys" + ; \ No newline at end of file diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_query_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_query_bnf.txt new file mode 100644 index 000000000000..d4968e5db33c --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_query_bnf.txt @@ -0,0 +1,14 @@ +"json_query(" expression "," expression passingClause? wrapperClause? onErrorClause? onEmptyClause? ")" + +wrapperClause + : "with" ("conditional"|"unconditional")? "array"? "wrapper" + | "without" "array"? "wrapper" + +passingClause + : "passing" expression "as" identifier ("," expression "as" identifier)* + +onErrorClause + : ( "error" | "null" | ( "empty" ( "array" | "object" )? ) ) "on error"; + +onEmptyClause + : ( "error" | "null" | ( "empty" ( "array" | "object" )? ) ) "on empty"; diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt new file mode 100644 index 000000000000..fe2c284d92cc --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt @@ -0,0 +1,10 @@ +"json_value(" expression "," expression passingClause? ("returning" castTarget)? onErrorClause? onEmptyClause? ")" + +passingClause + : "passing" expression "as" identifier ("," expression "as" identifier)* + +onErrorClause + : ( "error" | "null" | ( "default" expression ) ) "on error"; + +onEmptyClause + : ( "error" | "null" | ( "default" expression ) ) "on empty"; \ 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 eebf2d06edfd..637b22a3b196 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 @@ -93,6 +93,7 @@ import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -257,9 +258,11 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR if ( getVersion().isSameOrAfter( 20 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INET, "inet", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); } else { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); @@ -364,9 +367,11 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser if ( getVersion().isSameOrAfter( 20, 0 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getInetJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) ); } } else { @@ -374,9 +379,11 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser if ( getVersion().isSameOrAfter( 20, 0 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); } } } @@ -386,9 +393,11 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser if ( getVersion().isSameOrAfter( 20, 0 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); } } @@ -488,6 +497,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_cockroachdb(); functionFactory.arrayToString_postgresql(); + functionFactory.jsonValue_cockroachdb(); + functionFactory.jsonQuery_cockroachdb(); + functionFactory.jsonExists_cockroachdb(); + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); + functionFactory.jsonArrayAgg_postgresql( false ); + functionFactory.jsonObjectAgg_postgresql( false ); + // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) .setExactArgumentCount( 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 08b45c6e1546..f819140159bc 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 @@ -430,6 +430,16 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); if ( getDB2Version().isSameOrAfter( 9, 5 ) ) { functionFactory.listagg( null ); + + if ( getDB2Version().isSameOrAfter( 11 ) ) { + functionFactory.jsonValue_no_passing(); + functionFactory.jsonQuery_no_passing(); + functionFactory.jsonExists_no_passing(); + functionFactory.jsonObject_db2(); + functionFactory.jsonArray_db2(); + functionFactory.jsonArrayAgg_db2(); + functionFactory.jsonObjectAgg_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 932f4ccf628e..49ce9210f2a3 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 @@ -98,6 +98,7 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTERVAL_SECOND; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -265,6 +266,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR } if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } } ddlTypeRegistry.addDescriptor( new NativeEnumDdlTypeImpl( this ) ); @@ -295,6 +297,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry } if ( getVersion().isSameOrAfter( 1, 4, 200 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonArrayJdbcType.INSTANCE ); } jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); @@ -399,8 +402,19 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayTrim_trim_array(); functionFactory.arrayFill_h2(); functionFactory.arrayToString_h2( getMaximumArraySize() ); + + if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { + functionFactory.jsonValue_h2(); + functionFactory.jsonQuery_h2(); + functionFactory.jsonExists_h2(); + functionFactory.jsonArrayAgg_h2(); + functionFactory.jsonObjectAgg_h2(); + } } else { + functionFactory.jsonObject(); + functionFactory.jsonArray(); + // Use group_concat until 2.x as listagg was buggy functionFactory.listagg_groupConcat(); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index 2446f29dd462..400dbadc9883 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -272,6 +272,13 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_hsql(); functionFactory.arrayToString_hsql(); + if ( getVersion().isSameOrAfter( 2, 7 ) ) { + functionFactory.jsonObject_hsqldb(); + functionFactory.jsonArray_hsqldb(); + functionFactory.jsonArrayAgg_hsqldb(); + functionFactory.jsonObjectAgg_h2(); + } + //trim() requires parameters to be cast when used as trim character functionContributions.getFunctionRegistry().register( "trim", new TrimFunction( this, 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 d56e791050cb..ab0286695e55 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 @@ -31,6 +31,7 @@ import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; import org.hibernate.type.descriptor.jdbc.JsonJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -91,6 +92,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio .getBasicTypeRegistry() .resolve( StandardBasicTypes.BOOLEAN ) ); + commonFunctionFactory.jsonValue_mariadb(); + commonFunctionFactory.jsonArray_mariadb(); + commonFunctionFactory.jsonQuery_mariadb(); + commonFunctionFactory.jsonArrayAgg_mariadb(); + commonFunctionFactory.jsonObjectAgg_mariadb(); if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) @@ -140,6 +146,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); // Make sure we register the JSON type descriptor before calling super, because MariaDB does not need casting jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, JsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, JsonArrayJdbcType.INSTANCE ); super.contributeTypes( typeContributions, serviceRegistry ); if ( getVersion().isSameOrAfter( 10, 7 ) ) { 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 80d960de4d4f..eddcf730d386 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 @@ -653,6 +653,16 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionRegistry.registerAlternateKey( "char", "chr" ); functionFactory.listagg_groupConcat(); + + if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) { + functionFactory.jsonValue_mysql(); + functionFactory.jsonQuery_mysql(); + functionFactory.jsonExists_mysql(); + functionFactory.jsonObject_mysql(); + functionFactory.jsonArray_mysql(); + functionFactory.jsonArrayAgg_mysql(); + functionFactory.jsonObjectAgg_mysql(); + } } @Override @@ -663,6 +673,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, MySQLCastingJsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, MySQLCastingJsonArrayJdbcType.INSTANCE ); } // MySQL requires a custom binder for binding untyped nulls with the NULL type diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index a85f18f43542..0317a649d1e7 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 @@ -23,20 +23,7 @@ import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.TypeContributions; import org.hibernate.cfg.Environment; -import org.hibernate.dialect.BooleanDecoder; -import org.hibernate.dialect.DmlTargetColumnQualifierSupport; -import org.hibernate.dialect.DatabaseVersion; -import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.OracleBooleanJdbcType; -import org.hibernate.dialect.OracleJdbcHelper; -import org.hibernate.dialect.OracleJsonJdbcType; -import org.hibernate.dialect.OracleReflectionStructJdbcType; -import org.hibernate.dialect.OracleTypes; -import org.hibernate.dialect.OracleUserDefinedTypeExporter; -import org.hibernate.dialect.OracleXmlJdbcType; -import org.hibernate.dialect.Replacer; -import org.hibernate.dialect.RowLockStrategy; -import org.hibernate.dialect.TimeZoneSupport; +import org.hibernate.dialect.*; import org.hibernate.dialect.aggregate.AggregateSupport; import org.hibernate.dialect.aggregate.OracleAggregateSupport; import org.hibernate.dialect.function.CommonFunctionFactory; @@ -104,9 +91,10 @@ import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; import org.hibernate.type.descriptor.jdbc.BlobJdbcType; import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType; import org.hibernate.type.descriptor.jdbc.NullJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType; +import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; +import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType; import org.hibernate.type.descriptor.jdbc.SqlTypedJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.ArrayDdlTypeImpl; @@ -127,6 +115,7 @@ import static org.hibernate.type.SqlTypes.ARRAY; import static org.hibernate.type.SqlTypes.BIGINT; import static org.hibernate.type.SqlTypes.BINARY; +import static org.hibernate.type.SqlTypes.BIT; import static org.hibernate.type.SqlTypes.BOOLEAN; import static org.hibernate.type.SqlTypes.DATE; import static org.hibernate.type.SqlTypes.DECIMAL; @@ -135,6 +124,7 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.REAL; @@ -320,6 +310,15 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayTrim_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); + + if ( getVersion().isSameOrAfter( 12 ) ) { + functionFactory.jsonValue_oracle(); + functionFactory.jsonQuery_oracle(); + functionFactory.jsonExists_oracle(); + functionFactory.jsonObject_oracle(); + functionFactory.jsonArray_oracle(); + functionFactory.jsonArrayAgg_oracle(); + } } @Override @@ -658,6 +657,7 @@ private void extractField(StringBuilder pattern, TemporalUnit unit, TemporalUnit protected String columnType(int sqlTypeCode) { switch ( sqlTypeCode ) { case BOOLEAN: + case BIT: // still, after all these years... return "number(1,0)"; @@ -716,9 +716,11 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "MDSYS.SDO_GEOMETRY", this ) ); if ( getVersion().isSameOrAfter( 21 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } else if ( getVersion().isSameOrAfter( 12 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "blob", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "blob", this ) ); } } @@ -888,9 +890,11 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry if ( getVersion().isSameOrAfter( 21 ) ) { typeContributions.contributeJdbcType( OracleJsonJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( OracleJsonArrayJdbcType.INSTANCE ); } else { typeContributions.contributeJdbcType( OracleJsonBlobJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( OracleJsonArrayBlobJdbcType.INSTANCE ); } } 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 219079710bcf..9abe00cdc8f3 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 @@ -109,6 +109,7 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -258,9 +259,11 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR // Prefer jsonb if possible if ( getVersion().isSameOrAfter( 9, 4 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); } else { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } } ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); @@ -620,6 +623,33 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_postgresql(); functionFactory.arrayToString_postgresql(); + if ( getVersion().isSameOrAfter( 17 ) ) { + functionFactory.jsonValue(); + functionFactory.jsonQuery(); + functionFactory.jsonExists(); + functionFactory.jsonObject(); + functionFactory.jsonArray(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); + } + else { + functionFactory.jsonValue_postgresql(); + functionFactory.jsonQuery_postgresql(); + functionFactory.jsonExists_postgresql(); + if ( getVersion().isSameOrAfter( 16 ) ) { + functionFactory.jsonObject(); + functionFactory.jsonArray(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); + } + else { + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); + functionFactory.jsonArrayAgg_postgresql( false ); + functionFactory.jsonObjectAgg_postgresql( false ); + } + } + if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions @@ -1401,17 +1431,21 @@ protected void contributePostgreSQLTypes(TypeContributions typeContributions, Se if ( getVersion().isSameOrAfter( 9, 4 ) ) { if ( PgJdbcHelper.isUsable( serviceRegistry ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } } else { if ( PgJdbcHelper.isUsable( serviceRegistry ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); } } } @@ -1427,9 +1461,11 @@ protected void contributePostgreSQLTypes(TypeContributions typeContributions, Se if ( getVersion().isSameOrAfter( 9, 2 ) ) { if ( getVersion().isSameOrAfter( 9, 4 ) ) { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSON_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSON_INSTANCE ); } } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index 924247be2641..be790570af90 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 @@ -402,8 +402,17 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); functionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); + if ( getVersion().isSameOrAfter( 13 ) ) { + functionFactory.jsonValue_sqlserver(); + functionFactory.jsonQuery_sqlserver(); + functionFactory.jsonExists_sqlserver(); + functionFactory.jsonObject_sqlserver(); + functionFactory.jsonArray_sqlserver(); + } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); + functionFactory.jsonArrayAgg_sqlserver(); + functionFactory.jsonObjectAgg_sqlserver(); } if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.leastGreatest(); 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 8c5201fdab50..1737e80444a2 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 @@ -145,9 +145,11 @@ VERSIONED : [vV] [eE] [rR] [sS] [iI] [oO] [nN] [eE] [dD]; NATURALID : [nN] [aA] [tT] [uU] [rR] [aA] [lL] [iI] [dD]; FK : [fF] [kK]; +ABSENT : [aA] [bB] [sS] [eE] [nN] [tT]; ALL : [aA] [lL] [lL]; AND : [aA] [nN] [dD]; ANY : [aA] [nN] [yY]; +ARRAY : [aA] [rR] [rR] [aA] [yY]; AS : [aA] [sS]; ASC : [aA] [sS] [cC]; AVG : [aA] [vV] [gG]; @@ -159,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]; +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]; CONTAINS : [cC] [oO] [nN] [tT] [aA] [iI] [nN] [sS]; @@ -221,6 +224,13 @@ 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_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_VALUE : [jJ] [sS] [oO] [nN] '_' [vV] [aA] [lL] [uU] [eE]; KEY : [kK] [eE] [yY]; KEYS : [kK] [eE] [yY] [sS]; LAST : [lL] [aA] [sS] [tT]; @@ -270,6 +280,7 @@ OVERFLOW : [oO] [vV] [eE] [rR] [fF] [lL] [oO] [wW]; 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]; 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]; @@ -277,6 +288,7 @@ PRECEDING : [pP] [rR] [eE] [cC] [eE] [dD] [iI] [nN] [gG]; QUARTER : [qQ] [uU] [aA] [rR] [tT] [eE] [rR]; RANGE : [rR] [aA] [nN] [gG] [eE]; RESPECT : [rR] [eE] [sS] [pP] [eE] [cC] [tT]; +RETURNING : [rR] [eE] [tT] [uU] [rR] [nN] [iI] [nN] [gG]; RIGHT : [rR] [iI] [gG] [hH] [tT]; ROLLUP : [rR] [oO] [lL] [lL] [uU] [pP]; ROW : [rR] [oO] [wW]; @@ -303,7 +315,9 @@ TRUNC : [tT] [rR] [uU] [nN] [cC]; TRUNCATE : [tT] [rR] [uU] [nN] [cC] [aA] [tT] [eE]; TYPE : [tT] [yY] [pP] [eE]; UNBOUNDED : [uU] [nN] [bB] [oO] [uU] [nN] [dD] [eE] [dD]; +UNCONDITIONAL : [uU] [nN] [cC] [oO] [nN] [dD] [iI] [tT] [iI] [oO] [nN] [aA] [lL]; UNION : [uU] [nN] [iI] [oO] [nN]; +UNIQUE : [uU] [nN] [iI] [qQ] [uU] [eE]; UPDATE : [uU] [pP] [dD] [aA] [tT] [eE]; USING : [uU] [sS] [iI] [nN] [gG]; VALUE : [vV] [aA] [lL] [uU] [eE]; @@ -314,6 +328,7 @@ WHERE : [wW] [hH] [eE] [rR] [eE]; WITH : [wW] [iI] [tT] [hH]; WITHIN : [wW] [iI] [tT] [hH] [iI] [nN]; WITHOUT : [wW] [iI] [tT] [hH] [oO] [uU] [tT]; +WRAPPER : [wW] [rR] [aA] [pP] [pP] [eE] [rR]; YEAR : [yY] [eE] [aA] [rR]; ZONED : [zZ] [oO] [nN] [eE] [dD]; 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 b9a113092a4d..dfcc5aa1a526 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 @@ -1109,6 +1109,7 @@ function | collectionFunctionMisuse | jpaNonstandardFunction | columnFunction + | jsonFunction | genericFunction ; @@ -1620,6 +1621,101 @@ rollup : ROLLUP LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)* RIGHT_PAREN ; +jsonFunction + : jsonArrayFunction + | jsonExistsFunction + | jsonObjectFunction + | jsonQueryFunction + | jsonValueFunction + | jsonArrayAggFunction + | jsonObjectAggFunction + ; + +/** + * The 'json_value()' function + */ +jsonValueFunction + : JSON_VALUE LEFT_PAREN expression COMMA expression jsonPassingClause? jsonValueReturningClause? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? RIGHT_PAREN + ; + +jsonPassingClause + : PASSING expressionOrPredicate AS identifier (COMMA expressionOrPredicate AS identifier)* + ; + +jsonValueReturningClause + : RETURNING castTarget + ; + +jsonValueOnErrorOrEmptyClause + : ( ERROR | NULL | ( DEFAULT expression ) ) ON (ERROR|EMPTY) + ; + +/** + * The 'json_query()' function + */ +jsonQueryFunction + : JSON_QUERY LEFT_PAREN expression COMMA expression jsonPassingClause? jsonQueryWrapperClause? jsonQueryOnErrorOrEmptyClause? jsonQueryOnErrorOrEmptyClause? RIGHT_PAREN + ; + +jsonQueryWrapperClause + : WITH (CONDITIONAL|UNCONDITIONAL)? ARRAY? WRAPPER + | WITHOUT ARRAY? WRAPPER + ; + +jsonQueryOnErrorOrEmptyClause + : ( ERROR | NULL | ( EMPTY ( ARRAY | OBJECT )? ) ) ON (ERROR|EMPTY); + +/** + * The 'json_exists()' function + */ +jsonExistsFunction + : JSON_EXISTS LEFT_PAREN expression COMMA expression jsonPassingClause? jsonExistsOnErrorClause? RIGHT_PAREN + ; + +jsonExistsOnErrorClause + : ( ERROR | TRUE | FALSE ) ON ERROR; + +/** + * The 'json_array()' function + */ +jsonArrayFunction + : JSON_ARRAY LEFT_PAREN (expressionOrPredicate (COMMA expressionOrPredicate)* jsonNullClause?)? RIGHT_PAREN + ; + +/** + * The 'json_object()' function + */ +jsonObjectFunction + : JSON_OBJECT LEFT_PAREN (jsonObjectFunctionEntries jsonNullClause?)? RIGHT_PAREN + ; + +jsonObjectFunctionEntries + : expressionOrPredicate COMMA expressionOrPredicate (COMMA expressionOrPredicate COMMA expressionOrPredicate)* + | (KEY? expressionOrPredicate VALUE expressionOrPredicate | expressionOrPredicate COLON expressionOrPredicate) (COMMA (KEY? expressionOrPredicate VALUE expressionOrPredicate | expressionOrPredicate COLON expressionOrPredicate))* + ; + +jsonNullClause + : (ABSENT|NULL) ON NULL + ; + +/** + * The 'json_arrayagg()' function + */ +jsonArrayAggFunction + : JSON_ARRAYAGG LEFT_PAREN expressionOrPredicate jsonNullClause? orderByClause? RIGHT_PAREN filterClause? + ; + +/** + * The 'json_objectagg()' function + */ +jsonObjectAggFunction + : JSON_OBJECTAGG LEFT_PAREN KEY? expressionOrPredicate (VALUE|COLON) expressionOrPredicate jsonNullClause? jsonUniqueKeysClause? RIGHT_PAREN filterClause? + ; + +jsonUniqueKeysClause + : (WITH|WITHOUT) UNIQUE KEYS + ; + /** * Support for "soft" keywords which may be used as identifiers * @@ -1636,9 +1732,11 @@ rollup nakedIdentifier : IDENTIFIER | QUOTED_IDENTIFIER - | (ALL + | (ABSENT + | ALL | AND | ANY + | ARRAY | AS | ASC | AVG @@ -1650,6 +1748,7 @@ rollup | CAST | COLLATE | COLUMN + | CONDITIONAL | CONFLICT | CONSTRAINT | CONTAINS @@ -1714,6 +1813,13 @@ rollup | INTO | IS | JOIN + | JSON_ARRAY + | JSON_ARRAYAGG + | JSON_EXISTS + | JSON_OBJECT + | JSON_OBJECTAGG + | JSON_QUERY + | JSON_VALUE | KEY | KEYS | LAST @@ -1764,6 +1870,7 @@ rollup | OVERLAY | PAD | PARTITION + | PASSING | PERCENT | PLACING | POSITION @@ -1771,6 +1878,7 @@ rollup | QUARTER | RANGE | RESPECT + | RETURNING // | RIGHT | ROLLUP | ROW @@ -1797,7 +1905,9 @@ rollup | TRUNCATE | TYPE | UNBOUNDED + | UNCONDITIONAL | UNION + | UNIQUE | UPDATE | USING | VALUE @@ -1810,6 +1920,7 @@ rollup | WITH | WITHIN | WITHOUT + | WRAPPER | YEAR | ZONED) { logUseOfReservedWordAsIdentifier( getCurrentToken() ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index fbd6ab712c38..6d66b5935168 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -132,6 +132,7 @@ import static org.hibernate.cfg.CacheSettings.QUERY_CACHE_LAYOUT; import static org.hibernate.cfg.PersistenceSettings.UNOWNED_ASSOCIATION_TRANSIENT_CHECK; import static org.hibernate.cfg.QuerySettings.DEFAULT_NULL_ORDERING; +import static org.hibernate.cfg.QuerySettings.JSON_FUNCTIONS_ENABLED; import static org.hibernate.cfg.QuerySettings.PORTABLE_INTEGER_DIVISION; import static org.hibernate.engine.config.spi.StandardConverters.BOOLEAN; import static org.hibernate.internal.CoreLogging.messageLogger; @@ -278,6 +279,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { private final boolean inClauseParameterPaddingEnabled; private final boolean portableIntegerDivisionEnabled; + private final boolean jsonFunctionsEnabled; private final int queryStatisticsMaxSize; @@ -618,6 +620,10 @@ else if ( jdbcTimeZoneValue != null ) { PORTABLE_INTEGER_DIVISION, configurationSettings ); + this.jsonFunctionsEnabled = getBoolean( + JSON_FUNCTIONS_ENABLED, + configurationSettings + ); this.queryStatisticsMaxSize = getInt( QUERY_STATISTICS_MAX_SIZE, @@ -1248,6 +1254,11 @@ public boolean inClauseParameterPaddingEnabled() { return this.inClauseParameterPaddingEnabled; } + @Override + public boolean isJsonFunctionsEnabled() { + return jsonFunctionsEnabled; + } + @Override public boolean isPortableIntegerDivisionEnabled() { return portableIntegerDivisionEnabled; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java index a1d9042ef9ff..746909e192d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java @@ -87,6 +87,7 @@ import org.hibernate.type.descriptor.java.CharacterArrayJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayAsStringJdbcType; import org.hibernate.type.descriptor.jdbc.JsonAsStringJdbcType; import org.hibernate.type.descriptor.jdbc.XmlAsStringJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; @@ -825,6 +826,7 @@ public void contributeType(CompositeUserType type) { addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.GEOGRAPHY, SqlTypes.GEOMETRY ); jdbcTypeRegistry.addDescriptorIfAbsent( JsonAsStringJdbcType.VARCHAR_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( JsonArrayAsStringJdbcType.VARCHAR_INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( XmlAsStringJdbcType.VARCHAR_INSTANCE ); addFallbackIfNecessary( jdbcTypeRegistry, SqlTypes.MATERIALIZED_BLOB, SqlTypes.BLOB ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java index 93256b10c7a9..b8aa75d266f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java @@ -423,6 +423,11 @@ public boolean inClauseParameterPaddingEnabled() { return delegate.inClauseParameterPaddingEnabled(); } + @Override + public boolean isJsonFunctionsEnabled() { + return delegate.isJsonFunctionsEnabled(); + } + @Override public boolean isPortableIntegerDivisionEnabled() { return delegate.isPortableIntegerDivisionEnabled(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java index 27d2d4e33568..b7db846b0fa1 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java @@ -268,6 +268,14 @@ default boolean inClauseParameterPaddingEnabled() { return false; } + /** + * @see org.hibernate.cfg.AvailableSettings#JSON_FUNCTIONS_ENABLED + */ + @Override + default boolean isJsonFunctionsEnabled() { + return false; + } + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java index bf0fc9571831..4f109cdb2db5 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java @@ -6,6 +6,7 @@ */ package org.hibernate.cfg; +import org.hibernate.Incubating; import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.query.spi.QueryPlan; @@ -17,6 +18,14 @@ * @author Steve Ebersole */ public interface QuerySettings { + /** + * Boolean setting to control if the use of tech preview JSON functions in HQL is enabled. + * By default, this is {@code false} i.e. disabled since the functions are still incubating. + * + * @since 7.0 + */ + @Incubating + String JSON_FUNCTIONS_ENABLED = "hibernate.query.hql.json_functions_enabled"; /** * Specifies that division of two integers should produce an integer on all * databases. By default, integer division in HQL can produce a non-integer diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java new file mode 100644 index 000000000000..2b6c337e0728 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java @@ -0,0 +1,106 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.BasicExtractor; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; +import org.hibernate.type.descriptor.jdbc.JsonJdbcType; + +import org.postgresql.util.PGobject; + +/** + * @author Christian Beikov + */ +public abstract class AbstractPostgreSQLJsonArrayPGObjectType extends JsonArrayJdbcType { + + private final boolean jsonb; + protected AbstractPostgreSQLJsonArrayPGObjectType(boolean jsonb) { + this.jsonb = jsonb; + } + + @Override + public int getJdbcTypeCode() { + return SqlTypes.OTHER; + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String stringValue = ( (AbstractPostgreSQLJsonArrayPGObjectType) getJdbcType() ).toString( + value, + getJavaType(), + options + ); + final PGobject holder = new PGobject(); + holder.setType( jsonb ? "jsonb" : "json" ); + holder.setValue( stringValue ); + st.setObject( index, holder ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String stringValue = ( (AbstractPostgreSQLJsonArrayPGObjectType) getJdbcType() ).toString( + value, + getJavaType(), + options + ); + final PGobject holder = new PGobject(); + holder.setType( jsonb ? "jsonb" : "json" ); + holder.setValue( stringValue ); + st.setObject( name, holder ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return getObject( rs.getObject( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return getObject( statement.getObject( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + return getObject( statement.getObject( name ), options ); + } + + private X getObject(Object object, WrapperOptions options) throws SQLException { + if ( object == null ) { + return null; + } + return ( (AbstractPostgreSQLJsonArrayPGObjectType) getJdbcType() ).fromString( + object.toString(), + getJavaType(), + options + ); + } + }; + } +} 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 1b5ad4046e25..ba8e57a9f88a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -96,6 +96,7 @@ import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -255,6 +256,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR // Prefer jsonb if possible ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INET, "inet", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); @@ -350,11 +352,13 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getIntervalJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getInetJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } } else { @@ -362,6 +366,7 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } // Force Blob binding to byte[] for CockroachDB @@ -460,6 +465,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_cockroachdb(); functionFactory.arrayToString_postgresql(); + functionFactory.jsonValue_cockroachdb(); + functionFactory.jsonQuery_cockroachdb(); + functionFactory.jsonExists_cockroachdb(); + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); + functionFactory.jsonArrayAgg_postgresql( false ); + functionFactory.jsonObjectAgg_postgresql( false ); + // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) .setExactArgumentCount( 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 8b58326f883d..b6aaccbc7721 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -416,6 +416,16 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); functionFactory.listagg( null ); + + if ( getDB2Version().isSameOrAfter( 11 ) ) { + functionFactory.jsonValue_no_passing(); + functionFactory.jsonQuery_no_passing(); + functionFactory.jsonExists_no_passing(); + functionFactory.jsonObject_db2(); + functionFactory.jsonArray_db2(); + functionFactory.jsonArrayAgg_db2(); + functionFactory.jsonObjectAgg_db2(); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index c5c94647c8ac..298588469c84 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -4548,15 +4548,7 @@ public boolean supportsWait() { * @apiNote Needed because MySQL has nonstandard escape characters */ public void appendLiteral(SqlAppender appender, String literal) { - appender.appendSql( '\'' ); - for ( int i = 0; i < literal.length(); i++ ) { - final char c = literal.charAt( i ); - if ( c == '\'' ) { - appender.appendSql( '\'' ); - } - appender.appendSql( c ); - } - appender.appendSql( '\'' ); + appender.appendSingleQuoteEscapedString( literal ); } /** 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 92011616a55a..6129eaad788f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -90,6 +90,7 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTERVAL_SECOND; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -229,6 +230,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "geometry", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( INTERVAL_SECOND, "interval second($p,$s)", this ) ); ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); ddlTypeRegistry.addDescriptor( new NativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NativeOrdinalEnumDdlTypeImpl( this ) ); } @@ -245,6 +247,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry jdbcTypeRegistry.addDescriptorIfAbsent( UUIDJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( H2DurationIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( H2JsonArrayJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); } @@ -340,6 +343,16 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayTrim_trim_array(); functionFactory.arrayFill_h2(); functionFactory.arrayToString_h2( getMaximumArraySize() ); + + functionFactory.jsonObject(); + functionFactory.jsonArray(); + if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { + functionFactory.jsonValue_h2(); + functionFactory.jsonQuery_h2(); + functionFactory.jsonExists_h2(); + functionFactory.jsonArrayAgg_h2(); + functionFactory.jsonObjectAgg_h2(); + } } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java new file mode 100644 index 000000000000..fa2bfcb07c00 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.BasicBinder; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; + +/** + * H2 requires binding JSON via {@code setBytes} methods. + */ +public class H2JsonArrayJdbcType extends JsonArrayJdbcType { + /** + * Singleton access + */ + public static final H2JsonArrayJdbcType INSTANCE = new H2JsonArrayJdbcType(); + + protected H2JsonArrayJdbcType() { + } + + @Override + public String toString() { + return "H2JsonArrayJdbcType"; + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String json = ( (H2JsonArrayJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setBytes( index, json.getBytes( StandardCharsets.UTF_8 ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String json = ( (H2JsonArrayJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setBytes( name, json.getBytes( StandardCharsets.UTF_8 ) ); + } + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java index b1816ee8cf2f..0c5d65af5cde 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java @@ -35,7 +35,7 @@ protected H2JsonJdbcType(EmbeddableMappingType embeddableMappingType) { @Override public String toString() { - return "FormatJsonJdbcType"; + return "H2JsonJdbcType"; } @Override 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 f755dd545196..945e6b62d0c6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -490,6 +490,20 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio ANY, ANY, ANY, typeConfiguration ); + + if ( getVersion().isSameOrAfter(2, 0, 20) ) { + // Introduced in 2.0 SPS 02 + functionFactory.jsonValue_no_passing(); + functionFactory.jsonQuery_no_passing(); + functionFactory.jsonExists_hana(); + if ( getVersion().isSameOrAfter(2, 0, 40) ) { + // Introduced in 2.0 SPS 04 + functionFactory.jsonObject_hana(); + functionFactory.jsonArray_hana(); + functionFactory.jsonArrayAgg_hana(); + functionFactory.jsonObjectAgg_hana(); + } + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index 68b7e4eb0204..cff35e0f2fe1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -207,6 +207,13 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_hsql(); functionFactory.arrayToString_hsql(); + if ( getVersion().isSameOrAfter( 2, 7 ) ) { + functionFactory.jsonObject_hsqldb(); + functionFactory.jsonArray_hsqldb(); + functionFactory.jsonArrayAgg_hsqldb(); + functionFactory.jsonObjectAgg_h2(); + } + //trim() requires parameters to be cast when used as trim character functionContributions.getFunctionRegistry().register( "trim", new TrimFunction( this, 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 a4ca109f3252..9baa7799150c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -32,6 +32,7 @@ import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; import org.hibernate.type.descriptor.jdbc.JsonJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; @@ -94,6 +95,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio .getBasicTypeRegistry() .resolve( StandardBasicTypes.BOOLEAN ) ); + commonFunctionFactory.jsonValue_mariadb(); + commonFunctionFactory.jsonArray_mariadb(); + commonFunctionFactory.jsonQuery_mariadb(); + commonFunctionFactory.jsonArrayAgg_mariadb(); + commonFunctionFactory.jsonObjectAgg_mariadb(); commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) .setInvariantType( functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) ) @@ -147,6 +153,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); // Make sure we register the JSON type descriptor before calling super, because MariaDB does not need casting jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, JsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, JsonArrayJdbcType.INSTANCE ); super.contributeTypes( typeContributions, serviceRegistry ); if ( getVersion().isSameOrAfter( 10, 7 ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java new file mode 100644 index 000000000000..c95e2e063532 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java @@ -0,0 +1,30 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect; + +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; + +/** + * @author Christian Beikov + */ +public class MySQLCastingJsonArrayJdbcType extends JsonArrayJdbcType { + /** + * Singleton access + */ + public static final JsonArrayJdbcType INSTANCE = new MySQLCastingJsonArrayJdbcType(); + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as json)" ); + } +} 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 5ccacaa7afe6..2ac77c5ef9e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -289,6 +289,11 @@ public boolean useMaterializedLobWhenCapacityExceeded() { // MySQL has no real concept of LOBs, so we can just use longtext/longblob with the materialized JDBC APIs return false; } + @Override + public void appendBooleanValueString(SqlAppender appender, boolean bool) { + // Use the true/false constants since these evaluate to true/false literals in JSON functions + appender.appendSql( bool ); + } @Override protected String castType(int sqlTypeCode) { @@ -634,6 +639,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } functionFactory.listagg_groupConcat(); + + functionFactory.jsonValue_mysql(); + functionFactory.jsonQuery_mysql(); + functionFactory.jsonExists_mysql(); + functionFactory.jsonObject_mysql(); + functionFactory.jsonArray_mysql(); + functionFactory.jsonArrayAgg_mysql(); + functionFactory.jsonObjectAgg_mysql(); } @Override @@ -643,6 +656,7 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, MySQLCastingJsonJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON_ARRAY, MySQLCastingJsonArrayJdbcType.INSTANCE ); // MySQL requires a custom binder for binding untyped nulls with the NULL type typeContributions.contributeJdbcType( NullJdbcType.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleBooleanJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleBooleanJdbcType.java index 0e2621158c5d..062b400b44d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleBooleanJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleBooleanJdbcType.java @@ -6,6 +6,7 @@ */ package org.hibernate.dialect; +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.BooleanJdbcType; @@ -14,6 +15,11 @@ public class OracleBooleanJdbcType extends BooleanJdbcType { public static final OracleBooleanJdbcType INSTANCE = new OracleBooleanJdbcType(); + @Override + public int getDdlTypeCode() { + return SqlTypes.BIT; + } + @Override public String getCheckCondition(String columnName, JavaType javaType, BasicValueConverter converter, Dialect dialect) { return columnName + " in (0,1)"; 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 3bfc63b02b02..65f25a67f521 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -48,8 +48,6 @@ import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; -import org.hibernate.internal.util.JdbcExceptionHelper; -import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.config.ConfigurationHelper; import org.hibernate.mapping.UserDefinedType; import org.hibernate.mapping.CheckConstraint; @@ -93,6 +91,7 @@ import org.hibernate.type.descriptor.jdbc.NullJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectJdbcType; import org.hibernate.type.descriptor.jdbc.ObjectNullAsNullTypeJdbcType; +import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; import org.hibernate.type.descriptor.jdbc.OracleJsonBlobJdbcType; import org.hibernate.type.descriptor.jdbc.SqlTypedJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; @@ -127,6 +126,7 @@ import static org.hibernate.type.SqlTypes.ARRAY; import static org.hibernate.type.SqlTypes.BIGINT; import static org.hibernate.type.SqlTypes.BINARY; +import static org.hibernate.type.SqlTypes.BIT; import static org.hibernate.type.SqlTypes.BOOLEAN; import static org.hibernate.type.SqlTypes.DATE; import static org.hibernate.type.SqlTypes.DECIMAL; @@ -135,6 +135,7 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INTEGER; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.NUMERIC; import static org.hibernate.type.SqlTypes.NVARCHAR; import static org.hibernate.type.SqlTypes.REAL; @@ -279,9 +280,20 @@ protected DatabaseVersion getMinimumSupportedVersion() { @Override public int getPreferredSqlTypeCodeForBoolean() { + // starting 23c we support Boolean type natively return getVersion().isSameOrAfter( 23 ) ? super.getPreferredSqlTypeCodeForBoolean() : Types.BIT; } + @Override + public void appendBooleanValueString(SqlAppender appender, boolean bool) { + if ( getVersion().isSameOrAfter( 23 ) ) { + appender.appendSql( bool ); + } + else { + super.appendBooleanValueString( appender, bool ); + } + } + @Override public void initializeFunctionRegistry(FunctionContributions functionContributions) { super.initializeFunctionRegistry(functionContributions); @@ -390,6 +402,13 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayTrim_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); + + functionFactory.jsonValue_oracle(); + functionFactory.jsonQuery_oracle(); + functionFactory.jsonExists_oracle(); + functionFactory.jsonObject_oracle(); + functionFactory.jsonArray_oracle(); + functionFactory.jsonArrayAgg_oracle(); } @Override @@ -723,9 +742,8 @@ protected String columnType(int sqlTypeCode) { if ( getVersion().isSameOrAfter( 23 ) ) { return super.columnType( sqlTypeCode ); } - else { - return "number(1,0)"; - } + case BIT: + return "number(1,0)"; case TINYINT: return "number(3,0)"; case SMALLINT: @@ -779,18 +797,22 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR ddlTypeRegistry.addDescriptor( new DdlTypeImpl( GEOMETRY, "MDSYS.SDO_GEOMETRY", this ) ); if ( getVersion().isSameOrAfter( 21 ) ) { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "json", this ) ); } else { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "blob", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "blob", this ) ); } ddlTypeRegistry.addDescriptor( new ArrayDdlTypeImpl( this, false ) ); ddlTypeRegistry.addDescriptor( TABLE, new ArrayDdlTypeImpl( this, false ) ); - if(getVersion().isSameOrAfter(23)) { - ddlTypeRegistry.addDescriptor(new NamedNativeEnumDdlTypeImpl(this)); + if ( getVersion().isSameOrAfter( 23 ) ) { + ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); } + // We need the DDL type during runtime to produce the proper encoding in certain functions + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( BIT, "number(1,0)", this ) ); } @Override @@ -930,8 +952,7 @@ public Exporter getUserDefinedTypeExporter() { @Override public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { super.contributeTypes( typeContributions, serviceRegistry ); - if ( getVersion().isBefore( 23 ) ) { - // starting 23c we support Boolean type natively + if ( ConfigurationHelper.getPreferredSqlTypeCodeForBoolean( serviceRegistry, this ) == BIT ) { typeContributions.contributeJdbcType( OracleBooleanJdbcType.INSTANCE ); } typeContributions.contributeJdbcType( OracleXmlJdbcType.INSTANCE ); @@ -950,9 +971,11 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry if ( getVersion().isSameOrAfter( 21 ) ) { typeContributions.contributeJdbcType( OracleJsonJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( OracleJsonArrayJdbcType.INSTANCE ); } else { typeContributions.contributeJdbcType( OracleJsonBlobJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( OracleJsonArrayBlobJdbcType.INSTANCE ); } if ( OracleJdbcHelper.isUsable( serviceRegistry ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java new file mode 100644 index 000000000000..c4652262aa5f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java @@ -0,0 +1,37 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect; + +import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.OracleJsonArrayBlobJdbcType; + +/** + * Specialized type mapping for {@code JSON} and the JSON SQL data type for Oracle. + * + * @author Christian Beikov + */ +public class OracleJsonArrayJdbcType extends OracleJsonArrayBlobJdbcType { + /** + * Singleton access + */ + public static final OracleJsonArrayJdbcType INSTANCE = new OracleJsonArrayJdbcType(); + + private OracleJsonArrayJdbcType() { + } + + @Override + public String toString() { + return "OracleJsonJdbcType"; + } + + @Override + public String getCheckCondition(String columnName, JavaType javaType, BasicValueConverter converter, Dialect dialect) { + // No check constraint necessary, because the JSON DDL type is already OSON encoded + return null; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java index 6ad591ca07de..d358708f3f33 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java @@ -54,6 +54,14 @@ public static JdbcType getJsonbJdbcType(ServiceRegistry serviceRegistry) { return createJdbcType( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonPGObjectJsonbType" ); } + public static JdbcType getJsonArrayJdbcType(ServiceRegistry serviceRegistry) { + return createJdbcType( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonArrayPGObjectJsonType" ); + } + + public static JdbcType getJsonbArrayJdbcType(ServiceRegistry serviceRegistry) { + return createJdbcType( serviceRegistry, "org.hibernate.dialect.PostgreSQLJsonArrayPGObjectJsonbType" ); + } + public static JdbcType createJdbcType(ServiceRegistry serviceRegistry, String className) { final ClassLoaderService classLoaderService = serviceRegistry.requireService( ClassLoaderService.class ); try { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java new file mode 100644 index 000000000000..9726402cae34 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java @@ -0,0 +1,41 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect; + +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType; + +/** + * @author Christian Beikov + */ +public class PostgreSQLCastingJsonArrayJdbcType extends JsonArrayJdbcType { + + public static final PostgreSQLCastingJsonArrayJdbcType JSON_INSTANCE = new PostgreSQLCastingJsonArrayJdbcType( false ); + public static final PostgreSQLCastingJsonArrayJdbcType JSONB_INSTANCE = new PostgreSQLCastingJsonArrayJdbcType( true ); + + private final boolean jsonb; + + public PostgreSQLCastingJsonArrayJdbcType(boolean jsonb) { + this.jsonb = jsonb; + } + + @Override + public void appendWriteExpression( + String writeExpression, + SqlAppender appender, + Dialect dialect) { + appender.append( "cast(" ); + appender.append( writeExpression ); + appender.append( " as " ); + if ( jsonb ) { + appender.append( "jsonb)" ); + } + else { + appender.append( "json)" ); + } + } +} 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 2cc8f3ba8b60..33a1e2cb876c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -111,6 +111,7 @@ import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.INET; import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.JSON_ARRAY; import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; import static org.hibernate.type.SqlTypes.LONG32VARBINARY; import static org.hibernate.type.SqlTypes.LONG32VARCHAR; @@ -260,6 +261,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR // Prefer jsonb if possible ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "jsonb", this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeEnumDdlTypeImpl( this ) ); ddlTypeRegistry.addDescriptor( new NamedNativeOrdinalEnumDdlTypeImpl( this ) ); @@ -582,6 +584,33 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_postgresql(); functionFactory.arrayToString_postgresql(); + if ( getVersion().isSameOrAfter( 17 ) ) { + functionFactory.jsonValue(); + functionFactory.jsonQuery(); + functionFactory.jsonExists(); + functionFactory.jsonObject(); + functionFactory.jsonArray(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); + } + else { + functionFactory.jsonValue_postgresql(); + functionFactory.jsonQuery_postgresql(); + functionFactory.jsonExists_postgresql(); + if ( getVersion().isSameOrAfter( 16 ) ) { + functionFactory.jsonObject(); + functionFactory.jsonArray(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); + } + else { + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); + functionFactory.jsonArrayAgg_postgresql( false ); + functionFactory.jsonObjectAgg_postgresql( false ); + } + } + functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions functionFactory.inverseDistributionOrderedSetAggregates(); @@ -1357,12 +1386,14 @@ protected void contributePostgreSQLTypes(TypeContributions typeContributions, Se jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getIntervalJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getStructJdbcType( serviceRegistry ) ); jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbJdbcType( serviceRegistry ) ); + jdbcTypeRegistry.addDescriptorIfAbsent( PgJdbcHelper.getJsonbArrayJdbcType( serviceRegistry ) ); } else { jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingInetJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLStructCastingJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } } else { @@ -1370,6 +1401,7 @@ protected void contributePostgreSQLTypes(TypeContributions typeContributions, Se jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingIntervalSecondJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLStructCastingJdbcType.INSTANCE ); jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonJdbcType.JSONB_INSTANCE ); + jdbcTypeRegistry.addDescriptorIfAbsent( PostgreSQLCastingJsonArrayJdbcType.JSONB_INSTANCE ); } // PostgreSQL requires a custom binder for binding untyped nulls as VARBINARY diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java new file mode 100644 index 000000000000..0ed27bc854f8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java @@ -0,0 +1,16 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect; + +/** + * @author Christian Beikov + */ +public class PostgreSQLJsonArrayPGObjectJsonType extends AbstractPostgreSQLJsonArrayPGObjectType { + public PostgreSQLJsonArrayPGObjectJsonType() { + super( false ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java new file mode 100644 index 000000000000..ba1fded5552e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java @@ -0,0 +1,16 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect; + +/** + * @author Christian Beikov + */ +public class PostgreSQLJsonArrayPGObjectJsonbType extends AbstractPostgreSQLJsonArrayPGObjectType { + public PostgreSQLJsonArrayPGObjectJsonbType() { + super( true ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 2e6a68f5aff2..1e0a36776cff 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -53,9 +53,7 @@ import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; -import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.internal.util.config.ConfigurationHelper; -import org.hibernate.internal.util.StringHelper; import org.hibernate.mapping.CheckConstraint; import org.hibernate.mapping.Column; import org.hibernate.persister.entity.mutation.EntityMutationTarget; @@ -422,8 +420,17 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); functionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); + if ( getVersion().isSameOrAfter( 13 ) ) { + functionFactory.jsonValue_sqlserver(); + functionFactory.jsonQuery_sqlserver(); + functionFactory.jsonExists_sqlserver(); + functionFactory.jsonObject_sqlserver(); + functionFactory.jsonArray_sqlserver(); + } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); + functionFactory.jsonArrayAgg_sqlserver(); + functionFactory.jsonObjectAgg_sqlserver(); } if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.leastGreatest(); 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 e6b685fe2467..0cac3d238d9e 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 @@ -43,6 +43,7 @@ import static org.hibernate.type.SqlTypes.ARRAY; import static org.hibernate.type.SqlTypes.BIGINT; import static org.hibernate.type.SqlTypes.BINARY; +import static org.hibernate.type.SqlTypes.BIT; import static org.hibernate.type.SqlTypes.BLOB; import static org.hibernate.type.SqlTypes.BOOLEAN; import static org.hibernate.type.SqlTypes.CLOB; @@ -134,6 +135,11 @@ public String aggregateComponentCustomReadExpression( parentPartExpression = aggregateParentReadExpression + ",'$."; } switch ( column.getTypeCode() ) { + case BIT: + return template.replace( + placeholder, + "decode(json_value(" + parentPartExpression + columnExpression + "'),'true',1,'false',0,null)" + ); case BOOLEAN: if ( column.getTypeName().toLowerCase( Locale.ROOT ).trim().startsWith( "number" ) ) { return template.replace( @@ -268,6 +274,8 @@ private String jsonCustomWriteExpression( switch ( jdbcType.getElementJdbcType().getDefaultSqlTypeCode() ) { case CLOB: return "(select json_arrayagg(to_clob(t.column_value)) from table(" + customWriteExpression + ") t)"; + case BIT: + return "decode(" + customWriteExpression + ",1,'true',0,'false',null)"; case BOOLEAN: final String elementTypeName = determineElementTypeName( column.toSize(), pluralType, typeConfiguration ); if ( elementTypeName.toLowerCase( Locale.ROOT ).trim().startsWith( "number" ) ) { @@ -276,6 +284,9 @@ private String jsonCustomWriteExpression( default: break; } + return customWriteExpression; + case BIT: + return "decode(" + customWriteExpression + ",1,'true',0,'false',null)"; case BOOLEAN: final String sqlTypeName = AbstractSqlAstTranslator.getSqlTypeName( column, typeConfiguration ); if ( sqlTypeName.toLowerCase( Locale.ROOT ).trim().startsWith( "number" ) ) { 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 0fc7902c1f08..076d3f8e2bda 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 @@ -78,6 +78,62 @@ import org.hibernate.dialect.function.array.OracleArrayContainsFunction; import org.hibernate.dialect.function.array.PostgreSQLArrayPositionsFunction; import org.hibernate.dialect.function.array.PostgreSQLArrayTrimEmulation; +import org.hibernate.dialect.function.json.CockroachDBJsonExistsFunction; +import org.hibernate.dialect.function.json.CockroachDBJsonQueryFunction; +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.JsonArrayAggFunction; +import org.hibernate.dialect.function.json.JsonArrayFunction; +import org.hibernate.dialect.function.json.JsonExistsFunction; +import org.hibernate.dialect.function.json.JsonObjectAggFunction; +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.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.OracleJsonArrayFunction; +import org.hibernate.dialect.function.json.OracleJsonObjectFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; +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.PostgreSQLJsonValueFunction; +import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; +import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; +import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; +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.SQLServerJsonValueFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; @@ -3322,4 +3378,424 @@ public void arrayToString_postgresql() { public void arrayToString_oracle() { functionRegistry.register( "array_to_string", new OracleArrayToStringFunction( typeConfiguration ) ); } + + /** + * json_value() function + */ + public void jsonValue() { + functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true, true ) ); + } + + /** + * json_value() function that doesn't support the passing clause + */ + public void jsonValue_no_passing() { + functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true, false ) ); + } + + /** + * Oracle json_value() function + */ + public void jsonValue_oracle() { + functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, false, false ) ); + } + + /** + * PostgreSQL json_value() function + */ + public void jsonValue_postgresql() { + functionRegistry.register( "json_value", new PostgreSQLJsonValueFunction( typeConfiguration ) ); + } + + /** + * CockroachDB json_value() function + */ + public void jsonValue_cockroachdb() { + functionRegistry.register( "json_value", new CockroachDBJsonValueFunction( typeConfiguration ) ); + } + + /** + * MySQL json_value() function + */ + public void jsonValue_mysql() { + functionRegistry.register( "json_value", new MySQLJsonValueFunction( typeConfiguration ) ); + } + + /** + * MariaDB json_value() function + */ + public void jsonValue_mariadb() { + functionRegistry.register( "json_value", new MariaDBJsonValueFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_value() function + */ + public void jsonValue_sqlserver() { + functionRegistry.register( "json_value", new SQLServerJsonValueFunction( typeConfiguration ) ); + } + + /** + * H2 json_value() function + */ + public void jsonValue_h2() { + functionRegistry.register( "json_value", new H2JsonValueFunction( typeConfiguration ) ); + } + + /** + * json_query() function + */ + public void jsonQuery() { + functionRegistry.register( "json_query", new JsonQueryFunction( typeConfiguration, true, true ) ); + } + + /** + * json_query() function + */ + public void jsonQuery_no_passing() { + functionRegistry.register( "json_query", new JsonQueryFunction( typeConfiguration, true, false ) ); + } + + /** + * Oracle json_query() function + */ + public void jsonQuery_oracle() { + functionRegistry.register( "json_query", new JsonQueryFunction( typeConfiguration, false, false ) ); + } + + /** + * PostgreSQL json_query() function + */ + public void jsonQuery_postgresql() { + functionRegistry.register( "json_query", new PostgreSQLJsonQueryFunction( typeConfiguration ) ); + } + + /** + * CockroachDB json_query() function + */ + public void jsonQuery_cockroachdb() { + functionRegistry.register( "json_query", new CockroachDBJsonQueryFunction( typeConfiguration ) ); + } + + /** + * MySQL json_query() function + */ + public void jsonQuery_mysql() { + functionRegistry.register( "json_query", new MySQLJsonQueryFunction( typeConfiguration ) ); + } + + /** + * MariaDB json_query() function + */ + public void jsonQuery_mariadb() { + functionRegistry.register( "json_query", new MariaDBJsonQueryFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_query() function + */ + public void jsonQuery_sqlserver() { + functionRegistry.register( "json_query", new SQLServerJsonQueryFunction( typeConfiguration ) ); + } + + /** + * H2 json_query() function + */ + public void jsonQuery_h2() { + functionRegistry.register( "json_query", new H2JsonQueryFunction( typeConfiguration ) ); + } + + /** + * json_exists() function + */ + public void jsonExists() { + functionRegistry.register( "json_exists", new JsonExistsFunction( typeConfiguration, true, true ) ); + } + + /** + * json_exists() function that doesn't support the passing clause + */ + public void jsonExists_no_passing() { + functionRegistry.register( "json_exists", new JsonExistsFunction( typeConfiguration, true, false ) ); + } + + /** + * Oracle json_exists() function + */ + public void jsonExists_oracle() { + functionRegistry.register( "json_exists", new JsonExistsFunction( typeConfiguration, false, true ) ); + } + + /** + * H2 json_exists() function + */ + public void jsonExists_h2() { + functionRegistry.register( "json_exists", new H2JsonExistsFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_exists() function + */ + public void jsonExists_sqlserver() { + functionRegistry.register( "json_exists", new SQLServerJsonExistsFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL json_exists() function + */ + public void jsonExists_postgresql() { + functionRegistry.register( "json_exists", new PostgreSQLJsonExistsFunction( typeConfiguration ) ); + } + + /** + * CockroachDB json_exists() function + */ + public void jsonExists_cockroachdb() { + functionRegistry.register( "json_exists", new CockroachDBJsonExistsFunction( typeConfiguration ) ); + } + + /** + * MySQL json_exists() function + */ + public void jsonExists_mysql() { + functionRegistry.register( "json_exists", new MySQLJsonExistsFunction( typeConfiguration ) ); + } + + /** + * SAP HANA json_exists() function + */ + public void jsonExists_hana() { + functionRegistry.register( "json_exists", new HANAJsonExistsFunction( typeConfiguration ) ); + } + + /** + * json_object() function + */ + public void jsonObject() { + functionRegistry.register( "json_object", new JsonObjectFunction( typeConfiguration, true ) ); + } + + /** + * DB2 json_object() function + */ + public void jsonObject_db2() { + functionRegistry.register( "json_object", new DB2JsonObjectFunction( typeConfiguration ) ); + } + + /** + * Oracle json_object() function + */ + public void jsonObject_oracle() { + functionRegistry.register( "json_object", new OracleJsonObjectFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_object() function + */ + public void jsonObject_sqlserver() { + functionRegistry.register( "json_object", new SQLServerJsonObjectFunction( typeConfiguration ) ); + } + + /** + * SAP HANA json_object() function + */ + public void jsonObject_hana() { + functionRegistry.register( "json_object", new HANAJsonObjectFunction( typeConfiguration ) ); + } + + /** + * HSQLDB json_object() function + */ + public void jsonObject_hsqldb() { + functionRegistry.register( "json_object", new HSQLJsonObjectFunction( typeConfiguration ) ); + } + + /** + * MySQL json_object() function + */ + public void jsonObject_mysql() { + functionRegistry.register( "json_object", new MySQLJsonObjectFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL json_object() function + */ + public void jsonObject_postgresql() { + functionRegistry.register( "json_object", new PostgreSQLJsonObjectFunction( typeConfiguration ) ); + } + + /** + * json_array() function + */ + public void jsonArray() { + functionRegistry.register( "json_array", new JsonArrayFunction( typeConfiguration ) ); + } + + /** + * DB2 json_array() function + */ + public void jsonArray_db2() { + functionRegistry.register( "json_array", new DB2JsonArrayFunction( typeConfiguration ) ); + } + + /** + * Oracle json_array() function + */ + public void jsonArray_oracle() { + functionRegistry.register( "json_array", new OracleJsonArrayFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_array() function + */ + public void jsonArray_sqlserver() { + functionRegistry.register( "json_array", new SQLServerJsonArrayFunction( typeConfiguration ) ); + } + + /** + * SAP HANA json_array() function + */ + public void jsonArray_hana() { + functionRegistry.register( "json_array", new HANAJsonArrayFunction( typeConfiguration ) ); + } + + /** + * HSQLDB json_array() function + */ + public void jsonArray_hsqldb() { + functionRegistry.register( "json_array", new HSQLJsonArrayFunction( typeConfiguration ) ); + } + + /** + * MySQL json_array() function + */ + public void jsonArray_mysql() { + functionRegistry.register( "json_array", new MySQLJsonArrayFunction( typeConfiguration ) ); + } + + /** + * MariaDB json_array() function + */ + public void jsonArray_mariadb() { + functionRegistry.register( "json_array", new MariaDBJsonArrayFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL json_array() function + */ + public void jsonArray_postgresql() { + functionRegistry.register( "json_array", new PostgreSQLJsonArrayFunction( typeConfiguration ) ); + } + + /** + * H2 json_arrayagg() function + */ + public void jsonArrayAgg_h2() { + functionRegistry.register( "json_arrayagg", new H2JsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * HSQLDB json_arrayagg() function + */ + public void jsonArrayAgg_hsqldb() { + functionRegistry.register( "json_arrayagg", new HSQLJsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * Oracle json_arrayagg() function + */ + public void jsonArrayAgg_oracle() { + functionRegistry.register( "json_arrayagg", new OracleJsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL json_arrayagg() function + */ + public void jsonArrayAgg_postgresql(boolean supportsStandard) { + functionRegistry.register( "json_arrayagg", new PostgreSQLJsonArrayAggFunction( supportsStandard, typeConfiguration ) ); + } + + /** + * SQL Server json_arrayagg() function + */ + public void jsonArrayAgg_sqlserver() { + functionRegistry.register( "json_arrayagg", new SQLServerJsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * MySQL json_arrayagg() function + */ + public void jsonArrayAgg_mysql() { + functionRegistry.register( "json_arrayagg", new MySQLJsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * MariaDB json_arrayagg() function + */ + public void jsonArrayAgg_mariadb() { + functionRegistry.register( "json_arrayagg", new MariaDBJsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * DB2 json_arrayagg() function + */ + public void jsonArrayAgg_db2() { + functionRegistry.register( "json_arrayagg", new DB2JsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * HANA json_arrayagg() function + */ + public void jsonArrayAgg_hana() { + functionRegistry.register( "json_arrayagg", new HANAJsonArrayAggFunction( typeConfiguration ) ); + } + + /** + * json_objectagg() function for H2 and HSQLDB + */ + public void jsonObjectAgg_h2() { + functionRegistry.register( "json_objectagg", new H2JsonObjectAggFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL json_objectagg() function + */ + public void jsonObjectAgg_postgresql(boolean supportsStandard) { + functionRegistry.register( "json_objectagg", new PostgreSQLJsonObjectAggFunction( supportsStandard, typeConfiguration ) ); + } + + /** + * MySQL json_objectagg() function + */ + public void jsonObjectAgg_mysql() { + functionRegistry.register( "json_objectagg", new MySQLJsonObjectAggFunction( typeConfiguration ) ); + } + + /** + * MariaDB json_objectagg() function + */ + public void jsonObjectAgg_mariadb() { + functionRegistry.register( "json_objectagg", new MariaDBJsonObjectAggFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_objectagg() function + */ + public void jsonObjectAgg_sqlserver() { + functionRegistry.register( "json_objectagg", new SQLServerJsonObjectAggFunction( typeConfiguration ) ); + } + + /** + * HANA json_objectagg() function + */ + public void jsonObjectAgg_hana() { + functionRegistry.register( "json_objectagg", new HANAJsonObjectAggFunction( typeConfiguration ) ); + } + + /** + * DB2 json_objectagg() function + */ + public void jsonObjectAgg_db2() { + functionRegistry.register( "json_objectagg", new DB2JsonObjectAggFunction( typeConfiguration ) ); + } } 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 new file mode 100644 index 000000000000..8d33450f4cb7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java @@ -0,0 +1,79 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; +import java.util.function.Supplier; + +import org.hibernate.metamodel.mapping.BasicValuedMapping; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmCastTarget; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.type.BasicType; +import org.hibernate.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.extractArgumentType; +import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.extractArgumentValuedMapping; +import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.isAssignableTo; +import static org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers.useImpliedTypeIfPossible; + +public class CastTargetReturnTypeResolver implements FunctionReturnTypeResolver { + + private final BasicType defaultType; + + public CastTargetReturnTypeResolver(TypeConfiguration typeConfiguration) { + this.defaultType = typeConfiguration.getBasicTypeForJavaType( String.class ); + } + + @Override + public ReturnableType resolveFunctionReturnType( + ReturnableType impliedType, + @Nullable SqmToSqlAstConverter converter, + List> arguments, + TypeConfiguration typeConfiguration) { + if ( arguments.size() > 2 ) { + int castTargetIndex = -1; + for ( int i = 2; i < arguments.size(); i++ ) { + if (arguments.get( i ) instanceof SqmCastTarget ) { + castTargetIndex = i; + break; + } + } + if ( castTargetIndex != -1 ) { + ReturnableType argType = extractArgumentType( arguments, castTargetIndex ); + return isAssignableTo( argType, impliedType ) ? impliedType : argType; + } + } + return defaultType; + } + + @Override + public BasicValuedMapping resolveFunctionReturnType( + Supplier impliedTypeAccess, + List arguments) { + if ( arguments.size() > 2 ) { + int castTargetIndex = -1; + for ( int i = 2; i < arguments.size(); i++ ) { + if (arguments.get( i ) instanceof CastTarget ) { + castTargetIndex = i; + break; + } + } + if ( castTargetIndex != -1 ) { + final BasicValuedMapping specifiedArgType = extractArgumentValuedMapping( arguments, castTargetIndex ); + return useImpliedTypeIfPossible( specifiedArgType, impliedTypeAccess.get() ); + } + } + return defaultType; + } +} 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 new file mode 100644 index 000000000000..858593e0a53e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.Dialect; +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.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * CockroachDB json_exists function. + */ +public class CockroachDBJsonExistsFunction extends JsonExistsFunction { + + public CockroachDBJsonExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + 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; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + else { + sqlAppender.appendSql( '(' ); + } + arguments.jsonDocument().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" ); + } + + sqlAppender.appendSql( "cast(" ); + expression.accept( walker ); + sqlAppender.appendSql( " as text)" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "] is not null" ); + } +} 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 new file mode 100644 index 000000000000..e011aebd3d81 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java @@ -0,0 +1,107 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.Dialect; +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.JdbcParameter; +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.type.spi.TypeConfiguration; + +/** + * CockroachDB json_query function. + */ +public class CockroachDBJsonQueryFunction extends JsonQueryFunction { + + public CockroachDBJsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + 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" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + } + final JsonQueryWrapMode wrapMode = arguments.wrapMode(); + + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "jsonb_build_array(" ); + } + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + 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; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + else { + sqlAppender.appendSql( '(' ); + } + arguments.jsonDocument().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" ); + } + + sqlAppender.appendSql( "cast(" ); + expression.accept( walker ); + sqlAppender.appendSql( " as text)" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( ']' ); + + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( ")" ); + } + } +} 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 new file mode 100644 index 000000000000..ada19353b3bd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java @@ -0,0 +1,106 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.Dialect; +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.JdbcParameter; +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.type.spi.TypeConfiguration; + +/** + * CockroachDB json_value function. + */ +public class CockroachDBJsonValueFunction extends JsonValueFunction { + + public CockroachDBJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // jsonb_path_query_first errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on CockroachDB" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on CockroachDB" ); + } + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + 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 ) { + sqlAppender.appendSql( "cast(" ); + } + final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + else { + sqlAppender.appendSql( '(' ); + } + arguments.jsonDocument().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" ); + } + + sqlAppender.appendSql( "cast(" ); + expression.accept( walker ); + sqlAppender.appendSql( " as text)" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( ']' ); + + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayAggFunction.java new file mode 100644 index 000000000000..ac999b0d108d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayAggFunction.java @@ -0,0 +1,99 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 json_arrayagg function. + */ +public class DB2JsonArrayAggFunction extends JsonArrayAggFunction { + + public DB2JsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + sqlAppender.appendSql( "'['||listagg(" ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + if ( nullBehavior != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arg, nullBehavior, translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arg, nullBehavior, translator ); + } + sqlAppender.appendSql( ",',')" ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( "||']'" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + sqlAppender.appendSql( "json_query(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null),'$.*')" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayFunction.java new file mode 100644 index 000000000000..cfe60664ebd0 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayFunction.java @@ -0,0 +1,30 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 json_array function. + */ +public class DB2JsonArrayFunction extends JsonArrayFunction { + + public DB2JsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + value.accept( walker ); + if ( ExpressionTypeHelper.isJson( value ) ) { + sqlAppender.appendSql( " format json" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectAggFunction.java new file mode 100644 index 000000000000..4b347f47d8c2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectAggFunction.java @@ -0,0 +1,91 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 json_objectagg function. + */ +public class DB2JsonObjectAggFunction extends JsonObjectAggFunction { + + public DB2JsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( ",", false, typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + if ( arguments.uniqueKeysBehavior() == JsonObjectAggUniqueKeysBehavior.WITH ) { + throw new QueryException( "Can't emulate json_objectagg 'with unique keys' clause." ); + } + sqlAppender.appendSql( "'{'||listagg(" ); + renderArgument( sqlAppender, arguments.key(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( "||':'||" ); + if ( caseWrapper ) { + if ( arguments.nullBehavior() != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_objectagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + } + sqlAppender.appendSql( ",',')||'}'" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert SQL type to JSON type + if ( nullBehavior == JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "coalesce(" ); + } + final JdbcMappingContainer expressionType = arg.getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() ) { + arg.accept( translator ); + } + else if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isBinary() ) { + sqlAppender.appendSql( "json_query(json_array(rawtohex(" ); + arg.accept( translator ); + sqlAppender.appendSql( ") null on null),'$.*')" ); + } + else { + sqlAppender.appendSql( "json_query(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null),'$.*')" ); + } + if ( nullBehavior == JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'null')" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectFunction.java new file mode 100644 index 000000000000..9e6f485e4290 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectFunction.java @@ -0,0 +1,30 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * DB2 json_object function. + */ +public class DB2JsonObjectFunction extends JsonObjectFunction { + + public DB2JsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + value.accept( walker ); + if ( ExpressionTypeHelper.isJson( value ) ) { + sqlAppender.appendSql( " format json" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java new file mode 100644 index 000000000000..8f18b2eefad2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java @@ -0,0 +1,61 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.Internal; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.sqm.CastType; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; + +@Internal +public class ExpressionTypeHelper { + + public static boolean isBoolean(SqlAstNode node) { + final Expression expression = (Expression) node; + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType.getJdbcTypeCount() == 1 + && isBoolean( expressionType.getSingleJdbcMapping().getCastType() ); + } + + public static boolean isNonNativeBoolean(SqlAstNode node) { + final Expression expression = (Expression) node; + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType.getJdbcTypeCount() == 1 + && isNonNativeBoolean( expressionType.getSingleJdbcMapping().getCastType() ); + } + + public static boolean isJson(SqlAstNode node) { + final Expression expression = (Expression) node; + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType.getJdbcTypeCount() == 1 + && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } + + public static boolean isBoolean(CastType castType) { + switch ( castType ) { + case BOOLEAN: + case TF_BOOLEAN: + case YN_BOOLEAN: + case INTEGER_BOOLEAN: + return true; + default: + return false; + } + } + + public static boolean isNonNativeBoolean(CastType castType) { + switch ( castType ) { + case TF_BOOLEAN: + case YN_BOOLEAN: + case INTEGER_BOOLEAN: + return true; + default: + return false; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonArrayAggFunction.java new file mode 100644 index 000000000000..c8aa67cd76ce --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonArrayAggFunction.java @@ -0,0 +1,27 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.spi.TypeConfiguration; + +/** + * H2 json_arrayagg function. + */ +public class H2JsonArrayAggFunction extends JsonArrayAggFunction { + + public H2JsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( true, typeConfiguration ); + } + + @Override + protected void renderReturningClause(SqlAppender sqlAppender, Expression arg, SqlAstTranslator translator) { + // No returning clause supported or needed + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java new file mode 100644 index 000000000000..12fdc9d55b7b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java @@ -0,0 +1,54 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonExistsErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 json_exists function. + */ +public class H2JsonExistsFunction extends JsonExistsFunction { + + public H2JsonExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // Json dereference errors by default if the JSON is invalid + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonExistsErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on H2" ); + } + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + catch (Exception ex) { + throw new QueryException( "H2 json_value only support literal json paths, but got " + arguments.jsonPath() ); + } + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( " is not null and " ); + H2JsonValueFunction.renderJsonPath( + sqlAppender, + arguments.jsonDocument(), + arguments.isJsonType(), + walker, + jsonPath, + arguments.passingClause() + ); + sqlAppender.appendSql( " is not null" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonObjectAggFunction.java new file mode 100644 index 000000000000..6ba7cee5d44c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonObjectAggFunction.java @@ -0,0 +1,29 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_objectagg function that uses no returning clause. + */ +public class H2JsonObjectAggFunction extends JsonObjectAggFunction { + + public H2JsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( ":", true, typeConfiguration ); + } + + @Override + protected void renderReturningClause( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + SqlAstTranslator translator) { + // No-op + } +} 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 new file mode 100644 index 000000000000..18088d0e5d8f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java @@ -0,0 +1,67 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonQueryEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 json_query function. + */ +public class H2JsonQueryFunction extends JsonQueryFunction { + + public H2JsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // Json dereference errors by default if the JSON is invalid + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on H2" ); + } + if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR ) { + throw new QueryException( "Can't emulate error on empty clause on H2" ); + } + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + catch (Exception ex) { + throw new QueryException( "H2 json_query only support literal json paths, but got " + arguments.jsonPath() ); + } + if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "'['||" ); + } + + sqlAppender.appendSql( "stringdecode(btrim(nullif(" ); + sqlAppender.appendSql( "cast(" ); + H2JsonValueFunction.renderJsonPath( + sqlAppender, + arguments.jsonDocument(), + arguments.isJsonType(), + walker, + jsonPath, + arguments.passingClause() + ); + sqlAppender.appendSql( " as varchar)" ); + sqlAppender.appendSql( ",'null'),'\"'))"); + if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "||']'" ); + } + } +} 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 new file mode 100644 index 000000000000..1777a2375607 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -0,0 +1,137 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; +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.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +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.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * H2 json_value function. + */ +public class H2JsonValueFunction extends JsonValueFunction { + + public H2JsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // Json dereference errors by default if the JSON is invalid + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on H2" ); + } + if ( arguments.emptyBehavior() == JsonValueEmptyBehavior.ERROR ) { + throw new QueryException( "Can't emulate error on empty clause on H2" ); + } + final Expression defaultExpression = arguments.emptyBehavior() == null + ? null + : arguments.emptyBehavior().getDefaultExpression(); + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( "cast(" ); + } + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + catch (Exception ex) { + throw new QueryException( "H2 json_value only support literal json paths, but got " + arguments.jsonPath() ); + } + + sqlAppender.appendSql( "stringdecode(btrim(nullif(" ); + if ( defaultExpression != null ) { + sqlAppender.appendSql( "coalesce(" ); + } + sqlAppender.appendSql( "cast(" ); + renderJsonPath( + sqlAppender, + arguments.jsonDocument(), + arguments.isJsonType(), + walker, + jsonPath, + arguments.passingClause() + ); + sqlAppender.appendSql( " as varchar)" ); + if ( defaultExpression != null ) { + sqlAppender.appendSql( ",cast(" ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " as varchar))" ); + } + sqlAppender.appendSql( ",'null'),'\"'))"); + + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } + } + + public static void renderJsonPath( + SqlAppender sqlAppender, + Expression jsonDocument, + boolean isJson, + SqlAstTranslator walker, + String jsonPath, + @Nullable JsonPathPassingClause passingClause) { + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute + && jsonDocument.getColumnReference() != null + || !isJson; + if ( needsWrapping ) { + sqlAppender.appendSql( '(' ); + } + jsonDocument.accept( walker ); + if ( needsWrapping ) { + if ( !isJson ) { + sqlAppender.append( " format json" ); + } + sqlAppender.appendSql( ')' ); + } + 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(); + sqlAppender.appendSql( "." ); + sqlAppender.appendDoubleQuoteEscapedString( 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" ); + } + + sqlAppender.appendSql( '[' ); + expression.accept( walker ); + sqlAppender.appendSql( "+1]" ); + } + else { + sqlAppender.appendSql( '[' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() + 1 ); + sqlAppender.appendSql( ']' ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayAggFunction.java new file mode 100644 index 000000000000..cc1828866143 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayAggFunction.java @@ -0,0 +1,109 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_arrayagg function. + */ +public class HANAJsonArrayAggFunction extends JsonArrayAggFunction { + + public HANAJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + sqlAppender.appendSql( "'['||string_agg(" ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + if ( nullBehavior != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arg, nullBehavior, translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arg, nullBehavior, translator ); + } + sqlAppender.appendSql( ",','" ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( ")||']'" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert the value to JSON + final JdbcMappingContainer expressionType = arg.getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() ) { + sqlAppender.appendSql( "cast(" ); + arg.accept( translator ); + sqlAppender.appendSql( " as nvarchar(" + Integer.MAX_VALUE + "))" ); + } + else { + sqlAppender.appendSql( "json_query((select " ); + arg.accept( translator ); + sqlAppender.appendSql( + " V from sys.dummy for json('arraywrap'='no','omitnull'='no') returns nvarchar(" + Integer.MAX_VALUE + ")),'$.V')" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayFunction.java new file mode 100644 index 000000000000..cfd929fb5168 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayFunction.java @@ -0,0 +1,159 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.ArrayList; +import java.util.List; + +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.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.internal.util.NumberHelper.digitCount; + +/** + * SAP HANA json_array function. + */ +public class HANAJsonArrayFunction extends JsonArrayFunction { + + public HANAJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "'[]'" ); + return; + } + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + argumentsCount = sqlAstArguments.size(); + } + final int digits = digitCount( argumentsCount ); + final String prefix = "0".repeat( digits ); + + final List jsonArgumentFields = getJsonArgumentFields( prefix, sqlAstArguments, argumentsCount ); + sqlAppender.appendSql( "(select json_query(t.x,'$[0].*' with wrapper) from (" ); + replaceJsonArgumentsEscaping( + sqlAppender, + sqlAstArguments, + walker, + 0, + jsonArgumentFields, + prefix, + argumentsCount, + nullBehavior + ); + sqlAppender.appendSql( ") t)" ); + } + + private static void replaceJsonArgumentsEscaping( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker, + int i, + List jsonArgumentFields, + String prefix, + int argumentsCount, + JsonNullBehavior nullBehavior) { + + if ( i < jsonArgumentFields.size() ) { + // Take the substring before the match + sqlAppender.appendSql( "select substring(t.x, 1, locate_regexpr(r.x in t.x) - 2)" ); + + // The match itself after replacing double backslashes and backslash escaped quotes + sqlAppender.appendSql( "|| replace(replace(substr_regexpr(r.x in t.x),'\\\\','\\'),'\\\"','\"')" ); + + // And the rest of the string after the match + sqlAppender.appendSql( "|| substring(t.x, locate_regexpr(r.x in t.x) + length(substr_regexpr(r.x in t.x)) + 1) x"); + + sqlAppender.appendSql( " from (" ); + replaceJsonArgumentsEscaping( + sqlAppender, + sqlAstArguments, + walker, + i + 1, + jsonArgumentFields, + prefix, + argumentsCount, + nullBehavior + ); + sqlAppender.appendSql( ") t" ); + + sqlAppender.appendSql( ",(select '" ); + sqlAppender.appendSql( valueExtractionPattern( jsonArgumentFields.get( i ) ) ); + sqlAppender.appendSql( "' x from sys.dummy) r" ); + } + else { + sqlAppender.appendSql( "select t.jsonresult x from (select " ); + renderArrayArguments( sqlAppender, sqlAstArguments, walker, prefix, argumentsCount ); + sqlAppender.appendSql( " from sys.dummy for json" ); + if ( nullBehavior == JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "('omitnull'='no')" ); + } + sqlAppender.appendSql( ") t" ); + } + } + + private static void renderArrayArguments( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker, + String prefix, + int argumentsCount) { + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( " C" ); + sqlAppender.append( prefix, 1, prefix.length() ); + sqlAppender.appendSql( "0" ); + for ( int i = 1; i < argumentsCount; i++ ) { + sqlAppender.appendSql( ',' ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( " C" ); + final String position = Integer.toString( i ); + sqlAppender.append( prefix, position.length(), prefix.length() ); + sqlAppender.appendSql( position ); + } + } + + private List getJsonArgumentFields( + String zeroPrefix, + List sqlAstArguments, + int argumentsCount) { + final ArrayList jsonArgumentIndexes = new ArrayList<>(); + for ( int i = 0; i < argumentsCount; i++ ) { + if ( ExpressionTypeHelper.isJson( sqlAstArguments.get( i ) ) ) { + final String position = Integer.toString( i ); + jsonArgumentIndexes.add( "C" + zeroPrefix.substring( position.length() ) + position ); + } + } + return jsonArgumentIndexes; + } + + private static String valueExtractionPattern(String attributeName) { + // (? returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_query(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + final Expression jsonPath = arguments.jsonPath(); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause == null ) { + jsonPath.accept( walker ); + } + else { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + + final JsonExistsErrorBehavior errorBehavior = arguments.errorBehavior(); + if ( errorBehavior != null && errorBehavior != JsonExistsErrorBehavior.FALSE ) { + if ( errorBehavior == JsonExistsErrorBehavior.TRUE ) { + sqlAppender.appendSql( " empty object on error" ); + } + else { + sqlAppender.appendSql( " error on error" ); + } + } + sqlAppender.appendSql( ") is not null" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectAggFunction.java new file mode 100644 index 000000000000..9b04ee7d0bf4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectAggFunction.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * HANA json_objectagg function. + */ +public class HANAJsonObjectAggFunction extends JsonObjectAggFunction { + + public HANAJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( ",", false, typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + if ( arguments.uniqueKeysBehavior() == JsonObjectAggUniqueKeysBehavior.WITH ) { + throw new QueryException( "Can't emulate json_objectagg 'with unique keys' clause." ); + } + sqlAppender.appendSql( "'{'||string_agg(" ); + renderArgument( sqlAppender, arguments.key(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( "||':'||" ); + if ( caseWrapper ) { + if ( arguments.nullBehavior() != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_objectagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + } + sqlAppender.appendSql( ",',')||'}'" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert SQL type to JSON type + final JdbcMappingContainer expressionType = arg.getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() ) { + sqlAppender.appendSql( "cast(" ); + arg.accept( translator ); + sqlAppender.appendSql( " as nvarchar(" + Integer.MAX_VALUE + "))" ); + } + else { + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + sqlAppender.appendSql( "json_query((select " ); + arg.accept( translator ); + sqlAppender.appendSql( + " V from sys.dummy for json('arraywrap'='no','omitnull'='no') returns nvarchar(" + Integer.MAX_VALUE + ")),'$.V')" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'null')" ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java new file mode 100644 index 000000000000..1c971e830bbd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java @@ -0,0 +1,141 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +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.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SAP HANA json_object function. + */ +public class HANAJsonObjectFunction extends JsonObjectFunction { + + public HANAJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "'{}'" ); + return; + } + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( ( sqlAstArguments.size() & 1 ) == 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( sqlAstArguments.size() - 1 ); + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + final List jsonArgumentFields = getJsonArgumentFields( sqlAstArguments, argumentsCount, walker ); + sqlAppender.appendSql( '(' ); + replaceJsonArgumentsEscaping( + sqlAppender, + sqlAstArguments, + walker, + 0, + jsonArgumentFields, + argumentsCount, + nullBehavior + ); + sqlAppender.appendSql( ')' ); + } + + private static void replaceJsonArgumentsEscaping( + SqlAppender sqlAppender, + List sqlAstArguments, + SqlAstTranslator walker, + int jsonArg, + List jsonArgumentFields, + int argumentsCount, + JsonNullBehavior nullBehavior) { + + if ( jsonArg < jsonArgumentFields.size() ) { + // Take the substring before the match + sqlAppender.appendSql( "select substring(t.x, 1, locate_regexpr(r.x in t.x) - 2)" ); + + // The match itself after replacing double backslashes and backslash escaped quotes + sqlAppender.appendSql( "|| replace(replace(substr_regexpr(r.x in t.x),'\\\\','\\'),'\\\"','\"')" ); + + // And the rest of the string after the match + sqlAppender.appendSql( "|| substring(t.x, locate_regexpr(r.x in t.x) + length(substr_regexpr(r.x in t.x)) + 1) x"); + + sqlAppender.appendSql( " from (" ); + replaceJsonArgumentsEscaping( + sqlAppender, + sqlAstArguments, + walker, + jsonArg + 1, + jsonArgumentFields, + argumentsCount, + nullBehavior + ); + sqlAppender.appendSql( ") t" ); + + sqlAppender.appendSql( ",(select '" ); + sqlAppender.appendSql( valueExtractionPattern( jsonArgumentFields.get( jsonArg ) ) ); + sqlAppender.appendSql( "' x from sys.dummy) r" ); + } + else { + sqlAppender.appendSql( "select t.jsonresult x from (select" ); + char separator = ' '; + for ( int i = 0; i < argumentsCount; i += 2 ) { + sqlAppender.appendSql( separator ); + final SqlAstNode key = sqlAstArguments.get( i ); + final SqlAstNode value = sqlAstArguments.get( i + 1 ); + value.accept( walker ); + sqlAppender.appendSql( ' ' ); + final String literalValue = walker.getLiteralValue( (Expression) key ); + sqlAppender.appendDoubleQuoteEscapedString( literalValue ); + separator = ','; + } + sqlAppender.appendSql( " from sys.dummy for json('arraywrap'='no'" ); + if ( nullBehavior == JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'omitnull'='no'" ); + } + sqlAppender.appendSql( ")) t" ); + } + } + + private List getJsonArgumentFields( + List sqlAstArguments, + int argumentsCount, + SqlAstTranslator walker) { + final ArrayList jsonArgumentIndexes = new ArrayList<>(); + for ( int i = 0; i < argumentsCount; i += 2 ) { + if ( ExpressionTypeHelper.isJson( sqlAstArguments.get( i + 1 ) ) ) { + jsonArgumentIndexes.add( walker.getLiteralValue( (Expression) sqlAstArguments.get( i ) ) ); + } + } + return jsonArgumentIndexes; + } + + private static String valueExtractionPattern(String attributeName) { + // (? translator) { + // No returning clause needed + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayFunction.java new file mode 100644 index 000000000000..f763534bf048 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayFunction.java @@ -0,0 +1,33 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * HSQLDB json_array function. + */ +public class HSQLJsonArrayFunction extends JsonArrayFunction { + + public HSQLJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "cast(null as int)" ); + } + else { + value.accept( walker ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonObjectFunction.java new file mode 100644 index 000000000000..50c27676ce8f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonObjectFunction.java @@ -0,0 +1,33 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * HSQLDB json_object function. + */ +public class HSQLJsonObjectFunction extends JsonObjectFunction { + + public HSQLJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "cast(null as int)" ); + } + else { + value.accept( walker ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java new file mode 100644 index 000000000000..824bb11cd71d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java @@ -0,0 +1,143 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.Collections; +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_arrayagg function. + */ +public class JsonArrayAggFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + protected final boolean supportsFilter; + + public JsonArrayAggFunction(boolean supportsFilter, TypeConfiguration typeConfiguration) { + super( + "json_arrayagg", + FunctionKind.ORDERED_SET_AGGREGATE, + StandardArgumentsValidators.between( 1, 2 ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON_ARRAY ) + ), + null + ); + this.supportsFilter = supportsFilter; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, Collections.emptyList(), returnType, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "json_arrayagg(" ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + if ( nullBehavior != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arg, nullBehavior, translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arg, nullBehavior, translator ); + } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + if ( nullBehavior == JsonNullBehavior.NULL ) { + sqlAppender.appendSql( " null on null" ); + } + else { + sqlAppender.appendSql( " absent on null" ); + } + renderReturningClause( sqlAppender, arg, translator ); + sqlAppender.appendSql( ')' ); + + if ( !caseWrapper && filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + } + + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + arg.accept( translator ); + } + + protected void renderReturningClause(SqlAppender sqlAppender, Expression arg, SqlAstTranslator translator) { + sqlAppender.appendSql( " returning " ); + sqlAppender.appendSql( + translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry() + .getTypeName( SqlTypes.JSON, translator.getSessionFactory().getJdbcServices().getDialect() ) + ); + } +} 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 new file mode 100644 index 000000000000..0fa325707bcc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java @@ -0,0 +1,77 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +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.JsonNullBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_array function. + */ +public class JsonArrayFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public JsonArrayFunction(TypeConfiguration typeConfiguration) { + super( + "json_array", + FunctionKind.NORMAL, + null, + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON_ARRAY ) + ), + null + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_array" ); + char separator = '('; + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( separator ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + argumentsCount = sqlAstArguments.size(); + } + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + renderValue( sqlAppender, sqlAstArguments.get( i ), walker ); + separator = ','; + } + if ( nullBehavior == JsonNullBehavior.NULL ) { + sqlAppender.appendSql( " null on null" ); + } + } + sqlAppender.appendSql( ')' ); + } + + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + value.accept( walker ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java new file mode 100644 index 000000000000..0d5c31c70edc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java @@ -0,0 +1,188 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression; +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.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.IMPLICIT_JSON; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.JSON; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * Standard json_exists function. + */ +public class JsonExistsFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + protected final boolean supportsJsonPathExpression; + protected final boolean supportsJsonPathPassingClause; + + public JsonExistsFunction( + TypeConfiguration typeConfiguration, + boolean supportsJsonPathExpression, + boolean supportsJsonPathPassingClause) { + super( + "json_exists", + FunctionKind.NORMAL, + new ArgumentTypesValidator( null, IMPLICIT_JSON, STRING ), + StandardFunctionReturnTypeResolvers.invariant( typeConfiguration.standardBasicTypeForJavaType( Boolean.class ) ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) + ); + this.supportsJsonPathExpression = supportsJsonPathExpression; + this.supportsJsonPathPassingClause = supportsJsonPathPassingClause; + } + + @Override + public boolean isPredicate() { + return true; + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + //noinspection unchecked + return (SelfRenderingSqmFunction) new SqmJsonExistsExpression( + this, + this, + arguments, + (ReturnableType) impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, JsonExistsArguments.extract( sqlAstArguments ), returnType, walker ); + } + + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_exists(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + final Expression jsonPath = arguments.jsonPath(); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( supportsJsonPathPassingClause || passingClause == null ) { + if ( supportsJsonPathExpression ) { + jsonPath.accept( walker ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + walker.getLiteralValue( jsonPath ) + ); + } + 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() ); + } + } + } + else { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + final JsonExistsErrorBehavior errorBehavior = arguments.errorBehavior(); + if ( errorBehavior != null && errorBehavior != JsonExistsErrorBehavior.FALSE ) { + if ( errorBehavior == JsonExistsErrorBehavior.TRUE ) { + sqlAppender.appendSql( " true on error" ); + } + else { + sqlAppender.appendSql( " error on error" ); + } + } + sqlAppender.appendSql( ')' ); + } + + protected record JsonExistsArguments( + Expression jsonDocument, + Expression jsonPath, + boolean isJsonType, + @Nullable JsonPathPassingClause passingClause, + @Nullable JsonExistsErrorBehavior errorBehavior) { + public static JsonExistsArguments extract(List sqlAstArguments) { + int nextIndex = 2; + JsonPathPassingClause passingClause = null; + JsonExistsErrorBehavior errorBehavior = null; + 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 JsonExistsErrorBehavior ) { + errorBehavior = (JsonExistsErrorBehavior) node; + nextIndex++; + } + } + final Expression jsonDocument = (Expression) sqlAstArguments.get( 0 ); + return new JsonExistsArguments( + jsonDocument, + (Expression) sqlAstArguments.get( 1 ), + jsonDocument.getExpressionType() != null + && jsonDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isJson(), + passingClause, + errorBehavior + ); + } + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectAggFunction.java new file mode 100644 index 000000000000..82d13a9caa98 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectAggFunction.java @@ -0,0 +1,172 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.sql.ast.Clause; +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Standard json_objectagg function. + */ +public class JsonObjectAggFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + protected final String valueSeparator; + protected final boolean supportsFilter; + + public JsonObjectAggFunction(String valueSeparator, boolean supportsFilter, TypeConfiguration typeConfiguration) { + super( + "json_objectagg", + FunctionKind.AGGREGATE, + StandardArgumentsValidators.between( 2, 4 ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + this.supportsFilter = supportsFilter; + this.valueSeparator = valueSeparator; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, returnType, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + render( sqlAppender, JsonObjectAggArguments.extract( sqlAstArguments ), filter, returnType, translator ); + } + + protected void render( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + sqlAppender.appendSql( "json_objectagg(" ); + arguments.key().accept( translator ); + sqlAppender.appendSql( valueSeparator ); + if ( caseWrapper ) { + if ( arguments.nullBehavior() != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_objectagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + } + if ( arguments.nullBehavior() == JsonNullBehavior.NULL ) { + sqlAppender.appendSql( " null on null" ); + } + else { + sqlAppender.appendSql( " absent on null" ); + } + renderUniqueAndReturningClause( sqlAppender, arguments, translator ); + sqlAppender.appendSql( ')' ); + + if ( !caseWrapper && filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + } + + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + arg.accept( translator ); + } + + protected void renderUniqueAndReturningClause(SqlAppender sqlAppender, JsonObjectAggArguments arguments, SqlAstTranslator translator) { + renderReturningClause( sqlAppender, arguments, translator ); + renderUniqueClause( sqlAppender, arguments, translator ); + } + + protected void renderReturningClause(SqlAppender sqlAppender, JsonObjectAggArguments arguments, SqlAstTranslator translator) { + sqlAppender.appendSql( " returning " ); + sqlAppender.appendSql( + translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry() + .getTypeName( SqlTypes.JSON, translator.getSessionFactory().getJdbcServices().getDialect() ) + ); + } + + protected void renderUniqueClause(SqlAppender sqlAppender, JsonObjectAggArguments arguments, SqlAstTranslator translator) { + if ( arguments.uniqueKeysBehavior() == JsonObjectAggUniqueKeysBehavior.WITH ) { + sqlAppender.appendSql( " with unique keys" ); + } + } + + protected record JsonObjectAggArguments( + Expression key, + Expression value, + @Nullable JsonNullBehavior nullBehavior, + @Nullable JsonObjectAggUniqueKeysBehavior uniqueKeysBehavior) { + public static JsonObjectAggArguments extract(List sqlAstArguments) { + int nextIndex = 2; + JsonNullBehavior nullBehavior = null; + JsonObjectAggUniqueKeysBehavior uniqueKeysBehavior = null; + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) node; + nextIndex++; + } + } + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonObjectAggUniqueKeysBehavior ) { + uniqueKeysBehavior = (JsonObjectAggUniqueKeysBehavior) node; + nextIndex++; + } + } + return new JsonObjectAggArguments( + (Expression) sqlAstArguments.get( 0 ), + (Expression) sqlAstArguments.get( 1 ), + nullBehavior, + uniqueKeysBehavior + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectArgumentsValidator.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectArgumentsValidator.java new file mode 100644 index 000000000000..b1c9af7aa4c5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectArgumentsValidator.java @@ -0,0 +1,112 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmJsonNullBehavior; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.spi.TypeConfiguration; + +import static org.hibernate.query.sqm.produce.function.ArgumentTypesValidator.checkArgumentType; +import static org.hibernate.query.sqm.produce.function.ArgumentTypesValidator.isUnknownExpressionType; +import static org.hibernate.type.descriptor.java.JavaTypeHelper.isUnknown; + +public class JsonObjectArgumentsValidator implements ArgumentsValidator { + + @Override + public void validate( + List> arguments, + String functionName, + TypeConfiguration typeConfiguration) { + if ( !arguments.isEmpty() ) { + final SqmTypedNode lastArgument = arguments.get( arguments.size() - 1 ); + final int argumentsCount; + if ( lastArgument instanceof SqmJsonNullBehavior ) { + argumentsCount = arguments.size() - 1; + } + else { + argumentsCount = arguments.size(); + } + checkArgumentsCount( argumentsCount ); + for ( int i = 0; i < argumentsCount; i += 2 ) { + final SqmTypedNode key = arguments.get( i ); + final SqmExpressible nodeType = key.getNodeType(); + final JavaType javaType = nodeType == null + ? null + : nodeType.getRelationalJavaType(); + if ( !isUnknown( javaType ) ) { + final DomainType domainType = key.getExpressible().getSqmType(); + if ( domainType instanceof JdbcMapping ) { + final JdbcMapping jdbcMapping = (JdbcMapping) domainType; + checkArgumentType( + i, + functionName, + FunctionParameterType.STRING, + jdbcMapping.getJdbcType(), + javaType.getJavaTypeClass() + ); + } + } + } + } + } + + @Override + public void validateSqlTypes(List arguments, String functionName) { + if ( !arguments.isEmpty() ) { + final SqlAstNode lastArgument = arguments.get( arguments.size() - 1 ); + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + argumentsCount = arguments.size() - 1; + } + else { + argumentsCount = arguments.size(); + } + checkArgumentsCount( argumentsCount ); + for ( int i = 0; i < argumentsCount; i += 2 ) { + final SqlAstNode argument = arguments.get( i ); + if ( argument instanceof Expression ) { + final Expression expression = (Expression) argument; + final JdbcMappingContainer expressionType = expression.getExpressionType(); + if ( expressionType != null && !isUnknownExpressionType( expressionType ) ) { + final JdbcMapping mapping = expressionType.getSingleJdbcMapping(); + checkArgumentType( + i, + functionName, + FunctionParameterType.STRING, + mapping.getJdbcType(), + mapping.getJavaTypeDescriptor().getJavaType() + ); + } + } + } + } + } + + private void checkArgumentsCount(int size) { + if ( ( size & 1 ) == 1 ) { + throw new FunctionArgumentException( + String.format( + "json_object must have an even number of arguments, but found %d", + size + ) + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectFunction.java new file mode 100644 index 000000000000..b165734f35fa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectFunction.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +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.JsonNullBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_object function. + */ +public class JsonObjectFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + protected final boolean colonSyntax; + + public JsonObjectFunction(TypeConfiguration typeConfiguration, boolean colonSyntax) { + super( + "json_object", + FunctionKind.NORMAL, + new JsonObjectArgumentsValidator(), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + this.colonSyntax = colonSyntax; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_object" ); + char separator = '('; + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( separator ); + } + else { + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( ( sqlAstArguments.size() & 1 ) == 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( sqlAstArguments.size() - 1 ); + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + for ( int i = 0; i < argumentsCount; i += 2 ) { + sqlAppender.appendSql( separator ); + final SqlAstNode key = sqlAstArguments.get( i ); + final SqlAstNode value = sqlAstArguments.get( i + 1 ); + key.accept( walker ); + if ( colonSyntax ) { + sqlAppender.appendSql( ':' ); + } + else { + sqlAppender.appendSql( " value " ); + } + renderValue( sqlAppender, value, walker ); + separator = ','; + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( " absent on null" ); + } + } + sqlAppender.appendSql( ')' ); + } + + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + value.accept( walker ); + } +} 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 new file mode 100644 index 000000000000..743b9af997ec --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -0,0 +1,213 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.QueryException; +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; + +public class JsonPathHelper { + + public static List parseJsonPathElements(String jsonPath) { + if ( jsonPath.charAt( 0 ) != '$' ) { + throw new QueryException( "Json path expression expression emulation only supports absolute paths i.e. must start with a '$' but got: " + jsonPath ); + } + final var jsonPathElements = new ArrayList(); + int startIndex; + int dotIndex; + + if ( jsonPath.length() > 1 ) { + if ( jsonPath.charAt( 1 ) == '.' ) { + startIndex = 2; + } + else { + final int bracketEndIndex = jsonPath.indexOf( ']' ); + parseBracket( jsonPath, 1, bracketEndIndex, jsonPathElements ); + startIndex = bracketEndIndex + 2; + } + + try { + while ( ( dotIndex = jsonPath.indexOf( '.', startIndex ) ) != -1 ) { + parseAttribute( jsonPath, startIndex, dotIndex, jsonPathElements ); + startIndex = dotIndex + 1; + } + if ( startIndex < jsonPath.length() ) { + parseAttribute( jsonPath, startIndex, jsonPath.length(), jsonPathElements ); + } + } + catch (Exception ex) { + throw new QueryException( "Can't emulate non-simple json path expression: " + jsonPath, ex ); + } + } + return jsonPathElements; + } + + public static void appendJsonPathConcatPassingClause( + SqlAppender sqlAppender, + Expression jsonPathExpression, + JsonPathPassingClause passingClause, SqlAstTranslator walker) { + appendJsonPathConcatenatedPassingClause( sqlAppender, jsonPathExpression, passingClause, walker, "concat", "," ); + } + + public static void appendJsonPathDoublePipePassingClause( + SqlAppender sqlAppender, + Expression jsonPathExpression, + JsonPathPassingClause passingClause, + SqlAstTranslator walker) { + appendJsonPathConcatenatedPassingClause( sqlAppender, jsonPathExpression, passingClause, walker, "", "||" ); + } + + public static void appendInlinedJsonPathIncludingPassingClause( + SqlAppender sqlAppender, + String prefix, + Expression jsonPathExpression, + JsonPathPassingClause passingClause, + SqlAstTranslator walker) { + final String jsonPath = walker.getLiteralValue( jsonPathExpression ); + final String[] parts = jsonPath.split( "\\$" ); + sqlAppender.append( '\'' ); + sqlAppender.append( prefix ); + final int start; + if ( parts[0].isEmpty() ) { + start = 2; + sqlAppender.append( '$' ); + sqlAppender.append( parts[1] ); + } + else { + start = 0; + } + for ( int i = start; i < parts.length; i++ ) { + final String part = parts[i]; + + final int parameterNameEndIndex = indexOfNonIdentifier( part, 0 ); + final String parameterName = part.substring( 0, parameterNameEndIndex ); + final Expression expression = passingClause.getPassingExpressions().get( parameterName ); + if ( expression == null ) { + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + Object literalValue = walker.getLiteralValue( expression ); + if ( literalValue instanceof String ) { + appendLiteral( sqlAppender, 0, (String) literalValue ); + } + else { + sqlAppender.appendSql( String.valueOf( literalValue ) ); + } + appendLiteral( sqlAppender, parameterNameEndIndex, part ); + } + sqlAppender.appendSql( '\'' ); + } + + private static void appendLiteral(SqlAppender sqlAppender, int parameterNameEndIndex, String part) { + for ( int j = parameterNameEndIndex; j < part.length(); j++ ) { + final char c = part.charAt( j ); + if ( c == '\'') { + sqlAppender.appendSql( '\'' ); + } + sqlAppender.appendSql( c ); + } + } + + private static void appendJsonPathConcatenatedPassingClause( + SqlAppender sqlAppender, + Expression jsonPathExpression, + JsonPathPassingClause passingClause, + SqlAstTranslator walker, + String concatStart, + String concatCombine) { + final String jsonPath = walker.getLiteralValue( jsonPathExpression ); + final String[] parts = jsonPath.split( "\\$" ); + sqlAppender.append( concatStart ); + final int start; + String separator = "("; + if ( parts[0].isEmpty() ) { + start = 2; + sqlAppender.append( separator ); + sqlAppender.append( "'$'" ); + sqlAppender.append( concatCombine ); + sqlAppender.appendSingleQuoteEscapedString( parts[1] ); + separator = concatCombine; + } + else { + start = 0; + } + for ( int i = start; i < parts.length; i++ ) { + final String part = parts[i]; + sqlAppender.append( separator ); + + final int parameterNameEndIndex = indexOfNonIdentifier( part, 0 ); + final String parameterName = part.substring( 0, parameterNameEndIndex ); + final Expression expression = passingClause.getPassingExpressions().get( parameterName ); + if ( expression == null ) { + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + expression.accept( walker ); + sqlAppender.append( ',' ); + sqlAppender.appendSingleQuoteEscapedString( part.substring( parameterNameEndIndex ) ); + separator = concatCombine; + } + sqlAppender.appendSql( ')' ); + } + + private static void parseAttribute(String jsonPath, int startIndex, int endIndex, ArrayList jsonPathElements) { + final int bracketIndex = jsonPath.indexOf( '[', startIndex ); + if ( bracketIndex != -1 && bracketIndex < endIndex ) { + jsonPathElements.add( new JsonAttribute( jsonPath.substring( startIndex, bracketIndex ) ) ); + parseBracket( jsonPath, bracketIndex, endIndex, jsonPathElements ); + } + else { + jsonPathElements.add( new JsonAttribute( jsonPath.substring( startIndex, endIndex ) ) ); + } + } + + private static void parseBracket(String jsonPath, int bracketStartIndex, int endIndex, ArrayList jsonPathElements) { + assert jsonPath.charAt( bracketStartIndex ) == '['; + final int bracketEndIndex = jsonPath.lastIndexOf( ']', endIndex ); + if ( bracketEndIndex < bracketStartIndex ) { + throw new QueryException( "Can't emulate non-simple json path expression: " + jsonPath ); + } + final int contentStartIndex = indexOfNonWhitespace( jsonPath, bracketStartIndex + 1 ); + final int contentEndIndex = lastIndexOfWhitespace( jsonPath, bracketEndIndex - 1 ); + if ( jsonPath.charAt( contentStartIndex ) == '$' ) { + jsonPathElements.add( new JsonParameterIndexAccess( jsonPath.substring( contentStartIndex + 1, contentEndIndex ) ) ); + } + else { + final int index = Integer.parseInt( jsonPath, contentStartIndex, contentEndIndex, 10 ); + jsonPathElements.add( new JsonIndexAccess( index ) ); + } + } + + public static int indexOfNonIdentifier(String jsonPath, int i) { + while ( i < jsonPath.length() && Character.isJavaIdentifierPart( jsonPath.charAt( i ) ) ) { + i++; + } + return i; + } + + private static int indexOfNonWhitespace(String jsonPath, int i) { + while ( i < jsonPath.length() && Character.isWhitespace( jsonPath.charAt( i ) ) ) { + i++; + } + return i; + } + + private static int lastIndexOfWhitespace(String jsonPath, int i) { + while ( i > 0 && Character.isWhitespace( jsonPath.charAt( i ) ) ) { + i--; + } + return i + 1; + } + + public sealed interface JsonPathElement {} + public record JsonAttribute(String attribute) implements JsonPathElement {} + public record JsonIndexAccess(int index) implements JsonPathElement {} + public record JsonParameterIndexAccess(String parameterName) implements JsonPathElement {} +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonQueryFunction.java new file mode 100644 index 000000000000..29f7543c2bac --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonQueryFunction.java @@ -0,0 +1,222 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; +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.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.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.IMPLICIT_JSON; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.JSON; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * Standard json_query function. + */ +public class JsonQueryFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + protected final boolean supportsJsonPathExpression; + protected final boolean supportsJsonPathPassingClause; + + public JsonQueryFunction( + TypeConfiguration typeConfiguration, + boolean supportsJsonPathExpression, + boolean supportsJsonPathPassingClause) { + super( + "json_query", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY ) + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) + ); + this.supportsJsonPathExpression = supportsJsonPathExpression; + this.supportsJsonPathPassingClause = supportsJsonPathPassingClause; + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + //noinspection unchecked + return (SelfRenderingSqmFunction) new SqmJsonQueryExpression( + this, + this, + arguments, + (ReturnableType) impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, JsonQueryArguments.extract( sqlAstArguments ), returnType, walker ); + } + + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_query(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( supportsJsonPathPassingClause || passingClause == null ) { + if ( supportsJsonPathExpression ) { + arguments.jsonPath().accept( walker ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + 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() ); + } + } + } + else { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + if ( arguments.wrapMode() != null ) { + switch ( arguments.wrapMode() ) { + case WITH_WRAPPER -> sqlAppender.appendSql( " with wrapper" ); + case WITHOUT_WRAPPER -> sqlAppender.appendSql( " without wrapper" ); + case WITH_CONDITIONAL_WRAPPER -> sqlAppender.appendSql( " with conditional wrapper" ); + } + } + if ( arguments.errorBehavior() != null ) { + switch ( arguments.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 ( arguments.emptyBehavior() != null ) { + switch ( arguments.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" ); + } + } + sqlAppender.appendSql( ')' ); + } + + protected record JsonQueryArguments( + Expression jsonDocument, + Expression jsonPath, + boolean isJsonType, + @Nullable JsonPathPassingClause passingClause, + @Nullable JsonQueryWrapMode wrapMode, + @Nullable JsonQueryErrorBehavior errorBehavior, + @Nullable JsonQueryEmptyBehavior emptyBehavior) { + public static JsonQueryArguments extract(List sqlAstArguments) { + int nextIndex = 2; + JsonPathPassingClause passingClause = null; + JsonQueryWrapMode wrapMode = null; + JsonQueryErrorBehavior errorBehavior = null; + JsonQueryEmptyBehavior emptyBehavior = null; + 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 JsonQueryWrapMode ) { + wrapMode = (JsonQueryWrapMode) node; + nextIndex++; + } + } + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonQueryErrorBehavior ) { + errorBehavior = (JsonQueryErrorBehavior) node; + nextIndex++; + } + } + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonQueryEmptyBehavior ) { + emptyBehavior = (JsonQueryEmptyBehavior) node; + } + } + final Expression jsonDocument = (Expression) sqlAstArguments.get( 0 ); + return new JsonQueryArguments( + jsonDocument, + (Expression) sqlAstArguments.get( 1 ), + jsonDocument.getExpressionType() != null + && jsonDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isJson(), + passingClause, + wrapMode, + errorBehavior, + emptyBehavior + ); + } + } +} 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 new file mode 100644 index 000000000000..f057613286bf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java @@ -0,0 +1,221 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.hibernate.query.ReturnableType; +import org.hibernate.query.spi.QueryEngine; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +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.SqmJsonValueExpression; +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.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.IMPLICIT_JSON; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.JSON; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; + +/** + * Standard json_value function. + */ +public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + protected final boolean supportsJsonPathExpression; + protected final boolean supportsJsonPathPassingClause; + + public JsonValueFunction( + TypeConfiguration typeConfiguration, + boolean supportsJsonPathExpression, + boolean supportsJsonPathPassingClause) { + super( + "json_value", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY ) + ), + new CastTargetReturnTypeResolver( typeConfiguration ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) + ); + this.supportsJsonPathExpression = supportsJsonPathExpression; + this.supportsJsonPathPassingClause = supportsJsonPathPassingClause; + } + + @Override + protected SelfRenderingSqmFunction generateSqmFunctionExpression( + List> arguments, + ReturnableType impliedResultType, + QueryEngine queryEngine) { + return new SqmJsonValueExpression<>( + this, + this, + arguments, + impliedResultType, + getArgumentsValidator(), + getReturnTypeResolver(), + queryEngine.getCriteriaBuilder(), + getName() + ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + render( sqlAppender, JsonValueArguments.extract( sqlAstArguments ), returnType, walker ); + } + + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "json_value(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( supportsJsonPathPassingClause || passingClause == null ) { + if ( supportsJsonPathExpression ) { + arguments.jsonPath().accept( walker ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + 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() ); + } + } + } + else { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( " returning " ); + arguments.returningType().accept( walker ); + } + if ( arguments.errorBehavior() != null ) { + if ( arguments.errorBehavior() == JsonValueErrorBehavior.ERROR ) { + sqlAppender.appendSql( " error on error" ); + } + else if ( arguments.errorBehavior() != JsonValueErrorBehavior.NULL ) { + final Expression defaultExpression = arguments.errorBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on error" ); + } + } + if ( arguments.emptyBehavior() != null ) { + if ( arguments.emptyBehavior() == JsonValueEmptyBehavior.ERROR ) { + sqlAppender.appendSql( " error on empty" ); + } + else if ( arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + final Expression defaultExpression = arguments.emptyBehavior().getDefaultExpression(); + assert defaultExpression != null; + sqlAppender.appendSql( " default " ); + defaultExpression.accept( walker ); + sqlAppender.appendSql( " on empty" ); + } + } + sqlAppender.appendSql( ')' ); + } + + protected record JsonValueArguments( + Expression jsonDocument, + Expression jsonPath, + boolean isJsonType, + @Nullable JsonPathPassingClause passingClause, + @Nullable CastTarget returningType, + @Nullable JsonValueErrorBehavior errorBehavior, + @Nullable JsonValueEmptyBehavior emptyBehavior) { + public static JsonValueArguments extract(List sqlAstArguments) { + int nextIndex = 2; + JsonPathPassingClause passingClause = null; + CastTarget castTarget = null; + JsonValueErrorBehavior errorBehavior = null; + JsonValueEmptyBehavior emptyBehavior = null; + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof CastTarget ) { + castTarget = (CastTarget) 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 JsonValueErrorBehavior ) { + errorBehavior = (JsonValueErrorBehavior) node; + nextIndex++; + } + } + if ( nextIndex < sqlAstArguments.size() ) { + final SqlAstNode node = sqlAstArguments.get( nextIndex ); + if ( node instanceof JsonValueEmptyBehavior ) { + emptyBehavior = (JsonValueEmptyBehavior) node; + } + } + final Expression jsonDocument = (Expression) sqlAstArguments.get( 0 ); + return new JsonValueArguments( + jsonDocument, + (Expression) sqlAstArguments.get( 1 ), + jsonDocument.getExpressionType() != null + && jsonDocument.getExpressionType().getSingleJdbcMapping().getJdbcType().isJson(), + passingClause, + castTarget, + errorBehavior, + emptyBehavior + ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAggFunction.java new file mode 100644 index 000000000000..6c822bd8a332 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAggFunction.java @@ -0,0 +1,104 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MariaDB json_arrayagg function. + */ +public class MariaDBJsonArrayAggFunction extends JsonArrayAggFunction { + + public MariaDBJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + sqlAppender.appendSql( "concat('[',group_concat(" ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + if ( nullBehavior != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arg, nullBehavior, translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arg, nullBehavior, translator ); + } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( " separator ','),']')" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert SQL type to JSON type + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + sqlAppender.appendSql( "json_extract(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( "),'$[0]')" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'null')" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayFunction.java new file mode 100644 index 000000000000..4fad7aa58735 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayFunction.java @@ -0,0 +1,73 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +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.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MariaDB json_array function. + */ +public class MariaDBJsonArrayFunction extends JsonArrayFunction { + + public MariaDBJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "json_array()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = null; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select json_arrayagg(t.v order by t.i) from (select 0 i,json_extract(json_array(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( "),'$[0]') v" ); + for ( int i = 1; i < argumentsCount; i++ ) { + sqlAppender.appendSql( " union all select " ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ",json_extract(json_array(" ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( "),'$[0]')" ); + } + sqlAppender.appendSql( ") t where t.v is not null)" ); + } + else { + sqlAppender.appendSql( "json_array" ); + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonObjectAggFunction.java new file mode 100644 index 000000000000..b3469ebb0d4a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonObjectAggFunction.java @@ -0,0 +1,41 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MariaDB json_objectagg function. + */ +public class MariaDBJsonObjectAggFunction extends MySQLJsonObjectAggFunction { + + public MariaDBJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert SQL type to JSON type + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + sqlAppender.appendSql( "json_extract(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( "),'$[0]')" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'null')" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonQueryFunction.java new file mode 100644 index 000000000000..08d1b8f4805a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonQueryFunction.java @@ -0,0 +1,107 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.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.type.spi.TypeConfiguration; + +/** + * MariaDB json_query function. + */ +public class MariaDBJsonQueryFunction extends JsonQueryFunction { + + public MariaDBJsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // json_extract errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR + || arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR + // Can't emulate DEFAULT ON EMPTY since we can't differentiate between a NULL value and EMPTY + || arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { + super.render( sqlAppender, arguments, returnType, walker ); + } + else { + final JsonQueryWrapMode wrapMode = arguments.wrapMode(); + final DecorationMode decorationMode = determineDecorationMode( arguments, walker, wrapMode ); + if ( decorationMode == DecorationMode.WRAP ) { + sqlAppender.appendSql( "concat('['," ); + } + else if ( decorationMode == DecorationMode.TRIM ) { + sqlAppender.appendSql( "trim(leading '[' from trim(trailing ']' from " ); + } + + sqlAppender.appendSql( "nullif(json_extract(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( "," ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause == null ) { + arguments.jsonPath().accept( walker ); + } + else { + JsonPathHelper.appendJsonPathConcatPassingClause( + sqlAppender, + arguments.jsonPath(), + passingClause, walker + ); + } + sqlAppender.appendSql( "),'null')" ); + if ( decorationMode == DecorationMode.WRAP ) { + sqlAppender.appendSql( ",']')" ); + } + else if ( decorationMode == DecorationMode.TRIM ) { + sqlAppender.appendSql( "))" ); + } + } + } + + enum DecorationMode { NONE, WRAP, TRIM } + + private static DecorationMode determineDecorationMode( + JsonQueryArguments arguments, + SqlAstTranslator walker, + JsonQueryWrapMode wrapMode) { + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + final String jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + if ( jsonPath.indexOf( '*' ) != -1 ) { + // If the JSON path contains a star, MySQL will always wrap the result + return DecorationMode.NONE; + } + else { + // Otherwise we have to wrap the result manually + return DecorationMode.WRAP; + } + } + else if ( wrapMode == JsonQueryWrapMode.WITHOUT_WRAPPER ) { + final String jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + if ( jsonPath.indexOf( '*' ) != -1 ) { + // If the JSON path contains a star, MySQL will always wrap the result, + // so we have to trim the brackets + return DecorationMode.TRIM; + } + else { + // Nothing to do + return DecorationMode.NONE; + } + } + else { + return DecorationMode.NONE; + } + } +} 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 new file mode 100644 index 000000000000..c144dee92a96 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java @@ -0,0 +1,64 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MariaDB json_value function. + */ +public class MariaDBJsonValueFunction extends JsonValueFunction { + + public MariaDBJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.NULL ) { + // MariaDB reports the error 4038 as warning and simply returns null + throw new QueryException( "Can't emulate on error clause on MariaDB" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on MariaDB" ); + } + if ( arguments.returningType() != null ) { + sqlAppender.append( "cast(" ); + } + sqlAppender.appendSql( "json_unquote(nullif(json_extract(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( "," ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause == null ) { + arguments.jsonPath().accept( walker ); + } + else { + JsonPathHelper.appendJsonPathConcatPassingClause( + sqlAppender, + arguments.jsonPath(), + passingClause, walker + ); + } + sqlAppender.appendSql( "),'null'))" ); + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayAggFunction.java new file mode 100644 index 000000000000..4e568127df79 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayAggFunction.java @@ -0,0 +1,104 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MySQL json_arrayagg function. + */ +public class MySQLJsonArrayAggFunction extends JsonArrayAggFunction { + + public MySQLJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + sqlAppender.appendSql( "cast(concat('[',group_concat(" ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + if ( nullBehavior != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arg, nullBehavior, translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arg, nullBehavior, translator ); + } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( " separator ','),']') as json)" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert SQL type to JSON type + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + sqlAppender.appendSql( "json_extract(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( "),'$[0]')" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",cast('null' as json))" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayFunction.java new file mode 100644 index 000000000000..80b2ae965b26 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayFunction.java @@ -0,0 +1,79 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MySQL json_array function. + */ +public class MySQLJsonArrayFunction extends JsonArrayFunction { + + public MySQLJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "json_array()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = null; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + // MySQL does not support retaining the order of arguments when using json_arrayagg, + // so we have to use manual array appending instead + sqlAppender.appendSql( '(' ); + for ( int i = argumentsCount - 1; i > 0; i-- ) { + sqlAppender.appendSql( "select case when t.v is null then x.v else json_array_append(x.v,'$',t.v) end v from (select " ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( " v) t,(" ); + } + sqlAppender.appendSql( "select case when t.v is null then json_array() else json_array(t.v) end v from (select " ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( " v) t" ); + for ( int i = 1; i < argumentsCount; i++ ) { + sqlAppender.appendSql( ") x" ); + } + sqlAppender.appendSql( ')' ); + } + else { + sqlAppender.appendSql( "json_array" ); + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java new file mode 100644 index 000000000000..f7e71abd3c3e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java @@ -0,0 +1,46 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonPathPassingClause; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MySQL json_exists function. + */ +public class MySQLJsonExistsFunction extends JsonExistsFunction { + + public MySQLJsonExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + final JsonPathPassingClause passingClause = arguments.passingClause(); + sqlAppender.appendSql( "json_contains_path(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ",'one'," ); + if ( passingClause == null ) { + arguments.jsonPath().accept( walker ); + } + else { + JsonPathHelper.appendJsonPathConcatPassingClause( + sqlAppender, + arguments.jsonPath(), + passingClause, walker + ); + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectAggFunction.java new file mode 100644 index 000000000000..152925c59ccc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectAggFunction.java @@ -0,0 +1,81 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MySQL json_objectagg function. + */ +public class MySQLJsonObjectAggFunction extends JsonObjectAggFunction { + + public MySQLJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( ",", false, typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + if ( arguments.uniqueKeysBehavior() == JsonObjectAggUniqueKeysBehavior.WITH ) { + throw new QueryException( "Can't emulate json_objectagg 'with unique keys' clause." ); + } + sqlAppender.appendSql( "concat('{',group_concat(concat(json_quote(" ); + arguments.key().accept( translator ); + sqlAppender.appendSql( "),':'," ); + if ( caseWrapper ) { + if ( arguments.nullBehavior() != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_objectagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + } + sqlAppender.appendSql( ") separator ','),'}')" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert SQL type to JSON type + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + sqlAppender.appendSql( "json_extract(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( "),'$[0]')" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",cast('null' as json))" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectFunction.java new file mode 100644 index 000000000000..47bd7fe67529 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectFunction.java @@ -0,0 +1,75 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +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.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MySQL json_object function. + */ +public class MySQLJsonObjectFunction extends JsonObjectFunction { + + public MySQLJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "json_object()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select json_objectagg(t.k,t.v) from (select " ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( " k,json_extract(json_array(" ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( "),'$[0]') v" ); + for ( int i = 2; i < argumentsCount; i += 2 ) { + sqlAppender.appendSql( " union all select " ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( ",json_extract(json_array(" ); + sqlAstArguments.get( i + 1 ).accept( walker ); + sqlAppender.appendSql( "),'$[0]')" ); + } + sqlAppender.appendSql( ") t where t.v<>json_extract(json_array(null), '$[0]'))" ); + } + else { + sqlAppender.appendSql( "json_object" ); + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonQueryFunction.java new file mode 100644 index 000000000000..e66a75970474 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonQueryFunction.java @@ -0,0 +1,107 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.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.type.spi.TypeConfiguration; + +/** + * MySQL json_query function. + */ +public class MySQLJsonQueryFunction extends JsonQueryFunction { + + public MySQLJsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // json_extract errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR + || arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR + // Can't emulate DEFAULT ON EMPTY since we can't differentiate between a NULL value and EMPTY + || arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { + super.render( sqlAppender, arguments, returnType, walker ); + } + else { + final JsonQueryWrapMode wrapMode = arguments.wrapMode(); + final DecorationMode decorationMode = determineDecorationMode( arguments, walker, wrapMode ); + if ( decorationMode == DecorationMode.WRAP ) { + sqlAppender.appendSql( "concat('['," ); + } + else if ( decorationMode == DecorationMode.TRIM ) { + sqlAppender.appendSql( "cast(trim(leading '[' from trim(trailing ']' from " ); + } + + sqlAppender.appendSql( "nullif(json_extract(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( "," ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause == null ) { + arguments.jsonPath().accept( walker ); + } + else { + JsonPathHelper.appendJsonPathConcatPassingClause( + sqlAppender, + arguments.jsonPath(), + passingClause, walker + ); + } + sqlAppender.appendSql( "),cast('null' as json))" ); + if ( decorationMode == DecorationMode.WRAP ) { + sqlAppender.appendSql( ",']')" ); + } + else if ( decorationMode == DecorationMode.TRIM ) { + sqlAppender.appendSql( ")) as json)" ); + } + } + } + + enum DecorationMode { NONE, WRAP, TRIM } + + private static DecorationMode determineDecorationMode( + JsonQueryArguments arguments, + SqlAstTranslator walker, + JsonQueryWrapMode wrapMode) { + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + final String jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + if ( jsonPath.indexOf( '*' ) != -1 ) { + // If the JSON path contains a star, MySQL will always wrap the result + return DecorationMode.NONE; + } + else { + // Otherwise we have to wrap the result manually + return DecorationMode.WRAP; + } + } + else if ( wrapMode == JsonQueryWrapMode.WITHOUT_WRAPPER ) { + final String jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + if ( jsonPath.indexOf( '*' ) != -1 ) { + // If the JSON path contains a star, MySQL will always wrap the result, + // so we have to trim the brackets + return DecorationMode.TRIM; + } + else { + // Nothing to do + return DecorationMode.NONE; + } + } + else { + return DecorationMode.NONE; + } + } +} 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 new file mode 100644 index 000000000000..138c536ce1e4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java @@ -0,0 +1,65 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MySQL json_value function. + */ +public class MySQLJsonValueFunction extends JsonValueFunction { + + public MySQLJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // json_extract errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.ERROR + || arguments.emptyBehavior() == JsonValueEmptyBehavior.ERROR + // Can't emulate DEFAULT ON EMPTY since we can't differentiate between a NULL value and EMPTY + || arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + super.render( sqlAppender, arguments, returnType, walker ); + } + else { + if ( arguments.returningType() != null ) { + sqlAppender.append( "cast(" ); + } + sqlAppender.appendSql( "json_unquote(nullif(json_extract(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( "," ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause == null ) { + arguments.jsonPath().accept( walker ); + } + else { + JsonPathHelper.appendJsonPathConcatPassingClause( + sqlAppender, + arguments.jsonPath(), + passingClause, walker + ); + } + sqlAppender.appendSql( "),cast('null' as json)))" ); + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java new file mode 100644 index 000000000000..8926ff0ad753 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java @@ -0,0 +1,39 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +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.JsonNullBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_arrayagg function. + */ +public class OracleJsonArrayAggFunction extends JsonArrayAggFunction { + + public OracleJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + arg.accept( translator ); + final JdbcMappingContainer expressionType = arg.getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() + && !SqlTypes.isJsonType( expressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() ) ) { + sqlAppender.appendSql( " format json" ); + } + } +} 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 new file mode 100644 index 000000000000..3a56bc71877f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java @@ -0,0 +1,54 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.dialect.function.CastFunction; +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.type.spi.TypeConfiguration; + +/** + * Oracle json_array function. + */ +public class OracleJsonArrayFunction extends JsonArrayFunction { + + private final CastTarget stringCastTarget; + private CastFunction castFunction; + + public OracleJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + if ( ExpressionTypeHelper.isNonNativeBoolean( value ) ) { + CastFunction castFunction = this.castFunction; + if ( castFunction == null ) { + castFunction = this.castFunction = (CastFunction) walker.getSessionFactory() + .getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( "cast" ); + } + castFunction.render( + sqlAppender, + List.of( value, stringCastTarget ), + (ReturnableType) stringCastTarget.getJdbcMapping(), + walker + ); + sqlAppender.appendSql( " format json" ); + } + else { + value.accept( walker ); + } + } +} 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 new file mode 100644 index 000000000000..049df279c481 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java @@ -0,0 +1,54 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.dialect.function.CastFunction; +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.type.spi.TypeConfiguration; + +/** + * Oracle json_object function. + */ +public class OracleJsonObjectFunction extends JsonObjectFunction { + + private final CastTarget stringCastTarget; + private CastFunction castFunction; + + public OracleJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true ); + this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + if ( ExpressionTypeHelper.isNonNativeBoolean( value ) ) { + CastFunction castFunction = this.castFunction; + if ( castFunction == null ) { + castFunction = this.castFunction = (CastFunction) walker.getSessionFactory() + .getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( "cast" ); + } + castFunction.render( + sqlAppender, + List.of( value, stringCastTarget ), + (ReturnableType) stringCastTarget.getJdbcMapping(), + walker + ); + sqlAppender.appendSql( " format json" ); + } + else { + value.accept( walker ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAggFunction.java new file mode 100644 index 000000000000..1e11f35dbe66 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAggFunction.java @@ -0,0 +1,102 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_arrayagg function. + */ +public class PostgreSQLJsonArrayAggFunction extends JsonArrayAggFunction { + + private final boolean supportsStandard; + + public PostgreSQLJsonArrayAggFunction(boolean supportsStandard, TypeConfiguration typeConfiguration) { + super( true, typeConfiguration ); + this.supportsStandard = supportsStandard; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + if ( supportsStandard ) { + super.render( sqlAppender, sqlAstArguments, filter, withinGroup, returnType, translator ); + } + else { + final String jsonTypeName = translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry() + .getTypeName( SqlTypes.JSON, translator.getSessionFactory().getJdbcServices().getDialect() ); + sqlAppender.appendSql( jsonTypeName ); + sqlAppender.appendSql( "_agg" ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + sqlAppender.appendSql( '(' ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + renderArgument( sqlAppender, arg, nullBehavior, translator ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( ')' ); + + if ( filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( " and " ); + arg.accept( translator ); + sqlAppender.appendSql( " is not null" ); + } + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + else if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( " filter (where " ); + arg.accept( translator ); + sqlAppender.appendSql( " is not null)" ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayFunction.java new file mode 100644 index 000000000000..9a37181f2441 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayFunction.java @@ -0,0 +1,87 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_array function. + */ +public class PostgreSQLJsonArrayFunction extends JsonArrayFunction { + + public PostgreSQLJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "jsonb_build_array()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = null; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select jsonb_agg(t.v order by t.i) from (values" ); + char separator = ' '; + for ( int i = 0; i < argumentsCount; i++ ) { + final SqlAstNode node = sqlAstArguments.get( i ); + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( '(' ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ',' ); + if ( node instanceof Literal && ( (Literal) node ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + node.accept( walker ); + if ( node instanceof Literal literal && literal.getJdbcMapping().getJdbcType().isString() ) { + // PostgreSQL until version 16 is not smart enough to infer the type of a string literal + sqlAppender.appendSql( "::text" ); + } + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( ')' ); + separator = ','; + } + sqlAppender.appendSql( ") t(i,v) where t.v is not null)" ); + } + else { + sqlAppender.appendSql( "jsonb_build_array" ); + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + } + } +} 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 new file mode 100644 index 000000000000..aa193c605500 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java @@ -0,0 +1,52 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.Map; + +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.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_exists function. + */ +public class PostgreSQLJsonExistsFunction extends JsonExistsFunction { + + public PostgreSQLJsonExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + sqlAppender.appendSql( "jsonb_path_exists(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + arguments.jsonPath().accept( walker ); + 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( ')' ); + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectAggFunction.java new file mode 100644 index 000000000000..df73b06ff828 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectAggFunction.java @@ -0,0 +1,82 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_objectagg function. + */ +public class PostgreSQLJsonObjectAggFunction extends JsonObjectAggFunction { + + private final boolean supportsStandard; + + public PostgreSQLJsonObjectAggFunction(boolean supportsStandard, TypeConfiguration typeConfiguration) { + super( ":", true, typeConfiguration ); + this.supportsStandard = supportsStandard; + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + if ( supportsStandard ) { + super.render( sqlAppender, arguments, filter, returnType, translator ); + } + else { + if ( arguments.uniqueKeysBehavior() == JsonObjectAggUniqueKeysBehavior.WITH ) { + throw new QueryException( "Can't emulate json_objectagg 'with unique keys' clause." ); + } + final String jsonTypeName = translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry() + .getTypeName( SqlTypes.JSON, translator.getSessionFactory().getJdbcServices().getDialect() ); + sqlAppender.appendSql( jsonTypeName ); + sqlAppender.appendSql( "_object_agg" ); + sqlAppender.appendSql( '(' ); + arguments.key().accept( translator ); + sqlAppender.appendSql( ',' ); + arguments.value().accept( translator ); + sqlAppender.appendSql( ')' ); + + if ( filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + if ( arguments.nullBehavior() != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( " and " ); + arguments.value().accept( translator ); + sqlAppender.appendSql( " is not null" ); + } + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + else if ( arguments.nullBehavior() != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( " filter (where " ); + arguments.value().accept( translator ); + sqlAppender.appendSql( " is not null)" ); + } + } + } + + @Override + protected void renderUniqueAndReturningClause(SqlAppender sqlAppender, JsonObjectAggArguments arguments, SqlAstTranslator translator) { + renderUniqueClause( sqlAppender, arguments, translator ); + renderReturningClause( sqlAppender, arguments, translator ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectFunction.java new file mode 100644 index 000000000000..3bd07316f386 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectFunction.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_object function. + */ +public class PostgreSQLJsonObjectFunction extends JsonObjectFunction { + + public PostgreSQLJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "jsonb_build_object()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select jsonb_object_agg(t.k,t.v) from (values" ); + char separator = ' '; + for ( int i = 0; i < argumentsCount; i += 2 ) { + final SqlAstNode key = sqlAstArguments.get( i ); + final SqlAstNode value = sqlAstArguments.get( i + 1 ); + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( '(' ); + key.accept( walker ); + sqlAppender.appendSql( ',' ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( walker ); + if ( value instanceof Literal literal && literal.getJdbcMapping().getJdbcType().isString() ) { + // PostgreSQL until version 16 is not smart enough to infer the type of a string literal + sqlAppender.appendSql( "::text" ); + } + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( ')' ); + separator = ','; + } + sqlAppender.appendSql( ") t(k,v) where t.v is not null)" ); + } + else { + sqlAppender.appendSql( "jsonb_build_object" ); + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + } + } +} 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 new file mode 100644 index 000000000000..4782d9912a90 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java @@ -0,0 +1,97 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.Map; + +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.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_query function. + */ +public class PostgreSQLJsonQueryFunction extends JsonQueryFunction { + + public PostgreSQLJsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + 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" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + } + final JsonQueryWrapMode wrapMode = arguments.wrapMode(); + + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "jsonb_path_query_array(" ); + } + else if ( wrapMode == JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER ) { + sqlAppender.appendSql( "(select case when count(*) over () > 1 then jsonb_agg(t.v) else percentile_disc(0) within group (order by t.v) end from jsonb_path_query(" ); + } + else { + sqlAppender.appendSql( "(select t.v 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)" ); + } + sqlAppender.appendSql( ',' ); + final SqlAstNode jsonPath = arguments.jsonPath(); + 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( ')' ); + } + + if ( wrapMode != JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( ") t(v))" ); + } + else { + sqlAppender.appendSql( ')' ); + } + } +} 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 new file mode 100644 index 000000000000..f6b7f8b63ad1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java @@ -0,0 +1,89 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.Map; + +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.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +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; + +/** + * PostgreSQL json_value function. + */ +public class PostgreSQLJsonValueFunction extends JsonValueFunction { + + public PostgreSQLJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, true ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // jsonb_path_query_first errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on PostgreSQL" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on PostgreSQL" ); + } + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( "cast(" ); + } + sqlAppender.appendSql( "jsonb_path_query_first(" ); + final boolean needsCast = !arguments.isJsonType() && arguments.jsonDocument() instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + arguments.jsonDocument().accept( walker ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + final SqlAstNode jsonPath = arguments.jsonPath(); + 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( ')' ); + } + // Unquote the value + sqlAppender.appendSql( ")#>>'{}'" ); + if ( arguments.returningType() != null ) { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java new file mode 100644 index 000000000000..8302e56b823f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java @@ -0,0 +1,101 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_arrayagg function. + */ +public class SQLServerJsonArrayAggFunction extends JsonArrayAggFunction { + + public SQLServerJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + sqlAppender.appendSql( "'['+string_agg(" ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + if ( nullBehavior != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arg, nullBehavior, translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arg, nullBehavior, translator ); + } + sqlAppender.appendSql( ",',')" ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( "+']'" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + sqlAppender.appendSql( "substring(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null),2,len(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null))-2)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java new file mode 100644 index 000000000000..a5066f7569a9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_array function. + */ +public class SQLServerJsonArrayFunction extends JsonArrayFunction { + + public SQLServerJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + if ( ExpressionTypeHelper.isBoolean( value ) ) { + sqlAppender.appendSql( "cast(" ); + value.accept( walker ); + sqlAppender.appendSql( " as bit)" ); + } + else { + value.accept( walker ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java new file mode 100644 index 000000000000..ca851ca4e6c2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_exists function. + */ +public class SQLServerJsonExistsFunction extends JsonExistsFunction { + + public SQLServerJsonExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + public boolean isPredicate() { + return false; + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() == JsonExistsErrorBehavior.TRUE ) { + throw new QueryException( "Can't emulate json_exists(... true on error) on SQL Server" ); + } + if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) { + sqlAppender.append( '(' ); + } + sqlAppender.appendSql( "json_path_exists(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + sqlAppender.appendSql( ')' ); + if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) { + // json_path_exists returns 0 if an invalid JSON is given, + // so we have to run openjson to be sure the json is valid and potentially throw an error + sqlAppender.appendSql( "=1 or (select v from openjson(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ") with (v varchar(max) " ); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + sqlAppender.appendSql( ")) is null)" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java new file mode 100644 index 000000000000..ac06321cf266 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java @@ -0,0 +1,80 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.Clause; +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.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_objectagg function. + */ +public class SQLServerJsonObjectAggFunction extends JsonObjectAggFunction { + + public SQLServerJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( ",", false, typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + if ( arguments.uniqueKeysBehavior() == JsonObjectAggUniqueKeysBehavior.WITH ) { + throw new QueryException( "Can't emulate json_objectagg 'with unique keys' clause." ); + } + sqlAppender.appendSql( "'{'+string_agg(" ); + renderArgument( sqlAppender, arguments.key(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( "+':'+" ); + if ( caseWrapper ) { + if ( arguments.nullBehavior() != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_objectagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + } + sqlAppender.appendSql( ",',')+'}'" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + // Convert SQL type to JSON type + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + sqlAppender.appendSql( "substring(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null),2,len(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null))-2)" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'null')" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java new file mode 100644 index 000000000000..d32fd2e116ab --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_object function. + */ +public class SQLServerJsonObjectFunction extends JsonObjectFunction { + + public SQLServerJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true ); + } + + @Override + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator walker) { + if ( ExpressionTypeHelper.isBoolean( value ) ) { + sqlAppender.appendSql( "cast(" ); + value.accept( walker ); + sqlAppender.appendSql( " as bit)" ); + } + else { + value.accept( walker ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonQueryFunction.java new file mode 100644 index 000000000000..64e1fd36888a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonQueryFunction.java @@ -0,0 +1,164 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +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.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.type.spi.TypeConfiguration; + +/** + * SQL Server json_query function. + */ +public class SQLServerJsonQueryFunction extends JsonQueryFunction { + + public SQLServerJsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // openjson errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on SQL server" ); + } + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( + walker.getLiteralValue( arguments.jsonPath() ) + ); + if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_ARRAY + || arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sqlAppender.appendSql( "coalesce(" ); + } + render( sqlAppender, arguments, jsonPathElements, jsonPathElements.size() - 1, walker ); + if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_ARRAY ) { + sqlAppender.appendSql( ",'[]')" ); + } + else if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.EMPTY_OBJECT ) { + sqlAppender.appendSql( ",'{}')" ); + } + } + + private void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + List jsonPathElements, + int index, + SqlAstTranslator walker) { + sqlAppender.appendSql( "(select " ); + final boolean aggregate = index == jsonPathElements.size() - 1 && ( + arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER + || arguments.wrapMode() == JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER + ); + if ( aggregate ) { + if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "'['+" ); + } + else { + sqlAppender.appendSql( "case when count(*)>1 then '[' else '' end+" ); + } + sqlAppender.appendSql( "string_agg(t.v,',')" ); + if ( arguments.wrapMode() == JsonQueryWrapMode.WITH_WRAPPER ) { + sqlAppender.appendSql( "+']'" ); + } + else { + sqlAppender.appendSql( "+case when count(*)>1 then ']' else '' end" ); + } + + // openjson unquotes values, so we have to quote them again + sqlAppender.appendSql( " from (select " ); + // type 0 is a null literal + sqlAppender.appendSql( "case t.type when 0 then 'null' when 1 then "); + // type 1 is a string literal. to quote it, we use for json path and trim the string down to just the value + sqlAppender.appendSql( + "(select substring(a.v,6,len(a.v)-6) from (select t.value a for json path,without_array_wrapper) a(v))" ); + sqlAppender.appendSql( " else t.value end v"); + + } + else { + sqlAppender.appendSql( "t.value" ); + } + sqlAppender.appendSql( " from openjson(" ); + if ( index == 0 ) { + arguments.jsonDocument().accept( walker ); + } + else { + render( sqlAppender, arguments, jsonPathElements, index - 1, walker ); + } + sqlAppender.appendSql( ')' ); + if ( arguments.emptyBehavior() == JsonQueryEmptyBehavior.ERROR ) { + sqlAppender.appendSql( " with (value nvarchar(max) " ); + final JsonPathHelper.JsonPathElement jsonPathElement = jsonPathElements.get( index ); + if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSql( "'strict $." ); + final String name = attribute.attribute(); + for ( int i = 0; i < name.length(); i++ ) { + final char c = name.charAt( i ); + if ( c == '\'' ) { + sqlAppender.append( '\'' ); + } + sqlAppender.append( c ); + } + sqlAppender.append( '\'' ); + } + else if ( jsonPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) { + sqlAppender.appendSql( "'strict $[" ); + sqlAppender.appendSql( indexAccess.index() ); + sqlAppender.appendSql( "]'" ); + } + else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess indexAccess ) { + final JsonPathPassingClause passingClause = arguments.passingClause(); + assert passingClause != null; + final Object literalValue = walker.getLiteralValue( + passingClause.getPassingExpressions().get( indexAccess.parameterName() ) + ); + sqlAppender.appendSql( "'strict $[" ); + sqlAppender.appendSql( literalValue.toString() ); + sqlAppender.appendSql( "]'" ); + } + else { + throw new UnsupportedOperationException( "Unsupported JSON path expression: " + jsonPathElement ); + } + sqlAppender.appendSql( " as json) t" ); + } + else { + sqlAppender.appendSql( " t where " ); + final JsonPathHelper.JsonPathElement jsonPathElement = jsonPathElements.get( index ); + if ( jsonPathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSql( "t.[key]=" ); + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( jsonPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) { + sqlAppender.appendSql( "t.[key]=" ); + sqlAppender.appendSql( indexAccess.index() ); + } + else if ( jsonPathElement instanceof JsonPathHelper.JsonParameterIndexAccess indexAccess ) { + final JsonPathPassingClause passingClause = arguments.passingClause(); + assert passingClause != null; + sqlAppender.appendSql( "t.[key]=" ); + passingClause.getPassingExpressions().get( indexAccess.parameterName() ).accept( walker ); + } + else { + throw new UnsupportedOperationException( "Unsupported JSON path expression: " + jsonPathElement ); + } + } + if ( aggregate ) { + sqlAppender.appendSql( ") t" ); + } + sqlAppender.appendSql( ")" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java new file mode 100644 index 000000000000..7a0663b82b9e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java @@ -0,0 +1,80 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +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.JsonPathPassingClause; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_value function. + */ +public class SQLServerJsonValueFunction extends JsonValueFunction { + + public SQLServerJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + // openjson errors by default + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on SQL server" ); + } + sqlAppender.appendSql( "(select v from openjson(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ") with (v " ); + if ( arguments.returningType() != null ) { + arguments.returningType().accept( walker ); + } + else { + sqlAppender.appendSql( "nvarchar(max)" ); + } + sqlAppender.appendSql( ' ' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + // The strict modifier will cause an error to be thrown if a field doesn't exist + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "strict ", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + "strict " + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + } + else if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + arguments.jsonPath().accept( walker ); + } + sqlAppender.appendSql( "))" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/NumberHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/NumberHelper.java new file mode 100644 index 000000000000..8db8505cb9b6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/NumberHelper.java @@ -0,0 +1,63 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.internal.util; + + +public final class NumberHelper { + + private NumberHelper() { + } + + public static int digitCount(int number) { + if ( number < 100000 ) { + if ( number < 100 ) { + if ( number < 10 ) { + return 1; + } + else { + return 2; + } + } + else { + if ( number < 1000 ) { + return 3; + } + else { + if ( number < 10000 ) { + return 4; + } + else { + return 5; + } + } + } + } + else { + if ( number < 10000000 ) { + if ( number < 1000000 ) { + return 6; + } + else { + return 7; + } + } + else { + if ( number < 100000000 ) { + return 8; + } + else { + if ( number < 1000000000 ) { + return 9; + } + else { + return 10; + } + } + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java index 695d866248e7..30208e9d7626 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/QuotingHelper.java @@ -6,6 +6,8 @@ */ package org.hibernate.internal.util; +import org.hibernate.sql.ast.spi.SqlAppender; + public final class QuotingHelper { private QuotingHelper() { /* static methods only - hide constructor */ @@ -152,4 +154,25 @@ public static String unquoteJavaStringLiteral(String text) { } return sb.toString(); } + + public static void appendDoubleQuoteEscapedString(StringBuilder sb, String text) { + appendWithDoubleEscaping( sb, text, '"' ); + } + + public static void appendSingleQuoteEscapedString(StringBuilder sb, String text) { + appendWithDoubleEscaping( sb, text, '\'' ); + } + + private static void appendWithDoubleEscaping(StringBuilder sb, String text, char quoteChar) { + sb.append( quoteChar ); + for ( int i = 0; i < text.length(); i++ ) { + final char c = text.charAt( i ); + if ( c == quoteChar ) { + sb.append( quoteChar ); + } + sb.append( c ); + } + sb.append( quoteChar ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java index 1a71df69bb6e..fabd291da0e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java @@ -17,10 +17,12 @@ import org.hibernate.Incubating; import org.hibernate.boot.registry.StandardServiceRegistry; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.service.ServiceRegistry; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.JdbcTypeNameMapper; @@ -511,6 +513,21 @@ public static synchronized int getPreferredSqlTypeCodeForBoolean(StandardService .getPreferredSqlTypeCodeForBoolean(); } + @Incubating + public static synchronized int getPreferredSqlTypeCodeForBoolean(ServiceRegistry serviceRegistry, Dialect dialect) { + final Integer typeCode = serviceRegistry.requireService( ConfigurationService.class ).getSetting( + AvailableSettings.PREFERRED_BOOLEAN_JDBC_TYPE, + TypeCodeConverter.INSTANCE + ); + if ( typeCode != null ) { + INCUBATION_LOGGER.incubatingSetting( AvailableSettings.PREFERRED_BOOLEAN_JDBC_TYPE ); + return typeCode; + } + + // default to the Dialect answer + return dialect.getPreferredSqlTypeCodeForBoolean(); + } + @Incubating public static synchronized int getPreferredSqlTypeCodeForDuration(StandardServiceRegistry serviceRegistry) { final Integer explicitSetting = serviceRegistry.requireService( ConfigurationService.class ).getSetting( 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 ee86f8664f22..4fb5af4f32ae 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 @@ -3682,6 +3682,236 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @Incubating JpaPredicate collectionIntersectsNullable(Collection collection1, Expression> collectionExpression2); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JSON functions + + /** + * @see #jsonValue(Expression, String, Class) + * @since 7.0 + */ + @Incubating + JpaJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath); + + /** + * Extracts a value by JSON path from a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath, Class returningType); + + /** + * @see #jsonValue(Expression, Expression, Class) + * @since 7.0 + */ + @Incubating + JpaJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath); + + /** + * Extracts a value by JSON path from a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath, Class returningType); + + /** + * @see #jsonQuery(Expression, Expression) + * @since 7.0 + */ + @Incubating + JpaJsonQueryExpression jsonQuery(Expression jsonDocument, String jsonPath); + + /** + * Queries values by JSON path from a JSON document. + * @since 7.0 + */ + @Incubating + JpaJsonQueryExpression jsonQuery(Expression jsonDocument, Expression jsonPath); + + /** + * Checks if a JSON document contains a node for the given JSON path. + * + * @since 7.0 + */ + @Incubating + JpaJsonExistsExpression jsonExists(Expression jsonDocument, String jsonPath); + + /** + * Checks if a JSON document contains a node for the given JSON path. + * + * @since 7.0 + */ + @Incubating + JpaJsonExistsExpression jsonExists(Expression jsonDocument, Expression jsonPath); + + /** + * Create a JSON object from the given map of key values. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObject(Map> keyValues); + + /** + * Create a JSON object from the given map of key values, retaining {@code null} values in the JSON. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectWithNulls(Map> keyValues); + + /** + * Create a JSON array from the array of values. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArray(Expression... values); + + /** + * Create a JSON object from the given array of values, retaining {@code null} values in the JSON array. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayWithNulls(Expression... values); + + /** + * Aggregates the given value into a JSON array. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAgg(Expression value); + + /** + * Aggregates the given value into a JSON array. + * Ordering values based on the given order by items. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAgg(Expression value, JpaOrder... orderBy); + + /** + * Aggregates the given value into a JSON array. + * Filtering rows that don't match the given filter predicate. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAgg(Expression value, Predicate filter); + + /** + * Aggregates the given value into a JSON array. + * Filtering rows that don't match the given filter predicate. + * Ordering values based on the given order by items. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAgg(Expression value, Predicate filter, JpaOrder... orderBy); + + /** + * Aggregates the given value into a JSON array, retaining {@code null} values in the JSON array. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAggWithNulls(Expression value); + + /** + * Aggregates the given value into a JSON array, retaining {@code null} values in the JSON array. + * Ordering values based on the given order by items. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAggWithNulls(Expression value, JpaOrder... orderBy); + + /** + * Aggregates the given value into a JSON array, retaining {@code null} values in the JSON array. + * Filtering rows that don't match the given filter predicate. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAggWithNulls(Expression value, Predicate filter); + + /** + * Aggregates the given value into a JSON array, retaining {@code null} values in the JSON array. + * Filtering rows that don't match the given filter predicate. + * Ordering values based on the given order by items. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonArrayAggWithNulls(Expression value, Predicate filter, JpaOrder... orderBy); + + /** + * Aggregates the given value under the given key into a JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAgg(Expression key, Expression value); + + /** + * Aggregates the given value under the given key into a JSON object, retaining {@code null} values in the JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAggWithNulls(Expression key, Expression value); + + /** + * Aggregates the given value under the given key into a JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value); + + /** + * Aggregates the given value under the given key into a JSON object, retaining {@code null} values in the JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value); + + /** + * Aggregates the given value under the given key into a JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAgg(Expression key, Expression value, Predicate filter); + + /** + * Aggregates the given value under the given key into a JSON object, retaining {@code null} values in the JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAggWithNulls(Expression key, Expression value, Predicate filter); + + /** + * Aggregates the given value under the given key into a JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value, Predicate filter); + + /** + * Aggregates the given value under the given key into a JSON object, retaining {@code null} values in the JSON object. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value, Predicate filter); @Override JpaPredicate and(List restrictions); 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 new file mode 100644 index 000000000000..cd91ffc10dab --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java @@ -0,0 +1,79 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Expression; + +/** + * A special expression for the {@code json_exists} function. + * @since 7.0 + */ +@Incubating +public interface JpaJsonExistsExpression extends JpaExpression { + /** + * Get the {@link ErrorBehavior} of this json value expression. + * + * @return the error behavior + */ + ErrorBehavior getErrorBehavior(); + + /** + * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json exists expression. + * + * @return {@code this} for method chaining + */ + JpaJsonExistsExpression unspecifiedOnError(); + /** + * Sets the {@link ErrorBehavior#ERROR} for this json exists expression. + * + * @return {@code this} for method chaining + */ + JpaJsonExistsExpression errorOnError(); + /** + * Sets the {@link ErrorBehavior#TRUE} for this json exists expression. + * + * @return {@code this} for method chaining + */ + JpaJsonExistsExpression trueOnError(); + /** + * Sets the {@link ErrorBehavior#FALSE} for this json exists expression. + * + * @return {@code this} for method chaining + */ + 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/JpaJsonQueryExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java new file mode 100644 index 000000000000..399fcdbbd4a5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java @@ -0,0 +1,204 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.criteria; + +import org.hibernate.Incubating; + +import jakarta.persistence.criteria.Expression; + +/** + * A special expression for the {@code json_query} function. + * @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(); + + /** + * Sets the {@link WrapMode#WITHOUT_WRAPPER} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression withoutWrapper(); + /** + * Sets the {@link WrapMode#WITH_WRAPPER} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression withWrapper(); + /** + * Sets the {@link WrapMode#WITH_CONDITIONAL_WRAPPER} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression withConditionalWrapper(); + /** + * Sets the {@link WrapMode#UNSPECIFIED} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression unspecifiedWrapper(); + + /** + * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression unspecifiedOnError(); + /** + * Sets the {@link ErrorBehavior#ERROR} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression errorOnError(); + /** + * Sets the {@link ErrorBehavior#NULL} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression nullOnError(); + /** + * Sets the {@link ErrorBehavior#EMPTY_ARRAY} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression emptyArrayOnError(); + /** + * Sets the {@link ErrorBehavior#EMPTY_OBJECT} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression emptyObjectOnError(); + + /** + * Sets the {@link EmptyBehavior#UNSPECIFIED} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression unspecifiedOnEmpty(); + /** + * Sets the {@link EmptyBehavior#ERROR} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression errorOnEmpty(); + /** + * Sets the {@link EmptyBehavior#NULL} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression nullOnEmpty(); + /** + * Sets the {@link EmptyBehavior#EMPTY_ARRAY} for this json query expression. + * + * @return {@code this} for method chaining + */ + JpaJsonQueryExpression emptyArrayOnEmpty(); + /** + * Sets the {@link EmptyBehavior#EMPTY_OBJECT} for this json query expression. + * + * @return {@code this} for method chaining + */ + 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/JpaJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java new file mode 100644 index 000000000000..87e3991621ab --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java @@ -0,0 +1,149 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.criteria; + +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(); + + /** + * Sets the {@link ErrorBehavior#UNSPECIFIED} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueExpression unspecifiedOnError(); + /** + * Sets the {@link ErrorBehavior#ERROR} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueExpression errorOnError(); + /** + * Sets the {@link ErrorBehavior#NULL} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueExpression nullOnError(); + /** + * Sets the {@link ErrorBehavior#DEFAULT} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueExpression defaultOnError(Expression expression); + + /** + * Sets the {@link EmptyBehavior#UNSPECIFIED} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueExpression unspecifiedOnEmpty(); + /** + * Sets the {@link EmptyBehavior#ERROR} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueExpression errorOnEmpty(); + /** + * Sets the {@link EmptyBehavior#NULL} for this json value expression. + * + * @return {@code this} for method chaining + */ + JpaJsonValueExpression nullOnEmpty(); + /** + * Sets the {@link EmptyBehavior#DEFAULT} for this json value expression. + * + * @return {@code this} for method chaining + */ + 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/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index e9353ece6ee1..598944c7c696 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 @@ -40,6 +40,9 @@ 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; @@ -3345,4 +3348,181 @@ public JpaPredicate collectionIntersectsNullable( Expression> collectionExpression2) { return criteriaBuilder.collectionIntersectsNullable( collection1, collectionExpression2 ); } + + @Override + @Incubating + public JpaJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath) { + return criteriaBuilder.jsonValue( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaJsonValueExpression jsonValue( + Expression jsonDocument, + String jsonPath, + Class returningType) { + return criteriaBuilder.jsonValue( jsonDocument, jsonPath, returningType ); + } + + @Override + @Incubating + public JpaJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath) { + return criteriaBuilder.jsonValue( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaJsonValueExpression jsonValue( + Expression jsonDocument, + Expression jsonPath, + Class returningType) { + return criteriaBuilder.jsonValue( jsonDocument, jsonPath, returningType ); + } + + @Override + @Incubating + public JpaJsonQueryExpression jsonQuery(Expression jsonDocument, String jsonPath) { + return criteriaBuilder.jsonQuery( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaJsonQueryExpression jsonQuery(Expression jsonDocument, Expression jsonPath) { + return criteriaBuilder.jsonQuery( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaJsonExistsExpression jsonExists(Expression jsonDocument, String jsonPath) { + return criteriaBuilder.jsonExists( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaJsonExistsExpression jsonExists(Expression jsonDocument, Expression jsonPath) { + return criteriaBuilder.jsonExists( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaExpression jsonObject(Map> keyValues) { + return criteriaBuilder.jsonObject( keyValues ); + } + + @Override + @Incubating + public JpaExpression jsonObjectWithNulls(Map> keyValues) { + return criteriaBuilder.jsonObjectWithNulls( keyValues ); + } + + @Override + @Incubating + public JpaExpression jsonArray(Expression... values) { + return criteriaBuilder.jsonArray( values ); + } + + @Override + @Incubating + public JpaExpression jsonArrayWithNulls(Expression... values) { + return criteriaBuilder.jsonArrayWithNulls( values ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAgg(Expression value) { + return criteriaBuilder.jsonArrayAgg( value ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAggWithNulls(Expression value) { + return criteriaBuilder.jsonArrayAggWithNulls( value ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAgg(Expression value, JpaOrder... orderBy) { + return criteriaBuilder.jsonArrayAgg( value, orderBy ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAgg(Expression value, Predicate filter) { + return criteriaBuilder.jsonArrayAgg( value, filter ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAgg(Expression value, Predicate filter, JpaOrder... orderBy) { + return criteriaBuilder.jsonArrayAgg( value, filter, orderBy ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAggWithNulls(Expression value, JpaOrder... orderBy) { + return criteriaBuilder.jsonArrayAggWithNulls( value, orderBy ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAggWithNulls(Expression value, Predicate filter) { + return criteriaBuilder.jsonArrayAggWithNulls( value, filter ); + } + + @Override + @Incubating + public JpaExpression jsonArrayAggWithNulls(Expression value, Predicate filter, JpaOrder... orderBy) { + return criteriaBuilder.jsonArrayAggWithNulls( value, filter, orderBy ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAgg(Expression key, Expression value) { + return criteriaBuilder.jsonObjectAgg( key, value ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAggWithNulls(Expression key, Expression value) { + return criteriaBuilder.jsonObjectAggWithNulls( key, value ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value) { + return criteriaBuilder.jsonObjectAggWithUniqueKeys( key, value ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value) { + return criteriaBuilder.jsonObjectAggWithUniqueKeysAndNulls( key, value ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAgg(Expression key, Expression value, Predicate filter) { + return criteriaBuilder.jsonObjectAgg( key, value, filter ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAggWithNulls(Expression key, Expression value, Predicate filter) { + return criteriaBuilder.jsonObjectAggWithNulls( key, value, filter ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value, Predicate filter) { + return criteriaBuilder.jsonObjectAggWithUniqueKeys( key, value, filter ); + } + + @Override + @Incubating + public JpaExpression jsonObjectAggWithUniqueKeysAndNulls( + Expression key, + Expression value, + Predicate filter) { + return criteriaBuilder.jsonObjectAggWithUniqueKeysAndNulls( key, value, filter ); + } } 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 478002be0542..fea582f027e3 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 @@ -24,6 +24,7 @@ import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; @@ -33,6 +34,7 @@ import java.util.Set; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; +import org.hibernate.cfg.QuerySettings; import org.hibernate.dialect.function.SqlColumn; import org.hibernate.grammars.hql.HqlLexer; import org.hibernate.grammars.hql.HqlParser; @@ -145,6 +147,11 @@ 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; @@ -2693,6 +2700,290 @@ public SqmPredicate visitContainsPredicate(HqlParser.ContainsPredicateContext ct return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); } + @Override + public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final SqmExpression jsonDocument = (SqmExpression) ctx.expression( 0 ).accept( this ); + final SqmExpression jsonPath = (SqmExpression) ctx.expression( 1 ).accept( this ); + final HqlParser.JsonValueReturningClauseContext returningClause = ctx.jsonValueReturningClause(); + final SqmCastTarget castTarget = returningClause == null + ? null + : (SqmCastTarget) returningClause.castTarget().accept( this ); + + final SqmJsonValueExpression jsonValue = (SqmJsonValueExpression) getFunctionDescriptor( "json_value" ).generateSqmExpression( + castTarget == null + ? asList( jsonDocument, jsonPath ) + : asList( jsonDocument, jsonPath, castTarget ), + null, + creationContext.getQueryEngine() + ); + for ( HqlParser.JsonValueOnErrorOrEmptyClauseContext subCtx : ctx.jsonValueOnErrorOrEmptyClause() ) { + final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 ); + final TerminalNode lastToken = (TerminalNode) subCtx.getChild( subCtx.getChildCount() - 1 ); + if ( lastToken.getSymbol().getType() == HqlParser.ERROR ) { + switch ( firstToken.getSymbol().getType() ) { + case HqlParser.NULL -> jsonValue.nullOnError(); + case HqlParser.ERROR -> jsonValue.errorOnError(); + case HqlParser.DEFAULT -> + jsonValue.defaultOnError( (SqmExpression) subCtx.expression().accept( this ) ); + } + } + else { + switch ( firstToken.getSymbol().getType() ) { + case HqlParser.NULL -> jsonValue.nullOnEmpty(); + case HqlParser.ERROR -> jsonValue.errorOnEmpty(); + case HqlParser.DEFAULT -> + jsonValue.defaultOnEmpty( (SqmExpression) subCtx.expression().accept( this ) ); + } + } + } + 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 + public SqmExpression visitJsonQueryFunction(HqlParser.JsonQueryFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final SqmExpression jsonDocument = (SqmExpression) ctx.expression( 0 ).accept( this ); + final SqmExpression jsonPath = (SqmExpression) ctx.expression( 1 ).accept( this ); + final SqmJsonQueryExpression jsonQuery = (SqmJsonQueryExpression) getFunctionDescriptor( "json_query" ).generateSqmExpression( + asList( jsonDocument, jsonPath ), + null, + creationContext.getQueryEngine() + ); + final HqlParser.JsonQueryWrapperClauseContext wrapperClause = ctx.jsonQueryWrapperClause(); + if ( wrapperClause != null ) { + final TerminalNode firstToken = (TerminalNode) wrapperClause.getChild( 0 ); + if ( firstToken.getSymbol().getType() == HqlParser.WITH ) { + final TerminalNode secondToken = (TerminalNode) wrapperClause.getChild( 1 ); + if ( wrapperClause.getChildCount() > 2 && secondToken.getSymbol().getType() == HqlParser.CONDITIONAL ) { + jsonQuery.withConditionalWrapper(); + } + else { + jsonQuery.withWrapper(); + } + } + else { + jsonQuery.withoutWrapper(); + } + } + for ( HqlParser.JsonQueryOnErrorOrEmptyClauseContext subCtx : ctx.jsonQueryOnErrorOrEmptyClause() ) { + final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 ); + final TerminalNode lastToken = (TerminalNode) subCtx.getChild( subCtx.getChildCount() - 1 ); + if ( lastToken.getSymbol().getType() == HqlParser.ERROR ) { + switch ( firstToken.getSymbol().getType() ) { + case HqlParser.NULL -> jsonQuery.nullOnError(); + case HqlParser.ERROR -> jsonQuery.errorOnError(); + case HqlParser.EMPTY -> { + final TerminalNode secondToken = (TerminalNode) subCtx.getChild( 1 ); + if ( secondToken.getSymbol().getType() == HqlParser.OBJECT ) { + jsonQuery.emptyObjectOnError(); + } + else { + jsonQuery.emptyArrayOnError(); + } + } + } + } + else { + switch ( firstToken.getSymbol().getType() ) { + case HqlParser.NULL -> jsonQuery.nullOnEmpty(); + case HqlParser.ERROR -> jsonQuery.errorOnEmpty(); + case HqlParser.EMPTY -> { + final TerminalNode secondToken = (TerminalNode) subCtx.getChild( 1 ); + if ( secondToken.getSymbol().getType() == HqlParser.OBJECT ) { + jsonQuery.emptyObjectOnEmpty(); + } + else { + jsonQuery.emptyArrayOnEmpty(); + } + } + } + } + } + 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 + public SqmExpression visitJsonExistsFunction(HqlParser.JsonExistsFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final SqmExpression jsonDocument = (SqmExpression) ctx.expression( 0 ).accept( this ); + final SqmExpression jsonPath = (SqmExpression) ctx.expression( 1 ).accept( this ); + + final SqmJsonExistsExpression jsonExists = (SqmJsonExistsExpression) getFunctionDescriptor( "json_exists" ).generateSqmExpression( + asList( jsonDocument, jsonPath ), + null, + creationContext.getQueryEngine() + ); + final HqlParser.JsonExistsOnErrorClauseContext subCtx = ctx.jsonExistsOnErrorClause(); + if ( subCtx != null ) { + final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 ); + switch ( firstToken.getSymbol().getType() ) { + case HqlParser.ERROR -> jsonExists.errorOnError(); + case HqlParser.TRUE -> jsonExists.trueOnError(); + case HqlParser.FALSE -> jsonExists.falseOnError(); + } + } + 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++ ) { + jsonExists.passing( + visitIdentifier( identifierContexts.get( i ) ), + (SqmExpression) expressionContexts.get( i ).accept( this ) + ); + } + } + return jsonExists; + } + + @Override + public SqmExpression visitJsonArrayFunction(HqlParser.JsonArrayFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final HqlParser.JsonNullClauseContext subCtx = ctx.jsonNullClause(); + final List argumentContexts = ctx.expressionOrPredicate(); + int count = argumentContexts.size(); + final List> arguments = new ArrayList<>( count + (subCtx == null ? 0 : 1 ) ); + for ( int i = 0; i < count; i++ ) { + arguments.add( (SqmTypedNode) argumentContexts.get(i).accept( this ) ); + } + if ( subCtx != null ) { + final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 ); + arguments.add( + firstToken.getSymbol().getType() == HqlParser.ABSENT + ? SqmJsonNullBehavior.ABSENT + : SqmJsonNullBehavior.NULL + ); + } + return getFunctionDescriptor( "json_array" ).generateSqmExpression( + arguments, + null, + creationContext.getQueryEngine() + ); + } + + @Override + public SqmExpression visitJsonObjectFunction(HqlParser.JsonObjectFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final HqlParser.JsonObjectFunctionEntriesContext entries = ctx.jsonObjectFunctionEntries(); + final List> arguments; + if ( entries == null ) { + arguments = Collections.emptyList(); + } + else { + final HqlParser.JsonNullClauseContext subCtx = ctx.jsonNullClause(); + final List argumentContexts = entries.expressionOrPredicate(); + int count = argumentContexts.size(); + arguments = new ArrayList<>( count + ( subCtx == null ? 0 : 1 ) ); + for ( int i = 0; i < count; i++ ) { + arguments.add( (SqmTypedNode) argumentContexts.get( i ).accept( this ) ); + } + if ( subCtx != null ) { + final TerminalNode firstToken = (TerminalNode) subCtx.getChild( 0 ); + arguments.add( + firstToken.getSymbol().getType() == HqlParser.ABSENT + ? SqmJsonNullBehavior.ABSENT + : SqmJsonNullBehavior.NULL + ); + } + } + return getFunctionDescriptor( "json_object" ).generateSqmExpression( + arguments, + null, + creationContext.getQueryEngine() + ); + } + + @Override + public Object visitJsonArrayAggFunction(HqlParser.JsonArrayAggFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final HqlParser.JsonNullClauseContext jsonNullClauseContext = ctx.jsonNullClause(); + final ArrayList> arguments = new ArrayList<>( jsonNullClauseContext == null ? 1 : 2 ); + arguments.add( (SqmTypedNode) ctx.expressionOrPredicate().accept( this ) ); + if ( jsonNullClauseContext != null ) { + final TerminalNode firstToken = (TerminalNode) jsonNullClauseContext.getChild( 0 ); + arguments.add( + firstToken.getSymbol().getType() == HqlParser.ABSENT + ? SqmJsonNullBehavior.ABSENT + : SqmJsonNullBehavior.NULL + ); + } + return getFunctionDescriptor( "json_arrayagg" ).generateOrderedSetAggregateSqmExpression( + arguments, + getFilterExpression( ctx ), + ctx.orderByClause() == null + ? null + : visitOrderByClause( ctx.orderByClause(), false ), + null, + creationContext.getQueryEngine() + ); + } + + @Override + public Object visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); + final HqlParser.JsonNullClauseContext jsonNullClauseContext = ctx.jsonNullClause(); + final HqlParser.JsonUniqueKeysClauseContext jsonUniqueKeysClauseContext = ctx.jsonUniqueKeysClause(); + final ArrayList> arguments = new ArrayList<>( 4 ); + for ( HqlParser.ExpressionOrPredicateContext subCtx : ctx.expressionOrPredicate() ) { + arguments.add( (SqmTypedNode) subCtx.accept( this ) ); + } + if ( jsonNullClauseContext != null ) { + final TerminalNode firstToken = (TerminalNode) jsonNullClauseContext.getChild( 0 ); + arguments.add( + firstToken.getSymbol().getType() == HqlParser.ABSENT + ? SqmJsonNullBehavior.ABSENT + : SqmJsonNullBehavior.NULL + ); + } + if ( jsonUniqueKeysClauseContext != null ) { + final TerminalNode firstToken = (TerminalNode) jsonUniqueKeysClauseContext.getChild( 0 ); + arguments.add( + firstToken.getSymbol().getType() == HqlParser.WITH + ? SqmJsonObjectAggUniqueKeysBehavior.WITH + : SqmJsonObjectAggUniqueKeysBehavior.WITHOUT + ); + } + return getFunctionDescriptor( "json_objectagg" ).generateAggregateSqmExpression( + arguments, + getFilterExpression( ctx ), + null, + creationContext.getQueryEngine() + ); + } + + private void checkJsonFunctionsEnabled(ParserRuleContext ctx) { + if ( !creationOptions.isJsonFunctionsEnabled() ) { + throw new SemanticException( + "Can't use function '" + ctx.children.get( 0 ).getText() + + "', because tech preview JSON functions are not enabled. To enable, set the '" + QuerySettings.JSON_FUNCTIONS_ENABLED + "' setting to 'true'.", + query + ); + } + } + @Override public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { final boolean negated = ctx.NOT() != null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java index 9f8b64dc692a..7b5469453915 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/spi/SqmCreationOptions.java @@ -26,6 +26,13 @@ default boolean useStrictJpaCompliance() { return false; } + /** + * @see org.hibernate.cfg.AvailableSettings#JSON_FUNCTIONS_ENABLED + */ + default boolean isJsonFunctionsEnabled() { + return false; + } + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java deleted file mode 100644 index d52e8ad0f0f4..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.query.internal; - -/** - * @author Christian Beikov - */ -public class QueryLiteralHelper { - private QueryLiteralHelper() { - // disallow direct instantiation - } - - public static String toStringLiteral(String value) { - final StringBuilder sb = new StringBuilder( value.length() + 2 ); - appendStringLiteral( sb, value ); - return sb.toString(); - } - - public static void appendStringLiteral(StringBuilder sb, String value) { - sb.append( '\'' ); - for ( int i = 0; i < value.length(); i++ ) { - final char c = value.charAt( i ); - if ( c == '\'' ) { - sb.append( '\'' ); - } - sb.append( c ); - } - sb.append( '\'' ); - } - -} diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java index bbfbf4ea6e8c..13a30af45cec 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java @@ -78,6 +78,11 @@ default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHand return ImmutableEntityUpdateQueryHandlingMode.WARNING; } + /** + * @see org.hibernate.cfg.AvailableSettings#JSON_FUNCTIONS_ENABLED + */ + boolean isJsonFunctionsEnabled(); + /** * @see org.hibernate.cfg.AvailableSettings#PORTABLE_INTEGER_DIVISION */ 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 9f200c8548fd..cc597072de9c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -46,6 +46,9 @@ import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; 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.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmTuple; import org.hibernate.query.sqm.tree.from.SqmRoot; @@ -615,6 +618,93 @@ default SqmPredicate collectionOverlapsNullable(Collection collection1, E @Override SqmPredicate collectionIntersectsNullable(Collection collection1, Expression> collectionExpression2); + @Override + SqmJsonValueExpression jsonValue( + Expression jsonDocument, + Expression jsonPath, + Class returningType); + + @Override + SqmJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath); + + @Override + SqmJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath, Class returningType); + + @Override + SqmJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath); + + @Override + SqmJsonQueryExpression jsonQuery(Expression jsonDocument, Expression jsonPath); + + @Override + SqmJsonQueryExpression jsonQuery(Expression jsonDocument, String jsonPath); + + @Override + SqmJsonExistsExpression jsonExists(Expression jsonDocument, Expression jsonPath); + + @Override + SqmJsonExistsExpression jsonExists(Expression jsonDocument, String jsonPath); + + @Override + SqmExpression jsonArrayWithNulls(Expression... values); + + @Override + SqmExpression jsonArray(Expression... values); + + @Override + SqmExpression jsonObjectWithNulls(Map> keyValues); + + @Override + SqmExpression jsonObject(Map> keyValues); + + @Override + SqmExpression jsonArrayAgg(Expression value); + + @Override + SqmExpression jsonArrayAggWithNulls(Expression value); + + @Override + SqmExpression jsonArrayAggWithNulls(Expression value, Predicate filter, JpaOrder... orderBy); + + @Override + SqmExpression jsonArrayAggWithNulls(Expression value, Predicate filter); + + @Override + SqmExpression jsonArrayAggWithNulls(Expression value, JpaOrder... orderBy); + + @Override + SqmExpression jsonArrayAgg(Expression value, Predicate filter, JpaOrder... orderBy); + + @Override + SqmExpression jsonArrayAgg(Expression value, Predicate filter); + + @Override + SqmExpression jsonArrayAgg(Expression value, JpaOrder... orderBy); + + @Override + SqmExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value); + + @Override + SqmExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value); + + @Override + SqmExpression jsonObjectAggWithNulls(Expression key, Expression value); + + @Override + SqmExpression jsonObjectAgg(Expression key, Expression value); + + @Override + SqmExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value, Predicate filter); + + @Override + SqmExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value, Predicate filter); + + @Override + SqmExpression jsonObjectAggWithNulls(Expression key, Expression value, Predicate filter); + + @Override + SqmExpression jsonObjectAgg(Expression key, Expression value, Predicate filter); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java index 30a671ce7f08..3a6b28f03862 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SqmFunctionDescriptor.java @@ -222,4 +222,13 @@ default FunctionKind getFunctionKind() { * @return an instance of {@link ArgumentsValidator} */ ArgumentsValidator getArgumentsValidator(); + + /** + * Whether the function renders as a predicate. + * + * @since 7.0 + */ + default boolean isPredicate() { + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java index fb3969228e12..e47513f3bfb5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java @@ -24,6 +24,11 @@ public boolean useStrictJpaCompliance() { return queryEngineOptions.getJpaCompliance().isJpaQueryComplianceEnabled(); } + @Override + public boolean isJsonFunctionsEnabled() { + return queryEngineOptions.isJsonFunctionsEnabled(); + } + @Override public boolean isPortableIntegerDivisionEnabled() { return queryEngineOptions.isPortableIntegerDivisionEnabled(); 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 6339000cb999..f32fef2f5a6f 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 @@ -123,6 +123,11 @@ 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; @@ -189,6 +194,7 @@ import jakarta.persistence.criteria.Subquery; import jakarta.persistence.criteria.TemporalField; import jakarta.persistence.metamodel.Bindable; +import org.checkerframework.checker.nullness.qual.Nullable; import static java.util.Arrays.asList; import static org.hibernate.query.internal.QueryHelper.highestPrecedenceType; @@ -5292,4 +5298,264 @@ public SqmExpression collectionToString( queryEngine ); } + + @Override + public SqmJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath) { + return jsonValue( jsonDocument, value( jsonPath ), null ); + } + + @Override + public SqmJsonValueExpression jsonValue( + Expression jsonDocument, + String jsonPath, + Class returningType) { + return jsonValue( jsonDocument, value( jsonPath ), returningType ); + } + + @Override + public SqmJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath) { + return jsonValue( jsonDocument, jsonPath, null ); + } + + @Override + @SuppressWarnings("unchecked") + public SqmJsonValueExpression jsonValue( + Expression jsonDocument, + Expression jsonPath, + Class returningType) { + if ( returningType == null ) { + return (SqmJsonValueExpression) getFunctionDescriptor( "json_value" ).generateSqmExpression( + asList( (SqmTypedNode) jsonDocument, (SqmTypedNode) jsonPath ), + null, + queryEngine + ); + } + else { + final BasicType type = getTypeConfiguration().standardBasicTypeForJavaType( returningType ); + return (SqmJsonValueExpression) getFunctionDescriptor( "json_value" ).generateSqmExpression( + asList( (SqmTypedNode) jsonDocument, (SqmTypedNode) jsonPath, new SqmCastTarget<>( type, this ) ), + type, + queryEngine + ); + } + } + + @Override + public SqmJsonQueryExpression jsonQuery(Expression jsonDocument, String jsonPath) { + return jsonQuery( jsonDocument, value( jsonPath ) ); + } + + @Override + public SqmJsonQueryExpression jsonQuery(Expression jsonDocument, Expression jsonPath) { + return (SqmJsonQueryExpression) getFunctionDescriptor( "json_query" ).generateSqmExpression( + asList( (SqmTypedNode) jsonDocument, (SqmTypedNode) jsonPath ), + null, + queryEngine + ); + } + + @Override + public SqmJsonExistsExpression jsonExists(Expression jsonDocument, String jsonPath) { + return jsonExists( jsonDocument, value( jsonPath ) ); + } + + @Override + public SqmJsonExistsExpression jsonExists(Expression jsonDocument, Expression jsonPath) { + return (SqmJsonExistsExpression) getFunctionDescriptor( "json_exists" ).generateSqmExpression( + asList( (SqmTypedNode) jsonDocument, (SqmTypedNode) jsonPath ), + null, + queryEngine + ); + } + + @Override + public SqmExpression jsonArrayWithNulls(Expression... values) { + final var arguments = new ArrayList>( values.length + 1 ); + for ( Expression expression : values ) { + arguments.add( (SqmTypedNode) expression ); + } + arguments.add( SqmJsonNullBehavior.NULL ); + return getFunctionDescriptor( "json_array" ).generateSqmExpression( + arguments, + null, + queryEngine + ); + } + + @Override + public SqmExpression jsonArray(Expression... values) { + //noinspection unchecked + return getFunctionDescriptor( "json_array" ).generateSqmExpression( + (List>) (List) asList( values ), + null, + queryEngine + ); + } + + @Override + public SqmExpression jsonArrayAgg(Expression value) { + return jsonArrayAgg( (SqmExpression) value, null, null, null ); + } + + @Override + public SqmExpression jsonArrayAgg(Expression value, Predicate filter, JpaOrder... orderBy) { + return jsonArrayAgg( (SqmExpression) value, null, (SqmPredicate) filter, orderByClause( orderBy ) ); + } + + @Override + public SqmExpression jsonArrayAgg(Expression value, Predicate filter) { + return jsonArrayAgg( (SqmExpression) value, null, (SqmPredicate) filter, null ); + } + + @Override + public SqmExpression jsonArrayAgg(Expression value, JpaOrder... orderBy) { + return jsonArrayAgg( (SqmExpression) value, null, null, orderByClause( orderBy ) ); + } + + @Override + public SqmExpression jsonArrayAggWithNulls(Expression value) { + return jsonArrayAgg( (SqmExpression) value, SqmJsonNullBehavior.NULL, null, null ); + } + + @Override + public SqmExpression jsonArrayAggWithNulls(Expression value, Predicate filter, JpaOrder... orderBy) { + return jsonArrayAgg( + (SqmExpression) value, + SqmJsonNullBehavior.NULL, + (SqmPredicate) filter, + orderByClause( orderBy ) + ); + } + + @Override + public SqmExpression jsonArrayAggWithNulls(Expression value, Predicate filter) { + return jsonArrayAgg( (SqmExpression) value, SqmJsonNullBehavior.NULL, (SqmPredicate) filter, null ); + } + + @Override + public SqmExpression jsonArrayAggWithNulls(Expression value, JpaOrder... orderBy) { + return jsonArrayAgg( (SqmExpression) value, SqmJsonNullBehavior.NULL, null, orderByClause( orderBy ) ); + } + + @Override + public SqmExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value) { + return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, SqmJsonObjectAggUniqueKeysBehavior.WITH, null ); + } + + @Override + public SqmExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value) { + return jsonObjectAgg( key, value, null, SqmJsonObjectAggUniqueKeysBehavior.WITH, null ); + } + + @Override + public SqmExpression jsonObjectAggWithNulls(Expression key, Expression value) { + return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, null, null ); + } + + @Override + public SqmExpression jsonObjectAgg(Expression key, Expression value) { + return jsonObjectAgg( key, value, null, null, null ); + } + + @Override + public SqmExpression jsonObjectAggWithUniqueKeysAndNulls( + Expression key, + Expression value, + Predicate filter) { + return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, SqmJsonObjectAggUniqueKeysBehavior.WITH, filter ); + } + + @Override + public SqmExpression jsonObjectAggWithUniqueKeys(Expression key, Expression value, Predicate filter) { + return jsonObjectAgg( key, value, null, SqmJsonObjectAggUniqueKeysBehavior.WITH, filter ); + } + + @Override + public SqmExpression jsonObjectAggWithNulls(Expression key, Expression value, Predicate filter) { + return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, null, filter ); + } + + @Override + public SqmExpression jsonObjectAgg(Expression key, Expression value, Predicate filter) { + return jsonObjectAgg( key, value, null, null, filter ); + } + + private SqmExpression jsonObjectAgg( + Expression key, + Expression value, + @Nullable SqmJsonNullBehavior nullBehavior, + @Nullable SqmJsonObjectAggUniqueKeysBehavior uniqueKeysBehavior, + @Nullable Predicate filterPredicate) { + final ArrayList> arguments = new ArrayList<>( 4 ); + arguments.add( (SqmTypedNode) key ); + arguments.add( (SqmTypedNode) value ); + if ( nullBehavior != null ) { + arguments.add( nullBehavior ); + } + if ( uniqueKeysBehavior != null ) { + arguments.add( uniqueKeysBehavior ); + } + return getFunctionDescriptor( "json_objectagg" ).generateAggregateSqmExpression( + arguments, + (SqmPredicate) filterPredicate, + null, + queryEngine + ); + } + + private @Nullable SqmOrderByClause orderByClause(JpaOrder[] orderBy) { + if ( orderBy.length == 0 ) { + return null; + } + final SqmOrderByClause sqmOrderByClause = new SqmOrderByClause( orderBy.length ); + for ( JpaOrder jpaOrder : orderBy ) { + sqmOrderByClause.addSortSpecification( (SqmSortSpecification) jpaOrder ); + } + return sqmOrderByClause; + } + + private SqmExpression jsonArrayAgg( + SqmExpression value, + @Nullable SqmJsonNullBehavior nullBehavior, + @Nullable SqmPredicate filterPredicate, + @Nullable SqmOrderByClause orderByClause) { + return getFunctionDescriptor( "json_arrayagg" ).generateOrderedSetAggregateSqmExpression( + nullBehavior == null + ? Collections.singletonList( value ) + : Arrays.asList( value, SqmJsonNullBehavior.NULL ), + filterPredicate, + orderByClause, + null, + queryEngine + ); + } + + @Override + public SqmExpression jsonObjectWithNulls(Map> keyValues) { + final var arguments = keyValuesAsAlternatingList( keyValues ); + arguments.add( SqmJsonNullBehavior.NULL ); + return getFunctionDescriptor( "json_object" ).generateSqmExpression( + arguments, + null, + queryEngine + ); + } + + @Override + public SqmExpression jsonObject(Map> keyValues) { + return getFunctionDescriptor( "json_object" ).generateSqmExpression( + keyValuesAsAlternatingList( keyValues ), + null, + queryEngine + ); + } + + private ArrayList> keyValuesAsAlternatingList(Map> keyValues) { + final var list = new ArrayList>( keyValues.size() ); + for ( Map.Entry> entry : keyValues.entrySet() ) { + list.add( value( entry.getKey() ) ); + list.add( (SqmTypedNode) entry.getValue() ); + } + return list; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java index 73a29eee7ca5..53468401409c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/ArgumentTypesValidator.java @@ -195,7 +195,7 @@ public void validateSqlTypes(List arguments, String functi /** * We can't validate some expressions involving parameters / unknown functions. */ - private static boolean isUnknownExpressionType(JdbcMappingContainer expressionType) { + public static boolean isUnknownExpressionType(JdbcMappingContainer expressionType) { return expressionType instanceof JavaObjectType || expressionType instanceof BasicType && isUnknown( ((BasicType) expressionType).getJavaTypeDescriptor() ); @@ -219,7 +219,8 @@ private int validateArgument(int paramNumber, JdbcMappingContainer expressionTyp return paramNumber; } - private static void checkArgumentType( + @Internal + public static void checkArgumentType( int paramNumber, String functionName, FunctionParameterType type, JdbcType jdbcType, Type javaType) { if ( !isCompatible( type, jdbcType ) // as a special case, we consider a binary column @@ -250,6 +251,8 @@ private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcTyp case DATE -> jdbcType.hasDatePart(); case TIME -> jdbcType.hasTimePart(); case SPATIAL -> jdbcType.isSpatial(); + case JSON -> jdbcType.isJson(); + case IMPLICIT_JSON -> jdbcType.isImplicitJson(); default -> true; // TODO: should we throw here? }; } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java index 2564f6626448..ff4b15d21ab1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionParameterType.java @@ -88,6 +88,18 @@ public enum FunctionParameterType { * @see org.hibernate.type.SqlTypes#isSpatialType(int) */ SPATIAL, + /** + * Indicates that the argument should be a JSON type + * @see org.hibernate.type.SqlTypes#isJsonType(int) + * @since 7.0 + */ + JSON, + /** + * Indicates that the argument should be a JSON or String type + * @see org.hibernate.type.SqlTypes#isImplicitJsonType(int) + * @since 7.0 + */ + IMPLICIT_JSON, /** * Indicates a parameter that accepts any type, except untyped expressions like {@code null} literals */ diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index 950935888ffc..23a46948927f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -291,6 +291,7 @@ import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlTreeCreationException; import org.hibernate.sql.ast.SqlTreeCreationLogger; import org.hibernate.sql.ast.internal.TableGroupJoinHelper; @@ -299,6 +300,7 @@ import org.hibernate.sql.ast.spi.SqlAliasBaseConstant; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; import org.hibernate.sql.ast.spi.SqlAliasBaseManager; +import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.spi.SqlAstCreationContext; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.spi.SqlAstProcessingState; @@ -6442,7 +6444,22 @@ public Expression visitFunction(SqmFunction sqmFunction) { functionImpliedResultTypeAccess = inferrableTypeAccessStack.getCurrent(); inferrableTypeAccessStack.push( () -> null ); try { - return sqmFunction.convertToSqlAst( this ); + final Expression expression = sqmFunction.convertToSqlAst( this ); + if ( sqmFunction.getFunctionDescriptor().isPredicate() + && expression instanceof SelfRenderingExpression selfRenderingExpression) { + final BasicType booleanType = getBooleanType(); + return new CaseSearchedExpression( + booleanType, + List.of( + new CaseSearchedExpression.WhenFragment( + new SelfRenderingPredicate( selfRenderingExpression ), + new QueryLiteral<>( true, booleanType ) + ) + ), + new QueryLiteral<>( false, booleanType ) + ); + } + return expression; } finally { inferrableTypeAccessStack.pop(); @@ -8221,14 +8238,27 @@ public InSubQueryPredicate visitInSubQueryPredicate(SqmInSubQueryPredicate pr ); } - private JdbcMappingContainer getBooleanType() { + private BasicType getBooleanType() { return getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ); } @Override public Object visitBooleanExpressionPredicate(SqmBooleanExpressionPredicate predicate) { inferrableTypeAccessStack.push( this::getBooleanType ); - final Expression booleanExpression = (Expression) predicate.getBooleanExpression().accept( this ); + final SqmExpression sqmExpression = predicate.getBooleanExpression(); + final Expression booleanExpression = (Expression) sqmExpression.accept( this ); + if ( booleanExpression instanceof CaseSearchedExpression caseExpr + && sqmExpression instanceof SqmFunction sqmFunction + && sqmFunction.getFunctionDescriptor().isPredicate() ) { + // Functions that are rendered as predicates are always wrapped, + // so the following unwraps the predicate and returns it directly instead of wrapping once more + final Predicate sqlPredicate = caseExpr.getWhenFragments().get( 0 ).getPredicate(); + if ( predicate.isNegated() ) { + return new NegatedPredicate( sqlPredicate ); + } + return sqlPredicate; + } + inferrableTypeAccessStack.pop(); if ( booleanExpression instanceof SelfRenderingExpression ) { final Predicate sqlPredicate = new SelfRenderingPredicate( (SelfRenderingExpression) booleanExpression ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java new file mode 100644 index 000000000000..72af4fa2a33f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java @@ -0,0 +1,128 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.internal.util.QuotingHelper; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingSqmFunction; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +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.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Base class for expressions that contain a json path. Maintains a map of expressions for identifiers. + * + * @since 7.0 + */ +@Incubating +public abstract class AbstractSqmJsonPathExpression extends SelfRenderingSqmFunction { + + private @Nullable Map> passingExpressions; + + public AbstractSqmJsonPathExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + } + + protected AbstractSqmJsonPathExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name, + @Nullable Map> passingExpressions) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + this.passingExpressions = passingExpressions; + } + + public Map> getPassingExpressions() { + return passingExpressions == null ? Collections.emptyMap() : Collections.unmodifiableMap( passingExpressions ); + } + + protected void addPassingExpression(String identifier, SqmExpression expression) { + if ( passingExpressions == null ) { + passingExpressions = new HashMap<>(); + } + passingExpressions.put( identifier, expression ); + } + + protected Map> copyPassingExpressions(SqmCopyContext context) { + if ( passingExpressions == null ) { + return null; + } + final HashMap> copy = new HashMap<>( passingExpressions.size() ); + for ( Map.Entry> entry : passingExpressions.entrySet() ) { + copy.put( entry.getKey(), entry.getValue().copy( context ) ); + } + return copy; + } + + 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(), (Expression) entry.getValue().accept( walker ) ); + } + return new JsonPathPassingClause( converted ); + } + + protected void appendPassingExpressionHqlString(StringBuilder sb) { + if ( passingExpressions != null && !passingExpressions.isEmpty() ) { + sb.append( " passing " ); + for ( Map.Entry> entry : passingExpressions.entrySet() ) { + entry.getValue().appendHqlString( sb ); + sb.append( " as " ); + QuotingHelper.appendDoubleQuoteEscapedString( sb, entry.getKey() ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java new file mode 100644 index 000000000000..b56e3a0c9136 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java @@ -0,0 +1,191 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.criteria.JpaJsonExistsExpression; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +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.Expression; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Special expression for the json_exists function that also captures special syntax elements like error behavior and passing variables. + * + * @since 7.0 + */ +@Incubating +public class SqmJsonExistsExpression extends AbstractSqmJsonPathExpression implements JpaJsonExistsExpression { + private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; + + public SqmJsonExistsExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + } + + private SqmJsonExistsExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name, + @Nullable Map> passingExpressions, + ErrorBehavior errorBehavior) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name, + passingExpressions + ); + this.errorBehavior = errorBehavior; + } + + public SqmJsonExistsExpression copy(SqmCopyContext context) { + final SqmJsonExistsExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = new ArrayList<>( getArguments().size() ); + for ( SqmTypedNode argument : getArguments() ) { + arguments.add( argument.copy( context ) ); + } + return context.registerCopy( + this, + new SqmJsonExistsExpression( + getFunctionDescriptor(), + getFunctionRenderer(), + arguments, + getImpliedResultType(), + getArgumentsValidator(), + getReturnTypeResolver(), + nodeBuilder(), + getFunctionName(), + copyPassingExpressions( context ), + errorBehavior + ) + ); + } + + @Override + public ErrorBehavior getErrorBehavior() { + return errorBehavior; + } + + @Override + public SqmJsonExistsExpression unspecifiedOnError() { + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + return this; + } + + @Override + public SqmJsonExistsExpression errorOnError() { + this.errorBehavior = ErrorBehavior.ERROR; + return this; + } + + @Override + public SqmJsonExistsExpression trueOnError() { + this.errorBehavior = ErrorBehavior.TRUE; + return this; + } + + @Override + public SqmJsonExistsExpression falseOnError() { + this.errorBehavior = ErrorBehavior.FALSE; + return this; + } + + @Override + public SqmJsonExistsExpression passing( + String parameterName, + jakarta.persistence.criteria.Expression expression) { + addPassingExpression( parameterName, (SqmExpression) expression ); + return this; + } + + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + final @Nullable ReturnableType resultType = resolveResultType( walker ); + final List arguments = resolveSqlAstArguments( getArguments(), walker ); + final ArgumentsValidator validator = getArgumentsValidator(); + if ( validator != null ) { + validator.validateSqlTypes( arguments, getFunctionName() ); + } + final JsonPathPassingClause jsonPathPassingClause = createJsonPathPassingClause( walker ); + if ( jsonPathPassingClause != null ) { + arguments.add( jsonPathPassingClause ); + } + switch ( errorBehavior ) { + case ERROR -> arguments.add( JsonExistsErrorBehavior.ERROR ); + case TRUE -> arguments.add( JsonExistsErrorBehavior.TRUE ); + case FALSE -> arguments.add( JsonExistsErrorBehavior.FALSE ); + } + return new SelfRenderingFunctionSqlAstExpression( + getFunctionName(), + getFunctionRenderer(), + arguments, + resultType, + resultType == null ? null : getMappingModelExpressible( walker, resultType, arguments ) + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( "json_exists(" ); + getArguments().get( 0 ).appendHqlString( sb ); + sb.append( ',' ); + getArguments().get( 1 ).appendHqlString( sb ); + + appendPassingExpressionHqlString( sb ); + 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( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java new file mode 100644 index 000000000000..ee3399435cb7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java @@ -0,0 +1,63 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.query.sqm.tree.expression; + +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Describes how a {@code null} should be treated in a JSON document. + * + * @since 7.0 + */ +public enum SqmJsonNullBehavior implements SqmTypedNode { + /** + * {@code null} values are removed. + */ + ABSENT, + /** + * {@code null} values are retained as JSON {@code null} literals. + */ + NULL; + + @Override + public @Nullable SqmExpressible getNodeType() { + return null; + } + + @Override + public NodeBuilder nodeBuilder() { + return null; + } + + @Override + public SqmJsonNullBehavior copy(SqmCopyContext context) { + return this; + } + + @Override + public X accept(SemanticQueryWalker walker) { + //noinspection unchecked + return (X) (this == NULL ? JsonNullBehavior.NULL : JsonNullBehavior.ABSENT); + } + + @Override + public void appendHqlString(StringBuilder sb) { + if ( this == NULL ) { + sb.append( " null on null" ); + } + else { + sb.append( " absent on null" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java new file mode 100644 index 000000000000..7cfd8e3b3233 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java @@ -0,0 +1,63 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.query.sqm.tree.expression; + +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmTypedNode; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Specifies if a {@code json_objectagg} may aggregate duplicate keys. + * + * @since 7.0 + */ +public enum SqmJsonObjectAggUniqueKeysBehavior implements SqmTypedNode { + /** + * Aggregate only unique keys. Fail aggregation if a duplicate is encountered. + */ + WITH, + /** + * Aggregate duplicate keys without failing. + */ + WITHOUT; + + @Override + public @Nullable SqmExpressible getNodeType() { + return null; + } + + @Override + public NodeBuilder nodeBuilder() { + return null; + } + + @Override + public SqmJsonObjectAggUniqueKeysBehavior copy(SqmCopyContext context) { + return this; + } + + @Override + public X accept(SemanticQueryWalker walker) { + //noinspection unchecked + return (X) (this == WITH ? JsonObjectAggUniqueKeysBehavior.WITH : JsonObjectAggUniqueKeysBehavior.WITHOUT); + } + + @Override + public void appendHqlString(StringBuilder sb) { + if ( this == WITH ) { + sb.append( " with unique keys" ); + } + else { + sb.append( " without unique keys" ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonQueryExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonQueryExpression.java new file mode 100644 index 000000000000..f1c83bbd201d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonQueryExpression.java @@ -0,0 +1,295 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.criteria.JpaJsonQueryExpression; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +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.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; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Special expression for the json_query function that also captures special syntax elements like error and empty behavior. + * + * @since 7.0 + */ +@Incubating +public class SqmJsonQueryExpression extends AbstractSqmJsonPathExpression implements JpaJsonQueryExpression { + private WrapMode wrapMode = WrapMode.UNSPECIFIED; + private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; + private EmptyBehavior emptyBehavior = EmptyBehavior.UNSPECIFIED; + + public SqmJsonQueryExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + } + + private SqmJsonQueryExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name, + @Nullable Map> passingExpressions, + WrapMode wrapMode, + ErrorBehavior errorBehavior, + EmptyBehavior emptyBehavior) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name, + passingExpressions + ); + this.wrapMode = wrapMode; + this.errorBehavior = errorBehavior; + this.emptyBehavior = emptyBehavior; + } + + public SqmJsonQueryExpression copy(SqmCopyContext context) { + final SqmJsonQueryExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = new ArrayList<>( getArguments().size() ); + for ( SqmTypedNode argument : getArguments() ) { + arguments.add( argument.copy( context ) ); + } + return context.registerCopy( + this, + new SqmJsonQueryExpression( + getFunctionDescriptor(), + getFunctionRenderer(), + arguments, + getImpliedResultType(), + getArgumentsValidator(), + getReturnTypeResolver(), + nodeBuilder(), + getFunctionName(), + copyPassingExpressions( context ), + wrapMode, + errorBehavior, + emptyBehavior + ) + ); + } + + @Override + public WrapMode getWrapMode() { + return wrapMode; + } + + @Override + public ErrorBehavior getErrorBehavior() { + return errorBehavior; + } + + @Override + public EmptyBehavior getEmptyBehavior() { + return emptyBehavior; + } + + @Override + public SqmJsonQueryExpression withoutWrapper() { + this.wrapMode = WrapMode.WITHOUT_WRAPPER; + return this; + } + + @Override + public SqmJsonQueryExpression withWrapper() { + this.wrapMode = WrapMode.WITH_WRAPPER; + return this; + } + + @Override + public SqmJsonQueryExpression withConditionalWrapper() { + this.wrapMode = WrapMode.WITH_CONDITIONAL_WRAPPER; + return this; + } + + @Override + public SqmJsonQueryExpression unspecifiedWrapper() { + this.wrapMode = WrapMode.UNSPECIFIED; + return this; + } + + @Override + public SqmJsonQueryExpression unspecifiedOnError() { + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + return this; + } + + @Override + public SqmJsonQueryExpression errorOnError() { + this.errorBehavior = ErrorBehavior.ERROR; + return this; + } + + @Override + public SqmJsonQueryExpression nullOnError() { + this.errorBehavior = ErrorBehavior.NULL; + return this; + } + + @Override + public SqmJsonQueryExpression emptyArrayOnError() { + this.errorBehavior = ErrorBehavior.EMPTY_ARRAY; + return this; + } + + @Override + public SqmJsonQueryExpression emptyObjectOnError() { + this.errorBehavior = ErrorBehavior.EMPTY_OBJECT; + return this; + } + + @Override + public SqmJsonQueryExpression unspecifiedOnEmpty() { + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + return this; + } + + @Override + public SqmJsonQueryExpression errorOnEmpty() { + this.emptyBehavior = EmptyBehavior.ERROR; + return this; + } + + @Override + public SqmJsonQueryExpression nullOnEmpty() { + this.emptyBehavior = EmptyBehavior.NULL; + return this; + } + + @Override + public SqmJsonQueryExpression emptyArrayOnEmpty() { + this.emptyBehavior = EmptyBehavior.EMPTY_ARRAY; + return this; + } + + @Override + public SqmJsonQueryExpression emptyObjectOnEmpty() { + this.emptyBehavior = EmptyBehavior.EMPTY_OBJECT; + return this; + } + + @Override + public SqmJsonQueryExpression passing( + String parameterName, + jakarta.persistence.criteria.Expression expression) { + addPassingExpression( parameterName, (SqmExpression) expression ); + return this; + } + + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + final @Nullable ReturnableType resultType = resolveResultType( walker ); + final List arguments = resolveSqlAstArguments( getArguments(), walker ); + final ArgumentsValidator validator = getArgumentsValidator(); + if ( validator != null ) { + validator.validateSqlTypes( arguments, getFunctionName() ); + } + final JsonPathPassingClause jsonPathPassingClause = createJsonPathPassingClause( walker ); + if ( jsonPathPassingClause != null ) { + arguments.add( jsonPathPassingClause ); + } + switch ( wrapMode ) { + case WITH_WRAPPER -> arguments.add( JsonQueryWrapMode.WITH_WRAPPER ); + case WITHOUT_WRAPPER -> arguments.add( JsonQueryWrapMode.WITHOUT_WRAPPER ); + case WITH_CONDITIONAL_WRAPPER -> arguments.add( JsonQueryWrapMode.WITH_CONDITIONAL_WRAPPER ); + } + switch ( errorBehavior ) { + case NULL -> arguments.add( JsonQueryErrorBehavior.NULL ); + case ERROR -> arguments.add( JsonQueryErrorBehavior.ERROR ); + case EMPTY_OBJECT -> arguments.add( JsonQueryErrorBehavior.EMPTY_OBJECT ); + case EMPTY_ARRAY -> arguments.add( JsonQueryErrorBehavior.EMPTY_ARRAY ); + } + switch ( emptyBehavior ) { + case NULL -> arguments.add( JsonQueryEmptyBehavior.NULL ); + case ERROR -> arguments.add( JsonQueryEmptyBehavior.ERROR ); + case EMPTY_OBJECT -> arguments.add( JsonQueryEmptyBehavior.EMPTY_OBJECT ); + case EMPTY_ARRAY -> arguments.add( JsonQueryEmptyBehavior.EMPTY_ARRAY ); + } + return new SelfRenderingFunctionSqlAstExpression( + getFunctionName(), + getFunctionRenderer(), + arguments, + resultType, + resultType == null ? null : getMappingModelExpressible( walker, resultType, arguments ) + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( "json_query(" ); + getArguments().get( 0 ).appendHqlString( sb ); + sb.append( ',' ); + getArguments().get( 1 ).appendHqlString( sb ); + + appendPassingExpressionHqlString( sb ); + 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" ); + } + 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" ); + } + sb.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java new file mode 100644 index 000000000000..ab07af0dedc9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java @@ -0,0 +1,280 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.query.sqm.tree.expression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaJsonValueExpression; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.function.FunctionRenderer; +import org.hibernate.query.sqm.function.SelfRenderingFunctionSqlAstExpression; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.ArgumentsValidator; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; +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.Expression; +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.checkerframework.checker.nullness.qual.Nullable; + +/** + * Special expression for the json_value function that also captures special syntax elements like error and empty behavior. + * + * @since 7.0 + */ +@Incubating +public class SqmJsonValueExpression extends AbstractSqmJsonPathExpression implements JpaJsonValueExpression { + private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; + private SqmExpression errorDefaultExpression; + private EmptyBehavior emptyBehavior = EmptyBehavior.UNSPECIFIED; + private SqmExpression emptyDefaultExpression; + + public SqmJsonValueExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + } + + private SqmJsonValueExpression( + SqmFunctionDescriptor descriptor, + FunctionRenderer renderer, + List> arguments, + @Nullable ReturnableType impliedResultType, + @Nullable ArgumentsValidator argumentsValidator, + FunctionReturnTypeResolver returnTypeResolver, + NodeBuilder nodeBuilder, + String name, + @Nullable Map> passingExpressions, + ErrorBehavior errorBehavior, + SqmExpression errorDefaultExpression, + EmptyBehavior emptyBehavior, + SqmExpression emptyDefaultExpression) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name, + passingExpressions + ); + this.errorBehavior = errorBehavior; + this.errorDefaultExpression = errorDefaultExpression; + this.emptyBehavior = emptyBehavior; + this.emptyDefaultExpression = emptyDefaultExpression; + } + + public SqmJsonValueExpression copy(SqmCopyContext context) { + final SqmJsonValueExpression existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final List> arguments = new ArrayList<>( getArguments().size() ); + for ( SqmTypedNode argument : getArguments() ) { + arguments.add( argument.copy( context ) ); + } + return context.registerCopy( + this, + new SqmJsonValueExpression<>( + getFunctionDescriptor(), + getFunctionRenderer(), + arguments, + getImpliedResultType(), + getArgumentsValidator(), + getReturnTypeResolver(), + nodeBuilder(), + getFunctionName(), + copyPassingExpressions( context ), + errorBehavior, + errorDefaultExpression == null ? null : errorDefaultExpression.copy( context ), + emptyBehavior, + emptyDefaultExpression == null ? null : emptyDefaultExpression.copy( context ) + ) + ); + } + + @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 SqmJsonValueExpression unspecifiedOnError() { + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + this.errorDefaultExpression = null; + return this; + } + + @Override + public SqmJsonValueExpression errorOnError() { + this.errorBehavior = ErrorBehavior.ERROR; + this.errorDefaultExpression = null; + return this; + } + + @Override + public SqmJsonValueExpression nullOnError() { + this.errorBehavior = ErrorBehavior.NULL; + this.errorDefaultExpression = null; + return this; + } + + @Override + public SqmJsonValueExpression defaultOnError(jakarta.persistence.criteria.Expression expression) { + this.errorBehavior = ErrorBehavior.DEFAULT; + //noinspection unchecked + this.errorDefaultExpression = (SqmExpression) expression; + return this; + } + + @Override + public SqmJsonValueExpression unspecifiedOnEmpty() { + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + this.errorDefaultExpression = null; + return this; + } + + @Override + public SqmJsonValueExpression errorOnEmpty() { + this.emptyBehavior = EmptyBehavior.ERROR; + this.emptyDefaultExpression = null; + return this; + } + + @Override + public SqmJsonValueExpression nullOnEmpty() { + this.emptyBehavior = EmptyBehavior.NULL; + this.emptyDefaultExpression = null; + return this; + } + + @Override + public SqmJsonValueExpression defaultOnEmpty(jakarta.persistence.criteria.Expression expression) { + this.emptyBehavior = EmptyBehavior.DEFAULT; + //noinspection unchecked + this.emptyDefaultExpression = (SqmExpression) expression; + return this; + } + + @Override + public SqmJsonValueExpression passing( + String parameterName, + jakarta.persistence.criteria.Expression expression) { + addPassingExpression( parameterName, (SqmExpression) expression ); + return this; + } + + @Override + public Expression convertToSqlAst(SqmToSqlAstConverter walker) { + final @Nullable ReturnableType resultType = resolveResultType( walker ); + final List arguments = resolveSqlAstArguments( getArguments(), walker ); + final ArgumentsValidator validator = getArgumentsValidator(); + if ( validator != null ) { + validator.validateSqlTypes( arguments, getFunctionName() ); + } + final JsonPathPassingClause jsonPathPassingClause = createJsonPathPassingClause( walker ); + if ( jsonPathPassingClause != null ) { + arguments.add( jsonPathPassingClause ); + } + switch ( errorBehavior ) { + case NULL -> arguments.add( JsonValueErrorBehavior.NULL ); + case ERROR -> arguments.add( JsonValueErrorBehavior.ERROR ); + case DEFAULT -> arguments.add( JsonValueErrorBehavior.defaultOnError( + (Expression) errorDefaultExpression.accept( walker ) + ) ); + } + switch ( emptyBehavior ) { + case NULL -> arguments.add( JsonValueEmptyBehavior.NULL ); + case ERROR -> arguments.add( JsonValueEmptyBehavior.ERROR ); + case DEFAULT -> arguments.add( JsonValueEmptyBehavior.defaultOnEmpty( + (Expression) emptyDefaultExpression.accept( walker ) + ) ); + } + return new SelfRenderingFunctionSqlAstExpression( + getFunctionName(), + getFunctionRenderer(), + arguments, + resultType, + resultType == null ? null : getMappingModelExpressible( walker, resultType, arguments ) + ); + } + + @Override + public void appendHqlString(StringBuilder sb) { + sb.append( "json_value(" ); + getArguments().get( 0 ).appendHqlString( sb ); + sb.append( ',' ); + getArguments().get( 1 ).appendHqlString( sb ); + + appendPassingExpressionHqlString( sb ); + if ( getArguments().size() > 2 ) { + sb.append( " returning " ); + getArguments().get( 2 ).appendHqlString( sb ); + } + switch ( errorBehavior ) { + case NULL -> sb.append( " null on error" ); + case ERROR -> sb.append( " error on error" ); + case DEFAULT -> { + 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 -> { + sb.append( " default " ); + emptyDefaultExpression.appendHqlString( sb ); + sb.append( " on empty" ); + } + } + sb.append( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java index 4b6cda0387a8..d32ca58a224c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmLiteral.java @@ -6,7 +6,7 @@ */ package org.hibernate.query.sqm.tree.expression; -import org.hibernate.query.internal.QueryLiteralHelper; +import org.hibernate.internal.util.QuotingHelper; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.SqmExpressible; @@ -84,7 +84,7 @@ public static void appendHqlString(StringBuilder sb, JavaType javaType, @ else { final String string = javaType.toString( value ); if ( javaType.getJavaTypeClass() == String.class ) { - QueryLiteralHelper.appendStringLiteral( sb, string ); + QuotingHelper.appendSingleQuoteEscapedString( sb, string ); } else { sb.append( string ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java index d81e3914e1e3..605896a99391 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java @@ -12,6 +12,7 @@ import org.hibernate.internal.util.collections.Stack; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -23,6 +24,12 @@ public interface SqlAstTranslator extends SqlAstWalker SessionFactoryImplementor getSessionFactory(); + /** + * Returns the literal value of the given expression, inlining a parameter value if necessary. + * @since 7.0 + */ + X getLiteralValue(Expression expression); + /** * Renders the given SQL AST node with the given rendering mode. */ diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 35209b7bf23b..a13e17bc6d86 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -40,6 +40,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.FilterJdbcParameter; import org.hibernate.internal.util.MathHelper; +import org.hibernate.internal.util.QuotingHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.internal.util.collections.Stack; @@ -121,6 +122,7 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.ExtractUnit; import org.hibernate.sql.ast.tree.expression.Format; +import org.hibernate.sql.ast.tree.expression.FunctionExpression; import org.hibernate.sql.ast.tree.expression.JdbcLiteral; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.expression.Literal; @@ -550,6 +552,16 @@ public void appendSql(boolean value) { sqlBuffer.append( value ); } + @Override + public void appendDoubleQuoteEscapedString(String value) { + QuotingHelper.appendDoubleQuoteEscapedString( sqlBuffer, value ); + } + + @Override + public void appendSingleQuoteEscapedString(String value) { + QuotingHelper.appendSingleQuoteEscapedString( sqlBuffer, value ); + } + @Override public Appendable append(CharSequence csq) { sqlBuffer.append( csq ); @@ -660,6 +672,11 @@ protected void setLimitParameter(JdbcParameter limitParameter) { this.limitParameter = limitParameter; } + @Override + public X getLiteralValue(Expression expression) { + return interpretExpression( expression, jdbcParameterBindings ); + } + @SuppressWarnings("unchecked") protected R interpretExpression(Expression expression, JdbcParameterBindings jdbcParameterBindings) { if ( expression instanceof Literal ) { @@ -677,6 +694,20 @@ else if ( expression instanceof SqmParameterInterpretation ) { } return (R) getParameterBindValue( (JdbcParameter) ( (SqmParameterInterpretation) expression).getResolvedExpression() ); } + else if ( expression instanceof FunctionExpression functionExpression ) { + if ( "concat".equals( functionExpression.getFunctionName() ) ) { + final List arguments = functionExpression.getArguments(); + final StringBuilder sb = new StringBuilder(); + for ( SqlAstNode argument : arguments ) { + final Object argumentLiteral = interpretExpression( (Expression) argument, jdbcParameterBindings ); + if ( argumentLiteral == null ) { + return null; + } + sb.append( argumentLiteral ); + } + return (R) sb.toString(); + } + } throw new UnsupportedOperationException( "Can't interpret expression: " + expression ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java index f6797cc15e19..1a80b11be7fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAppender.java @@ -6,6 +6,8 @@ */ package org.hibernate.sql.ast.spi; +import org.hibernate.internal.util.QuotingHelper; + /** * Access to appending SQL fragments to an in-flight buffer * @@ -46,6 +48,18 @@ default void appendSql(boolean value) { appendSql( String.valueOf( value ) ); } + default void appendDoubleQuoteEscapedString(String value) { + final StringBuilder sb = new StringBuilder( value.length() + 2 ); + QuotingHelper.appendDoubleQuoteEscapedString( sb, value ); + appendSql( sb.toString() ); + } + + default void appendSingleQuoteEscapedString(String value) { + final StringBuilder sb = new StringBuilder( value.length() + 2 ); + QuotingHelper.appendSingleQuoteEscapedString( sb, value ); + appendSql( sb.toString() ); + } + default Appendable append(CharSequence csq) { appendSql( csq.toString() ); return this; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java new file mode 100644 index 000000000000..ba2f8a799015 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java @@ -0,0 +1,25 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +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 JsonExistsErrorBehavior implements SqlAstNode { + TRUE, + FALSE, + ERROR; + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonExistsErrorBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonNullBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonNullBehavior.java new file mode 100644 index 000000000000..8ef7b60bc2c5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonNullBehavior.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +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 JsonNullBehavior implements SqlAstNode { + NULL, + ABSENT; + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonNullBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonObjectAggUniqueKeysBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonObjectAggUniqueKeysBehavior.java new file mode 100644 index 000000000000..6d2c32eaaf5c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonObjectAggUniqueKeysBehavior.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +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 JsonObjectAggUniqueKeysBehavior implements SqlAstNode { + WITH, + WITHOUT; + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonObjectAggUniqueKeysBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java new file mode 100644 index 000000000000..c77ef12a4802 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.ast.tree.expression; + +import java.util.Map; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +/** + * @since 7.0 + */ +public class JsonPathPassingClause implements SqlAstNode { + + private final Map passingExpressions; + + public JsonPathPassingClause(Map passingExpressions) { + this.passingExpressions = passingExpressions; + } + + public Map getPassingExpressions() { + return passingExpressions; + } + + @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/JsonQueryEmptyBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryEmptyBehavior.java new file mode 100644 index 000000000000..db4f304ec05c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryEmptyBehavior.java @@ -0,0 +1,26 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +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 JsonQueryEmptyBehavior implements SqlAstNode { + ERROR, + NULL, + EMPTY_ARRAY, + EMPTY_OBJECT; + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonQueryEmptyBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryErrorBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryErrorBehavior.java new file mode 100644 index 000000000000..0aec8a3438c8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryErrorBehavior.java @@ -0,0 +1,26 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +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 JsonQueryErrorBehavior implements SqlAstNode { + ERROR, + NULL, + EMPTY_ARRAY, + EMPTY_OBJECT; + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonQueryErrorBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryWrapMode.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryWrapMode.java new file mode 100644 index 000000000000..b951d4f970aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryWrapMode.java @@ -0,0 +1,25 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +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 JsonQueryWrapMode implements SqlAstNode { + WITH_WRAPPER, + WITHOUT_WRAPPER, + WITH_CONDITIONAL_WRAPPER; + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonQueryWrapMode doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueEmptyBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueEmptyBehavior.java new file mode 100644 index 000000000000..121968ab5d99 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueEmptyBehavior.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.ast.tree.expression; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * @since 7.0 + */ +public class JsonValueEmptyBehavior implements SqlAstNode { + public static final JsonValueEmptyBehavior NULL = new JsonValueEmptyBehavior( null ); + public static final JsonValueEmptyBehavior ERROR = new JsonValueEmptyBehavior( null ); + + private final @Nullable Expression defaultExpression; + + private JsonValueEmptyBehavior(@Nullable Expression defaultExpression) { + this.defaultExpression = defaultExpression; + } + + public static JsonValueEmptyBehavior defaultOnEmpty(Expression defaultExpression) { + return new JsonValueEmptyBehavior( defaultExpression ); + } + + public @Nullable Expression getDefaultExpression() { + return defaultExpression; + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonValueEmptyBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueErrorBehavior.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueErrorBehavior.java new file mode 100644 index 000000000000..d70a16fa0e37 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueErrorBehavior.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.sql.ast.tree.expression; + +import org.hibernate.sql.ast.SqlAstWalker; +import org.hibernate.sql.ast.tree.SqlAstNode; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * @since 7.0 + */ +public class JsonValueErrorBehavior implements SqlAstNode { + public static final JsonValueErrorBehavior NULL = new JsonValueErrorBehavior( null ); + public static final JsonValueErrorBehavior ERROR = new JsonValueErrorBehavior( null ); + + private final @Nullable Expression defaultExpression; + + private JsonValueErrorBehavior(@Nullable Expression defaultExpression) { + this.defaultExpression = defaultExpression; + } + + public static JsonValueErrorBehavior defaultOnError(Expression defaultExpression) { + return new JsonValueErrorBehavior( defaultExpression ); + } + + public @Nullable Expression getDefaultExpression() { + return defaultExpression; + } + + @Override + public void accept(SqlAstWalker sqlTreeWalker) { + throw new UnsupportedOperationException("JsonValueErrorBehavior doesn't support walking"); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java index 081e60f283b3..aeda3626a73d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java @@ -991,4 +991,36 @@ public static boolean isEnumType(int typeCode) { return false; } } + + /** + * Does the typecode represent a JSON type. + * + * @param typeCode - a JDBC type code + * @since 7.0 + */ + public static boolean isJsonType(int typeCode) { + switch ( typeCode ) { + case JSON: + case JSON_ARRAY: + return true; + default: + return false; + } + } + + /** + * Does the typecode represent a JSON type or a type that can be implicitly cast to JSON. + * + * @param typeCode - a JDBC type code + * @since 7.0 + */ + public static boolean isImplicitJsonType(int typeCode) { + switch ( typeCode ) { + case JSON: + case JSON_ARRAY: + return true; + default: + return isCharacterOrClobType( typeCode ); + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java index 4123d1ea3c50..56fc53b7615a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java @@ -427,6 +427,16 @@ default boolean isSpatial() { return isSpatialType( getDefaultSqlTypeCode() ); } + @Incubating + default boolean isJson() { + return isJsonType( getDefaultSqlTypeCode() ); + } + + @Incubating + default boolean isImplicitJson() { + return isImplicitJsonType( getDefaultSqlTypeCode() ); + } + @Incubating default boolean isBoolean() { return getDefaultSqlTypeCode() == BOOLEAN; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java new file mode 100644 index 000000000000..b67e056acb8f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java @@ -0,0 +1,164 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; + +/** + * Specialized type mapping for {@code JSON_ARRAY} and the JSON SQL data type. + * + * @author Christian Beikov + */ +public class JsonArrayAsStringJdbcType extends JsonArrayJdbcType implements AdjustableJdbcType { + /** + * Singleton access + */ + public static final JsonArrayAsStringJdbcType VARCHAR_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.LONG32VARCHAR ); + public static final JsonArrayAsStringJdbcType NVARCHAR_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.LONG32NVARCHAR ); + public static final JsonArrayAsStringJdbcType CLOB_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.CLOB ); + public static final JsonArrayAsStringJdbcType NCLOB_INSTANCE = new JsonArrayAsStringJdbcType( SqlTypes.NCLOB ); + + private final boolean nationalized; + private final int ddlTypeCode; + protected JsonArrayAsStringJdbcType(int ddlTypeCode) { + this.ddlTypeCode = ddlTypeCode; + this.nationalized = ddlTypeCode == SqlTypes.LONG32NVARCHAR + || ddlTypeCode == SqlTypes.NCLOB; + } + + @Override + public int getJdbcTypeCode() { + return nationalized ? SqlTypes.NVARCHAR : SqlTypes.VARCHAR; + } + + @Override + public int getDdlTypeCode() { + return ddlTypeCode; + } + + @Override + public String toString() { + return "JsonArrayAsStringJdbcType"; + } + + @Override + public JdbcType resolveIndicatedType(JdbcTypeIndicators indicators, JavaType domainJtd) { + // Depending on the size of the column, we might have to adjust the jdbc type code for DDL. + // In some DBMS we can compare LOBs with special functions which is handled in the SqlAstTranslators, + // but that requires the correct jdbc type code to be available, which we ensure this way + if ( needsLob( indicators ) ) { + return indicators.isNationalized() ? NCLOB_INSTANCE : CLOB_INSTANCE; + } + else { + return indicators.isNationalized() ? NVARCHAR_INSTANCE : VARCHAR_INSTANCE; + } + } + + protected boolean needsLob(JdbcTypeIndicators indicators) { + final Dialect dialect = indicators.getDialect(); + final long length = indicators.getColumnLength(); + final long maxLength = indicators.isNationalized() + ? dialect.getMaxNVarcharLength() + : dialect.getMaxVarcharLength(); + if ( length > maxLength ) { + return true; + } + + final DdlTypeRegistry ddlTypeRegistry = indicators.getTypeConfiguration().getDdlTypeRegistry(); + final String typeName = ddlTypeRegistry.getTypeName( getDdlTypeCode(), dialect ); + return typeName.equals( ddlTypeRegistry.getTypeName( SqlTypes.CLOB, dialect ) ) + || typeName.equals( ddlTypeRegistry.getTypeName( SqlTypes.NCLOB, dialect ) ); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + if ( nationalized ) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String json = ( (JsonArrayAsStringJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + if ( options.getDialect().supportsNationalizedMethods() ) { + st.setNString( index, json ); + } + else { + st.setString( index, json ); + } + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String json = ( (JsonArrayAsStringJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + if ( options.getDialect().supportsNationalizedMethods() ) { + st.setNString( name, json ); + } + else { + st.setString( name, json ); + } + } + }; + } + else { + return super.getBinder( javaType ); + } + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + if ( nationalized ) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + if ( options.getDialect().supportsNationalizedMethods() ) { + return fromString( rs.getNString( paramIndex ), getJavaType(), options ); + } + else { + return fromString( rs.getString( paramIndex ), getJavaType(), options ); + } + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) + throws SQLException { + if ( options.getDialect().supportsNationalizedMethods() ) { + return fromString( statement.getNString( index ), getJavaType(), options ); + } + else { + return fromString( statement.getString( index ), getJavaType(), options ); + } + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) + throws SQLException { + if ( options.getDialect().supportsNationalizedMethods() ) { + return fromString( statement.getNString( name ), getJavaType(), options ); + } + else { + return fromString( statement.getString( name ), getJavaType(), options ); + } + } + + }; + } + else { + return super.getExtractor( javaType ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java new file mode 100644 index 000000000000..4835cd84bd90 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java @@ -0,0 +1,113 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.type.descriptor.jdbc; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.JavaType; + +/** + * Specialized type mapping for {@code JSON_ARRAY} and the JSON ARRAY SQL data type. + * + * @author Christian Beikov + */ +public class JsonArrayJdbcType implements JdbcType { + /** + * Singleton access + */ + public static final JsonArrayJdbcType INSTANCE = new JsonArrayJdbcType(); + + protected JsonArrayJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return SqlTypes.VARCHAR; + } + + @Override + public int getDefaultSqlTypeCode() { + return SqlTypes.JSON_ARRAY; + } + + @Override + public String toString() { + return "JsonArrayJdbcType"; + } + + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { + // No literal support for now + return null; + } + + protected X fromString(String string, JavaType javaType, WrapperOptions options) throws SQLException { + if ( string == null ) { + return null; + } + return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().fromString( + string, + javaType, + options + ); + } + + protected String toString(X value, JavaType javaType, WrapperOptions options) { + return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().toString( + value, + javaType, + options + ); + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String json = ( (JsonArrayJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setString( index, json ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String json = ( (JsonArrayJdbcType) getJdbcType() ).toString( value, getJavaType(), options ); + st.setString( name, json ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return fromString( rs.getString( paramIndex ), getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return fromString( statement.getString( index ), getJavaType(), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return fromString( statement.getString( name ), getJavaType(), options ); + } + + }; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java new file mode 100644 index 000000000000..12fe9aaff652 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java @@ -0,0 +1,109 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.type.descriptor.jdbc; + +import org.hibernate.dialect.Dialect; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; +import org.hibernate.type.descriptor.java.JavaType; + +import java.nio.charset.StandardCharsets; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * Specialized type mapping for {@code JSON} and the BLOB SQL data type for Oracle. + * + * @author Christian Beikov + */ +public class OracleJsonArrayBlobJdbcType extends JsonArrayJdbcType { + /** + * Singleton access + */ + public static final OracleJsonArrayBlobJdbcType INSTANCE = new OracleJsonArrayBlobJdbcType(); + + protected OracleJsonArrayBlobJdbcType() { + } + + @Override + public int getJdbcTypeCode() { + return SqlTypes.BLOB; + } + + @Override + public String toString() { + return "JsonArrayBlobJdbcType"; + } + + @Override + public String getCheckCondition(String columnName, JavaType javaType, BasicValueConverter converter, Dialect dialect) { + return columnName + " is json"; + } + + @Override + public ValueBinder getBinder(JavaType javaType) { + return new BasicBinder<>( javaType, this ) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) + throws SQLException { + final String json = OracleJsonArrayBlobJdbcType.this.toString( + value, + getJavaType(), + options + ); + st.setBytes( index, json.getBytes( StandardCharsets.UTF_8 ) ); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) + throws SQLException { + final String json = OracleJsonArrayBlobJdbcType.this.toString( + value, + getJavaType(), + options + ); + st.setBytes( name, json.getBytes( StandardCharsets.UTF_8 ) ); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaType javaType) { + return new BasicExtractor<>( javaType, this ) { + @Override + protected X doExtract(ResultSet rs, int paramIndex, WrapperOptions options) throws SQLException { + return fromString( rs.getBytes( paramIndex ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return fromString( statement.getBytes( index ), options ); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return fromString( statement.getBytes( name ), options ); + } + + private X fromString(byte[] json, WrapperOptions options) throws SQLException { + if ( json == null ) { + return null; + } + return OracleJsonArrayBlobJdbcType.this.fromString( + new String( json, StandardCharsets.UTF_8 ), + getJavaType(), + options + ); + } + }; + } +} 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 78a15af2451f..d627e3295d42 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 @@ -28,16 +28,14 @@ * * @author Christian Beikov */ -public class OracleJsonBlobJdbcType implements AggregateJdbcType { +public class OracleJsonBlobJdbcType extends JsonJdbcType { /** * Singleton access */ public static final OracleJsonBlobJdbcType INSTANCE = new OracleJsonBlobJdbcType( null ); - private final EmbeddableMappingType embeddableMappingType; - protected OracleJsonBlobJdbcType(EmbeddableMappingType embeddableMappingType) { - this.embeddableMappingType = embeddableMappingType; + super( embeddableMappingType ); } @Override @@ -45,22 +43,11 @@ public int getJdbcTypeCode() { return SqlTypes.BLOB; } - @Override - public int getDefaultSqlTypeCode() { - return SqlTypes.JSON; - } - @Override public String toString() { return "JsonBlobJdbcType"; } - @Override - public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaType javaType) { - // No literal support for now - return null; - } - @Override public String getCheckCondition(String columnName, JavaType javaType, BasicValueConverter converter, Dialect dialect) { return columnName + " is json"; @@ -74,50 +61,6 @@ public AggregateJdbcType resolveAggregateJdbcType( return new OracleJsonBlobJdbcType( mappingType ); } - @Override - public EmbeddableMappingType getEmbeddableMappingType() { - return embeddableMappingType; - } - - protected X fromString(String string, JavaType javaType, WrapperOptions options) throws SQLException { - if ( embeddableMappingType != null ) { - return JsonHelper.fromString( - embeddableMappingType, - string, - javaType.getJavaTypeClass() != Object[].class, - options - ); - } - return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().fromString( - string, - javaType, - options - ); - } - - @Override - public Object createJdbcValue(Object domainValue, WrapperOptions options) throws SQLException { - assert embeddableMappingType != null; - return JsonHelper.toString( embeddableMappingType, domainValue, options ); - } - - @Override - public Object[] extractJdbcValues(Object rawJdbcValue, WrapperOptions options) throws SQLException { - assert embeddableMappingType != null; - return JsonHelper.fromString( embeddableMappingType, (String) rawJdbcValue, false, options ); - } - - protected String toString(X value, JavaType javaType, WrapperOptions options) { - if ( embeddableMappingType != null ) { - return JsonHelper.toString( embeddableMappingType, value, options ); - } - return options.getSessionFactory().getFastSessionServices().getJsonFormatMapper().toString( - value, - javaType, - options - ); - } - @Override public ValueBinder getBinder(JavaType javaType) { return new BasicBinder<>( javaType, this ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java b/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java index 93aa5d361427..83302699a3e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java +++ b/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java @@ -352,6 +352,8 @@ public BasicType resolveCastTargetType(String name) { case "truefalse": return basicTypeRegistry.getRegisteredType( StandardBasicTypes.TRUE_FALSE.getName() ); case "yesno": return basicTypeRegistry.getRegisteredType( StandardBasicTypes.YES_NO.getName() ); case "numericboolean": return basicTypeRegistry.getRegisteredType( StandardBasicTypes.NUMERIC_BOOLEAN.getName() ); + case "json": return basicTypeRegistry.resolve( Object.class, SqlTypes.JSON ); + case "xml": return basicTypeRegistry.resolve( Object.class, SqlTypes.SQLXML ); //really not sure about this one - it works well for casting from binary //to UUID, but people will want to use it to cast from varchar, and that //won't work at all without some special casing in the Dialects diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/JsonPathHelperTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/JsonPathHelperTest.java new file mode 100644 index 000000000000..0d00f0cf780a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/JsonPathHelperTest.java @@ -0,0 +1,89 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2010, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.orm.test.dialect.function; + +import java.util.List; + +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.query.Query; + +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JsonPathHelperTest { + + @Test + public void testRoot() { + assertEquals( + List.of(), + JsonPathHelper.parseJsonPathElements( "$" ) + ); + } + + @Test + public void testRootArray() { + assertEquals( + List.of( new JsonPathHelper.JsonIndexAccess( 0 ) ), + JsonPathHelper.parseJsonPathElements( "$[0]" ) + ); + } + + @Test + public void testDeReferenceRootArray() { + assertEquals( + List.of( new JsonPathHelper.JsonIndexAccess( 0 ), new JsonPathHelper.JsonAttribute( "attribute" ) ), + JsonPathHelper.parseJsonPathElements( "$[0].attribute" ) + ); + } + + @Test + public void testSimplePath() { + assertEquals( + List.of( new JsonPathHelper.JsonAttribute( "attribute" ) ), + JsonPathHelper.parseJsonPathElements( "$.attribute" ) + ); + } + + @Test + public void testArrayPath() { + assertEquals( + List.of( new JsonPathHelper.JsonAttribute( "attribute" ), new JsonPathHelper.JsonIndexAccess( 0 ) ), + JsonPathHelper.parseJsonPathElements( "$.attribute[0]" ) + ); + } + + @Test + public void testDeepArrayPath() { + assertEquals( + List.of( + new JsonPathHelper.JsonAttribute( "attribute" ), + new JsonPathHelper.JsonIndexAccess( 0 ), + new JsonPathHelper.JsonAttribute( "subAttribute" ) + ), + JsonPathHelper.parseJsonPathElements( "$.attribute[0].subAttribute" ) + ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/fetching/FetchingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/fetching/FetchingTest.java index e6e633ebbe06..39f2dbdc07d2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/fetching/FetchingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/fetching/FetchingTest.java @@ -27,7 +27,6 @@ import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; import org.hibernate.testing.orm.junit.Jpa; import org.hibernate.testing.orm.junit.RequiresDialect; -import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; @@ -43,7 +42,6 @@ FetchingTest.Project.class }) @RequiresDialect(H2Dialect.class) -@SkipForDialect(dialectClass = H2Dialect.class, majorVersion = 2, matchSubTypes = true, reason = "See https://github.com/h2database/h2database/issues/3338") public class FetchingTest { @Test @@ -170,17 +168,11 @@ public static class Employee { @NaturalId private String username; - @Column(name = "pswd") + @Column(name = "pswd", columnDefinition = "varbinary") @ColumnTransformer( - read = "decrypt('AES', '00', pswd )", + read = "trim(trailing u&'\\0000' from cast(decrypt('AES', '00', pswd ) as character varying))", write = "encrypt('AES', '00', ?)" ) -// For H2 2.0.202+ one must use the varbinary DDL type -// @Column(name = "pswd", columnDefinition = "varbinary") -// @ColumnTransformer( -// read = "trim(trailing u&'\\0000' from cast(decrypt('AES', '00', pswd ) as character varying))", -// write = "encrypt('AES', '00', ?)" -// ) private String password; private int accessLevel; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/EntityWithJson.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/EntityWithJson.java new file mode 100644 index 000000000000..558ca50e848d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/EntityWithJson.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class EntityWithJson { + @Id + private Long id; + @JdbcTypeCode(SqlTypes.JSON) + private Map json = new HashMap<>();; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Map getJson() { + return json; + } + + public void setJson(Map json) { + this.json = json; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAggregateTest.java new file mode 100644 index 000000000000..3cb15e705627 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAggregateTest.java @@ -0,0 +1,57 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +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.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonArrayAgg.class) +public class JsonArrayAggregateTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-arrayagg-example[] + em.createQuery( "select json_arrayagg(e.theString) from EntityOfBasics e" ).getResultList(); + //end::hql-json-arrayagg-example[] + } ); + } + + @Test + public void testNull(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-arrayagg-null-example[] + em.createQuery( "select json_arrayagg(e.theString null on null) from EntityOfBasics e" ).getResultList(); + //end::hql-json-arrayagg-null-example[] + } ); + } + + @Test + public void testOrderBy(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-arrayagg-order-by-example[] + em.createQuery( "select json_arrayagg(e.theString order by e.id) from EntityOfBasics e" ).getResultList(); + //end::hql-json-arrayagg-order-by-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayTest.java new file mode 100644 index 000000000000..7edd9ee8483f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayTest.java @@ -0,0 +1,54 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import org.hibernate.cfg.QuerySettings; + +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.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonArray.class) +public class JsonArrayTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-array-example[] + em.createQuery( "select json_array('val1', 'val2'), json_array(1, false, 'val')" ).getResultList(); + //end::hql-json-array-example[] + } ); + } + + @Test + public void testNullClause(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-array-on-null-example[] + em.createQuery("select json_array(null, 1 null on null)" ).getResultList(); + //end::hql-json-array-on-null-example[] + } ); + } + + @Test + public void testAbsentOnNull(SessionFactoryScope scope) { + scope.inSession( em -> { + em.createQuery("select json_array(null, 1 absent on null)" ).getResultList(); + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java new file mode 100644 index 000000000000..84a373cdf209 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java @@ -0,0 +1,108 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import java.util.HashMap; +import java.util.List; + +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.sql.exec.ExecutionException; + +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.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = EntityWithJson.class) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonExists.class) +public class JsonExistsTest { + + @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-exists-example[] + List results = em.createQuery( "select json_exists(e.json, '$.theString') from EntityWithJson e", Boolean.class ) + .getResultList(); + //end::hql-json-exists-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + @SkipForDialect(dialectClass = OracleDialect.class, majorVersion = 21, matchSubTypes = true, reason = "Oracle bug in versions before 23") + public void testPassing(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-exists-passing-example[] + List results = em.createQuery( "select json_exists(e.json, '$.theArray[$idx]' passing 1 as idx) from EntityWithJson e", Boolean.class ) + .getResultList(); + //end::hql-json-exists-passing-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + @SkipForDialect(dialectClass = MariaDBDialect.class, reason = "MariaDB reports the error 4038 as warning and simply returns null") + public void testOnError(SessionFactoryScope scope) { + scope.inSession( em -> { + try { + //tag::hql-json-exists-on-error-example[] + em.createQuery( "select json_exists('invalidJson', '$.theInt' error on error) from EntityWithJson e") + .getResultList(); + //end::hql-json-exists-on-error-example[] + fail("error clause should fail because of invalid json document"); + } + catch ( HibernateException e ) { + if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) { + throw e; + } + } + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectAggregateTest.java new file mode 100644 index 000000000000..7b1a9f276bd2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectAggregateTest.java @@ -0,0 +1,70 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import org.hibernate.cfg.QuerySettings; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.HANADialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +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.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonObjectAgg.class) +public class JsonObjectAggregateTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-objectagg-example[] + em.createQuery( "select json_objectagg(e.theString value e.id) from EntityOfBasics e" ).getResultList(); + //end::hql-json-objectagg-example[] + } ); + } + + @Test + public void testNull(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-objectagg-null-example[] + em.createQuery( "select json_objectagg(e.theString : e.id null on null) from EntityOfBasics e" ).getResultList(); + //end::hql-json-objectagg-null-example[] + } ); + } + + @Test + @SkipForDialect(dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "MySQL has no way to throw an error on duplicate json object keys. The last one always wins.") + @SkipForDialect(dialectClass = SQLServerDialect.class, reason = "SQL Server has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = DB2Dialect.class, reason = "DB2 has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 15, matchSubTypes = true, reason = "CockroachDB has no way to throw an error on duplicate json object keys.") + public void testUniqueKeys(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-objectagg-unique-keys-example[] + em.createQuery( "select json_objectagg(e.theString : e.id with unique keys) from EntityOfBasics e" ).getResultList(); + //end::hql-json-objectagg-unique-keys-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectTest.java new file mode 100644 index 000000000000..e5dfa2f4997d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectTest.java @@ -0,0 +1,54 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import org.hibernate.cfg.QuerySettings; + +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.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonObject.class) +public class JsonObjectTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-object-example[] + em.createQuery( "select json_object('key', 'value'), json_object(KEY 'key1' VALUE 'value1', 'key2' VALUE 'value2', 'key3': 'value3')" ).getResultList(); + //end::hql-json-object-example[] + } ); + } + + @Test + public void testNullClause(SessionFactoryScope scope) { + scope.inSession( em -> { + em.createQuery("select json_object('key': null null on null)" ).getResultList(); + } ); + } + + @Test + public void testAbsentOnNull(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-object-on-null-example[] + em.createQuery("select json_object('key': null absent on null)" ).getResultList(); + //end::hql-json-object-on-null-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonQueryTest.java new file mode 100644 index 000000000000..c9f7b301df03 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonQueryTest.java @@ -0,0 +1,138 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import java.util.HashMap; +import java.util.List; + +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.sql.exec.ExecutionException; + +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.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = EntityWithJson.class) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonQuery.class) +public class JsonQueryTest { + + @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-query-example[] + List results = em.createQuery( "select json_query(e.json, '$.theString') from EntityWithJson e", Tuple.class ) + .getResultList(); + //end::hql-json-query-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + public void testPassing(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-query-passing-example[] + List results = em.createQuery( "select json_query(e.json, '$.theArray[$idx]' passing 1 as idx) from EntityWithJson e", Tuple.class ) + .getResultList(); + //end::hql-json-query-passing-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + public void testWithWrapper(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-query-with-wrapper-example[] + List results = em.createQuery( "select json_query(e.json, '$.theInt' with wrapper) from EntityWithJson e", Tuple.class ) + .getResultList(); + //end::hql-json-query-with-wrapper-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + @SkipForDialect(dialectClass = MariaDBDialect.class, reason = "MariaDB reports the error 4038 as warning and simply returns null") + public void testOnError(SessionFactoryScope scope) { + scope.inSession( em -> { + try { + //tag::hql-json-query-on-error-example[] + em.createQuery( "select json_query('invalidJson', '$.theInt' error on error) from EntityWithJson e") + .getResultList(); + //end::hql-json-query-on-error-example[] + fail("error clause should fail because of invalid json document"); + } + catch ( HibernateException e ) { + if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) { + throw e; + } + } + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonValueErrorBehavior.class) + public void testOnEmpty(SessionFactoryScope scope) { + scope.inSession( em -> { + try { + //tag::hql-json-query-on-empty-example[] + em.createQuery("select json_query(e.json, '$.nonExisting' error on empty error on error) from EntityWithJson e" ) + .getResultList(); + //end::hql-json-query-on-empty-example[] + fail("empty clause should fail because of json path doesn't produce results"); + } + catch ( HibernateException e ) { + if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) { + throw e; + } + } + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java new file mode 100644 index 000000000000..231d983f1d0b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java @@ -0,0 +1,138 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import java.util.HashMap; +import java.util.List; + +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.sql.exec.ExecutionException; + +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.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = EntityWithJson.class) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonValue.class) +public class JsonValueTest { + + @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-value-example[] + List results = em.createQuery( "select json_value(e.json, '$.theString') from EntityWithJson e", Tuple.class ) + .getResultList(); + //end::hql-json-value-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + public void testPassing(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-value-passing-example[] + List results = em.createQuery( "select json_value(e.json, '$.theArray[$idx]' passing 1 as idx) from EntityWithJson e", Tuple.class ) + .getResultList(); + //end::hql-json-value-passing-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + public void testReturning(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-value-returning-example[] + List results = em.createQuery( "select json_value(e.json, '$.theInt' returning Integer) from EntityWithJson e", Tuple.class ) + .getResultList(); + //end::hql-json-value-returning-example[] + assertEquals( 1, results.size() ); + } ); + } + + @Test + @SkipForDialect(dialectClass = MariaDBDialect.class, reason = "MariaDB reports the error 4038 as warning and simply returns null") + public void testOnError(SessionFactoryScope scope) { + scope.inSession( em -> { + try { + //tag::hql-json-value-on-error-example[] + em.createQuery( "select json_value('invalidJson', '$.theInt' error on error) from EntityWithJson e") + .getResultList(); + //end::hql-json-value-on-error-example[] + fail("error clause should fail because of invalid json document"); + } + catch ( HibernateException e ) { + if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) { + throw e; + } + } + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonValueErrorBehavior.class) + public void testOnEmpty(SessionFactoryScope scope) { + scope.inSession( em -> { + try { + //tag::hql-json-value-on-empty-example[] + em.createQuery("select json_value(e.json, '$.nonExisting' error on empty error on error) from EntityWithJson e" ) + .getResultList(); + //end::hql-json-value-on-empty-example[] + fail("empty clause should fail because of json path doesn't produce results"); + } + catch ( HibernateException e ) { + if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) { + throw e; + } + } + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/lob/SerializableTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/lob/SerializableTypeTest.java index 9fc3cccbe037..2b967e7c8b8a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/lob/SerializableTypeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/lob/SerializableTypeTest.java @@ -30,7 +30,7 @@ public class SerializableTypeTest { @Test - @SkipForDialect(dialectClass = SybaseASEDialect.class, majorVersion = 15, matchSubTypes = true, reason = "HHH-6425") + @SkipForDialect(dialectClass = SybaseASEDialect.class, reason = "HHH-6425") public void testNewSerializableType(SessionFactoryScope scope) { final String initialPayloadText = "Initial payload"; final String changedPayloadText = "Changed payload"; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java index 43d05d985acc..bd516a38d039 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java @@ -30,7 +30,6 @@ @SkipForDialect(dialectClass = HSQLDialect.class) @SkipForDialect(dialectClass = DerbyDialect.class) @SkipForDialect(dialectClass = SybaseASEDialect.class) -@SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 10, matchSubTypes = true) @SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 11, matchSubTypes = true) // 'generated always' was added in 12 @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "generated always is not supported in Altibase") public class GeneratedAlwaysTest { 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 new file mode 100644 index 000000000000..3b904a1f2557 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -0,0 +1,543 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.hql; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.HANADialect; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.type.SqlTypes; + +import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; +import org.hibernate.testing.orm.junit.DialectContext; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +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.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BinaryNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.NullNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@DomainModel( annotatedClasses = { + JsonFunctionTests.JsonHolder.class, + EntityOfBasics.class +}) +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18496") +public class JsonFunctionTests { + + JsonHolder entity; + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + entity = new JsonHolder(); + entity.id = 1L; + entity.json = new HashMap<>(); + entity.json.put( "theInt", 1 ); + entity.json.put( "theFloat", 0.1 ); + entity.json.put( "theString", "abc" ); + entity.json.put( "theBoolean", true ); + entity.json.put( "theNull", null ); + entity.json.put( "theArray", new String[] { "a", "b", "c" } ); + entity.json.put( "theObject", new HashMap<>( entity.json ) ); + entity.json.put( + "theNestedObjects", + List.of( + Map.of( "id", 1, "name", "val1" ), + Map.of( "id", 2, "name", "val2" ), + Map.of( "id", 3, "name", "val3" ) + ) + ); + em.persist(entity); + + EntityOfBasics e1 = new EntityOfBasics(); + e1.setId( 1 ); + e1.setTheString( "Dog" ); + e1.setTheInteger( 0 ); + e1.setTheUuid( UUID.randomUUID() ); + EntityOfBasics e2 = new EntityOfBasics(); + e2.setId( 2 ); + e2.setTheString( "Cat" ); + e2.setTheInteger( 0 ); + + em.persist( e1 ); + em.persist( e2 ); + } + ); + } + + @AfterEach + public void cleanupData(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + em.createMutationQuery( "delete from EntityOfBasics" ).executeUpdate(); + em.createMutationQuery( "delete from JsonHolder" ).executeUpdate(); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonValue.class) + public void testJsonValue(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "json_value(e.json, '$.theInt'), " + + "json_value(e.json, '$.theFloat'), " + + "json_value(e.json, '$.theString'), " + + "json_value(e.json, '$.theBoolean'), " + + "json_value(e.json, '$.theNull'), " + + "json_value(e.json, '$.theArray'), " + + "json_value(e.json, '$.theArray[1]'), " + + "json_value(e.json, '$.theObject'), " + + "json_value(e.json, '$.theObject.theInt'), " + + "json_value(e.json, '$.theObject.theArray[2]') " + + "from JsonHolder e " + + "where e.id = 1L", + Tuple.class + ).getSingleResult(); + assertEquals( entity.json.get( "theInt" ).toString(), tuple.get( 0 ) ); + assertEquals( entity.json.get( "theFloat" ), Double.parseDouble( tuple.get( 1, String.class ) ) ); + assertEquals( entity.json.get( "theString" ), tuple.get( 2 ) ); + assertEquals( entity.json.get( "theBoolean" ).toString(), tuple.get( 3 ) ); + assertNull( tuple.get( 4 ) ); + // PostgreSQL emulation returns non-null value +// assertNull( tuple.get( 5 ) ); + assertEquals( ( (String[]) entity.json.get( "theArray" ) )[1], tuple.get( 6 ) ); + // PostgreSQL emulation returns non-null value +// assertNull( tuple.get( 7 ) ); + assertEquals( entity.json.get( "theInt" ).toString(), tuple.get( 8 ) ); + assertEquals( ( (String[]) entity.json.get( "theArray" ) )[2], tuple.get( 9 ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonValue.class) + public void testJsonValueExpression(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select json_value('{\"theArray\":[1,10]}', '$.theArray[$idx]' passing :idx as idx) ", + Tuple.class + ).setParameter( "idx", 0 ).getSingleResult(); + assertEquals( "1", tuple.get( 0 ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonQuery.class) + public void testJsonQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "json_query(e.json, '$.theArray'), " + + "json_query(e.json, '$.theNestedObjects'), " + + "json_query(e.json, '$.theNestedObjects[$idx]' passing :idx as idx with wrapper) " + + "from JsonHolder e " + + "where e.id = 1L", + Tuple.class + ).setParameter( "idx", 0 ).getSingleResult(); + assertEquals( parseJson( "[\"a\",\"b\",\"c\"]" ), parseJson( tuple.get( 0, String.class ) ) ); + assertEquals( + parseJson( + "[{\"id\":1,\"name\":\"val1\"},{\"id\":2,\"name\":\"val2\"},{\"id\":3,\"name\":\"val3\"}]" ), + parseJson( tuple.get( 1, String.class ) ) + ); + assertEquals( parseJson( "[{\"id\":1,\"name\":\"val1\"}]" ), parseJson( tuple.get( 2, String.class ) ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonQueryNestedPath.class) + public void testJsonQueryNested(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "json_query(e.json, '$.theNestedObjects[*].id' with wrapper) " + + "from JsonHolder e " + + "where e.id = 1L", + Tuple.class + ).getSingleResult(); + assertEquals( parseJson( "[1,2,3]" ), parseJson( tuple.get( 0, String.class ) ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArray.class) + public void testJsonArray(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "json_array(), " + + "json_array(1, 2, 3), " + + "json_array(0.1, 0.2, 0.3), " + + "json_array('a', 'b', 'c'), " + + "json_array(true, false, true), " + + "json_array(null, null null on null), " + + "json_array(json_array(), json_array(1), json_array('a'), null null on null)", + Tuple.class + ).getSingleResult(); + assertEquals( "[]", tuple.get( 0 ) ); + assertArrayEquals( new Integer[] { 1, 2, 3 }, parseArray( tuple.get( 1 ).toString() ) ); + assertArrayEquals( new Double[] { 0.1, 0.2, 0.3 }, parseArray( tuple.get( 2 ).toString() ) ); + assertArrayEquals( new String[] { "a", "b", "c" }, parseArray( tuple.get( 3 ).toString() ) ); + assertArrayEquals( new Boolean[] { true, false, true }, parseArray( tuple.get( 4 ).toString() ) ); + assertArrayEquals( new Object[] { null, null }, parseArray( tuple.get( 5 ).toString() ) ); + assertArrayEquals( + new Object[] { List.of(), List.of( 1 ), List.of( "a" ), null }, + parseArray( tuple.get( 6 ).toString() ) + ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonObject.class) + public void testJsonObject(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_object(" + + "'theInt', 1, " + + "'theFloat', 0.1, " + + "'theString', 'abc', " + + "'theBoolean', true, " + + "'theNull', null, " + + "'theObject', json_object(" + + " 'theInt', 1, " + + " 'theFloat', 0.1, " + + " 'theString', 'abc', " + + " 'theBoolean', true, " + + " 'theNull', null " + + " absent on null" + + ")) ", + String.class + ).getSingleResult(); + Map map = parseObject( json ); + assertEquals( entity.json.get( "theInt" ).toString(), map.get( "theInt" ).toString() ); + assertEquals( entity.json.get( "theFloat" ), Double.parseDouble( map.get( "theFloat" ).toString() ) ); + assertEquals( entity.json.get( "theString" ), map.get( "theString" ) ); + assertEquals( entity.json.get( "theBoolean" ), map.get( "theBoolean" ) ); + assertTrue( map.containsKey( "theNull" ) ); + assertNull( map.get( "theNull" ) ); + Map nested = (Map) map.get( "theObject" ); + assertEquals( entity.json.get( "theInt" ).toString(), nested.get( "theInt" ).toString() ); + assertEquals( entity.json.get( "theFloat" ), Double.parseDouble( nested.get( "theFloat" ).toString() ) ); + assertEquals( entity.json.get( "theString" ), nested.get( "theString" ) ); + assertEquals( entity.json.get( "theBoolean" ), nested.get( "theBoolean" ) ); + // HSQLDB bug: https://sourceforge.net/p/hsqldb/bugs/1720/ + if ( !( DialectContext.getDialect() instanceof HSQLDialect ) ) { + assertFalse( nested.containsKey( "theNull" ) ); + } + assertNull( nested.get( "theNull" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonObject.class) + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArray.class) + public void testJsonObjectAndArray(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select json_object(" + + "'a', json_array( 1, 2, 3 ), " + + "'b', json_object(" + + " 'c', json_array( 4, 5, 6 ) " + + ")), " + + "json_array(json_object('a', 1), json_object('b', 'c'), json_object('c', null))", + Tuple.class + ).getSingleResult(); + Map map = parseObject( tuple.get( 0 ).toString() ); + assertEquals( List.of( 1,2,3 ), map.get( "a" ) ); + assertInstanceOf( Map.class, map.get( "b" ) ); + Map nested = (Map) map.get( "b" ); + assertEquals( List.of( 4, 5, 6 ), nested.get( "c" ) ); + + Object[] array = parseArray( tuple.get( 1 ).toString() ); + assertEquals( 3, array.length ); + assertInstanceOf( Map.class, array[0] ); + assertInstanceOf( Map.class, array[1] ); + assertInstanceOf( Map.class, array[2] ); + assertEquals( 1, ( (Map) array[0] ).get( "a" ) ); + assertEquals( "c", ( (Map) array[1] ).get( "b" ) ); + Map nested2 = (Map) array[2]; + assertTrue( nested2.containsKey( "c" ) ); + assertNull( nested2.get( "c" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonExists.class) + @SkipForDialect(dialectClass = OracleDialect.class, majorVersion = 21, matchSubTypes = true, reason = "Oracle bug in versions before 23") + public void testJsonExists(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Tuple tuple = session.createQuery( + "select " + + "json_exists(e.json, '$.theUnknown'), " + + "json_exists(e.json, '$.theInt'), " + + "json_exists(e.json, '$.theArray[0]'), " + + "json_exists(e.json, '$.theArray[$idx]' passing :idx as idx) " + + "from JsonHolder e " + + "where e.id = 1L", + Tuple.class + ).setParameter( "idx", 3 ).getSingleResult(); + assertEquals( false, tuple.get( 0 ) ); + assertEquals( true, tuple.get( 1 ) ); + assertEquals( true, tuple.get( 2 ) ); + assertEquals( false, tuple.get( 3 ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayAgg.class) + public void testJsonArrayAgg(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String jsonArray = session.createQuery( + "select json_arrayagg(e.theString) " + + "from EntityOfBasics e", + String.class + ).getSingleResult(); + Object[] array = parseArray( jsonArray ); + assertEquals( 2, array.length ); + assertTrue( Arrays.asList( array ).contains( "Cat" ) ); + assertTrue( Arrays.asList( array ).contains( "Dog" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayAgg.class) + public void testJsonArrayAggOrderBy(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String jsonArray = session.createQuery( + "select json_arrayagg(e.theString order by e.theString)" + + "from EntityOfBasics e", + String.class + ).getSingleResult(); + Object[] array = parseArray( jsonArray ); + assertArrayEquals( new Object[]{ "Cat", "Dog" }, array ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonObjectAgg.class) + public void testJsonObjectAgg(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String jsonArray = session.createQuery( + "select json_objectagg(e.theString value e.id) " + + "from EntityOfBasics e", + String.class + ).getSingleResult(); + Map object = parseObject( jsonArray ); + assertEquals( 2, object.size() ); + assertEquals( 1, object.get( "Dog" ) ); + assertEquals( 2, object.get( "Cat" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonObjectAgg.class) + public void testJsonObjectAggNullFilter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String jsonArray = session.createQuery( + "select json_objectagg(e.theString value e.theUuid) " + + "from EntityOfBasics e", + String.class + ).getSingleResult(); + Map object = parseObject( jsonArray ); + assertEquals( 1, object.size() ); + assertTrue( object.containsKey( "Dog" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonObjectAgg.class) + public void testJsonObjectAggNullClause(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String jsonArray = session.createQuery( + "select json_objectagg(e.theString value e.theUuid null on null) " + + "from EntityOfBasics e", + String.class + ).getSingleResult(); + Map object = parseObject( jsonArray ); + assertEquals( 2, object.size() ); + assertNotNull( object.get( "Dog" ) ); + assertNull( object.get( "Cat" ) ); + assertTrue( object.containsKey( "Cat" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonObjectAgg.class) + @SkipForDialect(dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "MySQL has no way to throw an error on duplicate json object keys. The last one always wins.") + @SkipForDialect(dialectClass = SQLServerDialect.class, reason = "SQL Server has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = DB2Dialect.class, reason = "DB2 has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB has no way to throw an error on duplicate json object keys.") + @SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 15, matchSubTypes = true, reason = "CockroachDB has no way to throw an error on duplicate json object keys.") + public void testJsonObjectAggUniqueKeys(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + try { + session.createQuery( + "select json_objectagg(str(e.theInteger) value e.theString with unique keys) " + + "from EntityOfBasics e", + String.class + ).getSingleResult(); + fail("Should fail because keys are not unique"); + } + catch (HibernateException e) { + assertInstanceOf( JDBCException.class, e ); + } + } + ); + } + + 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 ); + } + } + + private static Object[] parseArray(String json) { + try { + return MAPPER.readValue( json, Object[].class ); + } + catch (JsonProcessingException e) { + throw new RuntimeException( e ); + } + } + + private static Object parseJson(String json) { + try { + return toJavaNode( MAPPER.readTree( json ) ); + } + catch (JsonProcessingException e) { + throw new RuntimeException( e ); + } + } + + private static Object toJavaNode(JsonNode jsonNode) { + if ( jsonNode instanceof ArrayNode arrayNode ) { + final var list = new ArrayList<>( arrayNode.size() ); + for ( JsonNode node : arrayNode ) { + list.add( toJavaNode( node ) ); + } + return list; + } + else if ( jsonNode instanceof ObjectNode object ) { + final var map = new HashMap<>( object.size() ); + final Iterator> iter = object.fields(); + while ( iter.hasNext() ) { + final Map.Entry entry = iter.next(); + map.put( entry.getKey(), toJavaNode( entry.getValue() ) ); + } + return map; + } + else if ( jsonNode instanceof NullNode ) { + return null; + } + else if ( jsonNode instanceof NumericNode numericNode ) { + return numericNode.numberValue(); + } + else if ( jsonNode instanceof BooleanNode booleanNode ) { + return booleanNode.booleanValue(); + } + else if ( jsonNode instanceof TextNode textNode ) { + return textNode.textValue(); + } + else if ( jsonNode instanceof BinaryNode binaryNode ) { + return binaryNode.binaryValue(); + } + else { + throw new UnsupportedOperationException( "Unsupported node type: " + jsonNode.getClass().getName() ); + } + } + + @Entity(name = "JsonHolder") + public static class JsonHolder { + @Id + Long id; + @JdbcTypeCode(SqlTypes.JSON) + Map json; + } +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java index e4794c9120b2..a6c812733dd5 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java @@ -32,8 +32,8 @@ import org.hibernate.envers.query.criteria.internal.CriteriaTools; import org.hibernate.envers.query.order.NullPrecedence; import org.hibernate.envers.tools.Pair; +import org.hibernate.internal.util.QuotingHelper; import org.hibernate.query.Query; -import org.hibernate.query.internal.QueryLiteralHelper; import org.hibernate.type.BasicType; /** @@ -367,10 +367,10 @@ else if ( !orderFragments.isEmpty() ) { final Pair fragment = fragmentIterator.next(); sb.append( OrderByFragmentFunction.FUNCTION_NAME ).append( '(' ); // The first argument is the sqm alias of the from node - QueryLiteralHelper.appendStringLiteral( sb, fragment.getFirst() ); + QuotingHelper.appendSingleQuoteEscapedString( sb, fragment.getFirst() ); sb.append( ", " ); // The second argument is the collection role that contains the order by fragment - QueryLiteralHelper.appendStringLiteral( sb, fragment.getSecond() ); + QuotingHelper.appendSingleQuoteEscapedString( sb, fragment.getSecond() ); sb.append( ')' ); if ( fragmentIterator.hasNext() ) { sb.append( ", " ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java b/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java index 507a8a36c2e1..350dd785ee24 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java @@ -327,6 +327,8 @@ protected Ignore convertSkipToIgnore(FrameworkMethod frameworkMethod) { effectiveSkipForDialect.microVersion(), dialect, effectiveSkipForDialect.matchSubTypes() + ? DialectFilterExtension.VersionMatchMode.SAME_OR_OLDER + : DialectFilterExtension.VersionMatchMode.SAME ); if ( versionsMatch ) { @@ -479,6 +481,8 @@ private boolean isDialectMatchingRequired2(Collection FUNCTION_REGISTRIES = new HashMap<>(); + + public static boolean definesFunction(Dialect dialect, String functionName) { + SqmFunctionRegistry sqmFunctionRegistry = FUNCTION_REGISTRIES.get( dialect ); + if ( sqmFunctionRegistry == null ) { + final TypeConfiguration typeConfiguration = new TypeConfiguration(); + final SqmFunctionRegistry functionRegistry = new SqmFunctionRegistry(); + typeConfiguration.scope( new FakeMetadataBuildingContext( typeConfiguration, functionRegistry ) ); + final FakeTypeContributions typeContributions = new FakeTypeContributions( typeConfiguration ); + final FakeFunctionContributions functionContributions = new FakeFunctionContributions( + dialect, + typeConfiguration, + functionRegistry + ); + dialect.contribute( typeContributions, typeConfiguration.getServiceRegistry() ); + dialect.initializeFunctionRegistry( functionContributions ); + FUNCTION_REGISTRIES.put( dialect, sqmFunctionRegistry = functionContributions.functionRegistry ); + } + return sqmFunctionRegistry.findFunctionDescriptor( functionName ) != null; + } + + private static class FakeTypeContributions implements TypeContributions { + private final TypeConfiguration typeConfiguration; + + public FakeTypeContributions(TypeConfiguration typeConfiguration) { + this.typeConfiguration = typeConfiguration; + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return typeConfiguration; + } + } + + private static class FakeFunctionContributions implements FunctionContributions { + private final Dialect dialect; + private final TypeConfiguration typeConfiguration; + private final SqmFunctionRegistry functionRegistry; + + public FakeFunctionContributions(Dialect dialect, TypeConfiguration typeConfiguration, SqmFunctionRegistry functionRegistry) { + this.dialect = dialect; + this.typeConfiguration = typeConfiguration; + this.functionRegistry = functionRegistry; + } + + @Override + public Dialect getDialect() { + return dialect; + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return typeConfiguration; + } + + @Override + public SqmFunctionRegistry getFunctionRegistry() { + return functionRegistry; + } + + @Override + public ServiceRegistry getServiceRegistry() { + return null; + } + } + + public static class FakeMetadataBuildingContext implements MetadataBuildingContext, InFlightMetadataCollector { + + private final TypeConfiguration typeConfiguration; + private final SqmFunctionRegistry functionRegistry; + private final MetadataBuilderImpl.MetadataBuildingOptionsImpl options; + private final BootstrapContextImpl bootstrapContext; + private final Database database; + + public FakeMetadataBuildingContext(TypeConfiguration typeConfiguration, SqmFunctionRegistry functionRegistry) { + this.typeConfiguration = typeConfiguration; + this.functionRegistry = functionRegistry; + this.bootstrapContext = new BootstrapContextImpl(); + this.options = new MetadataBuilderImpl.MetadataBuildingOptionsImpl( bootstrapContext.getServiceRegistry() ); + this.options.setBootstrapContext( bootstrapContext ); + this.database = new Database( options, null ); + } + + @Override + public BootstrapContext getBootstrapContext() { + return bootstrapContext; + } + + @Override + public MetadataBuildingOptions getBuildingOptions() { + return options; + } + + @Override + public Database getDatabase() { + return database; + } + + @Override + public MetadataBuildingOptions getMetadataBuildingOptions() { + return options; + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return typeConfiguration; + } + + @Override + public SqmFunctionRegistry getFunctionRegistry() { + return functionRegistry; + } + + // The rest are no-ops + + @Override + public EffectiveMappingDefaults getEffectiveDefaults() { + return null; + } + + @Override + public InFlightMetadataCollector getMetadataCollector() { + return this; + } + + @Override + public ObjectNameNormalizer getObjectNameNormalizer() { + return null; + } + + @Override + public TypeDefinitionRegistry getTypeDefinitionRegistry() { + return null; + } + + @Override + public String getCurrentContributorName() { + return ""; + } + + @Override + public SourceModelBuildingContext getSourceModelBuildingContext() { + return null; + } + + @Override + public GlobalRegistrations getGlobalRegistrations() { + return null; + } + + @Override + public PersistenceUnitMetadata getPersistenceUnitMetadata() { + return null; + } + + @Override + public void addEntityBinding(PersistentClass persistentClass) throws DuplicateMappingException { + + } + + @Override + public Map getEntityBindingMap() { + return Map.of(); + } + + @Override + public void registerComponent(Component component) { + + } + + @Override + public void registerGenericComponent(Component component) { + + } + + @Override + public void registerEmbeddableSubclass(ClassDetails superclass, ClassDetails subclass) { + + } + + @Override + public List getEmbeddableSubclasses(ClassDetails superclass) { + return List.of(); + } + + @Override + public void addImport(String importName, String className) throws DuplicateMappingException { + + } + + @Override + public void addCollectionBinding(Collection collection) throws DuplicateMappingException { + + } + + @Override + public Table addTable( + String schema, + String catalog, + String name, + String subselect, + boolean isAbstract, + MetadataBuildingContext buildingContext) { + return null; + } + + @Override + public Table addDenormalizedTable( + String schema, + String catalog, + String name, + boolean isAbstract, + String subselect, + Table includedTable, + MetadataBuildingContext buildingContext) throws DuplicateMappingException { + return null; + } + + @Override + public void addNamedQuery(NamedHqlQueryDefinition query) throws DuplicateMappingException { + + } + + @Override + public void addNamedNativeQuery(NamedNativeQueryDefinition query) throws DuplicateMappingException { + + } + + @Override + public void addResultSetMapping(NamedResultSetMappingDescriptor resultSetMappingDefinition) + throws DuplicateMappingException { + + } + + @Override + public void addNamedProcedureCallDefinition(NamedProcedureCallDefinition definition) + throws DuplicateMappingException { + + } + + @Override + public void addNamedEntityGraph(NamedEntityGraphDefinition namedEntityGraphDefinition) { + + } + + @Override + public void addTypeDefinition(TypeDefinition typeDefinition) { + + } + + @Override + public void addFilterDefinition(FilterDefinition definition) { + + } + + @Override + public void addAuxiliaryDatabaseObject(AuxiliaryDatabaseObject auxiliaryDatabaseObject) { + + } + + @Override + public void addFetchProfile(FetchProfile profile) { + + } + + @Override + public void addIdentifierGenerator(IdentifierGeneratorDefinition generatorDefinition) { + + } + + @Override + public ConverterRegistry getConverterRegistry() { + return null; + } + + @Override + public void addAttributeConverter(ConverterDescriptor descriptor) { + + } + + @Override + public void addAttributeConverter(Class> converterClass) { + + } + + @Override + public void addRegisteredConversion(RegisteredConversion conversion) { + + } + + @Override + public ConverterAutoApplyHandler getAttributeConverterAutoApplyHandler() { + return null; + } + + @Override + public void addSecondPass(SecondPass secondPass) { + + } + + @Override + public void addSecondPass(SecondPass sp, boolean onTopOfTheQueue) { + + } + + @Override + public void addTableNameBinding(Identifier logicalName, Table table) { + + } + + @Override + public void addTableNameBinding( + String schema, + String catalog, + String logicalName, + String realTableName, + Table denormalizedSuperTable) { + + } + + @Override + public String getLogicalTableName(Table ownerTable) { + return ""; + } + + @Override + public String getPhysicalTableName(Identifier logicalName) { + return ""; + } + + @Override + public String getPhysicalTableName(String logicalName) { + return ""; + } + + @Override + public void addColumnNameBinding(Table table, Identifier logicalColumnName, Column column) { + + } + + @Override + public void addColumnNameBinding(Table table, String logicalColumnName, Column column) { + + } + + @Override + public String getPhysicalColumnName(Table table, Identifier logicalName) throws MappingException { + return ""; + } + + @Override + public String getPhysicalColumnName(Table table, String logicalName) throws MappingException { + return ""; + } + + @Override + public String getLogicalColumnName(Table table, Identifier physicalName) { + return ""; + } + + @Override + public String getLogicalColumnName(Table table, String physicalName) { + return ""; + } + + @Override + public void addDefaultIdentifierGenerator(IdentifierGeneratorDefinition generatorDefinition) { + + } + + @Override + public void addDefaultQuery(NamedHqlQueryDefinition queryDefinition) { + + } + + @Override + public void addDefaultNamedNativeQuery(NamedNativeQueryDefinition query) { + + } + + @Override + public void addDefaultResultSetMapping(NamedResultSetMappingDescriptor definition) { + + } + + @Override + public void addDefaultNamedProcedureCall(NamedProcedureCallDefinitionImpl procedureCallDefinition) { + + } + + @Override + public AnnotatedClassType addClassType(ClassDetails classDetails) { + return null; + } + + @Override + public AnnotatedClassType getClassType(ClassDetails classDetails) { + return null; + } + + @Override + public void addMappedSuperclass(Class type, MappedSuperclass mappedSuperclass) { + + } + + @Override + public MappedSuperclass getMappedSuperclass(Class type) { + return null; + } + + @Override + public PropertyData getPropertyAnnotatedWithMapsId(ClassDetails persistentClassDetails, String propertyName) { + return null; + } + + @Override + public void addPropertyAnnotatedWithMapsId( + ClassDetails entityClassDetails, + PropertyData propertyAnnotatedElement) { + + } + + @Override + public void addPropertyAnnotatedWithMapsIdSpecj( + ClassDetails entityClassDetails, + PropertyData specJPropertyData, + String s) { + + } + + @Override + public void addToOneAndIdProperty(ClassDetails entityClassDetails, PropertyData propertyAnnotatedElement) { + + } + + @Override + public PropertyData getPropertyAnnotatedWithIdAndToOne( + ClassDetails persistentClassDetails, + String propertyName) { + return null; + } + + @Override + public boolean isInSecondPass() { + return false; + } + + @Override + public NaturalIdUniqueKeyBinder locateNaturalIdUniqueKeyBinder(String entityName) { + return null; + } + + @Override + public void registerNaturalIdUniqueKeyBinder(String entityName, NaturalIdUniqueKeyBinder ukBinder) { + + } + + @Override + public void registerValueMappingResolver(Function resolver) { + + } + + @Override + public void addJavaTypeRegistration(Class javaType, JavaType jtd) { + + } + + @Override + public void addJdbcTypeRegistration(int typeCode, JdbcType jdbcType) { + + } + + @Override + public void registerEmbeddableInstantiator( + Class embeddableType, + Class instantiator) { + + } + + @Override + public Class findRegisteredEmbeddableInstantiator(Class embeddableType) { + return null; + } + + @Override + public void registerCompositeUserType(Class embeddableType, Class> userType) { + + } + + @Override + public Class> findRegisteredCompositeUserType(Class embeddableType) { + return null; + } + + @Override + public void registerUserType(Class embeddableType, Class> userType) { + + } + + @Override + public Class> findRegisteredUserType(Class basicType) { + return null; + } + + @Override + public void addCollectionTypeRegistration(CollectionTypeRegistration registrationAnnotation) { + + } + + @Override + public void addCollectionTypeRegistration( + CollectionClassification classification, + CollectionTypeRegistrationDescriptor descriptor) { + + } + + @Override + public CollectionTypeRegistrationDescriptor findCollectionTypeRegistration(CollectionClassification classification) { + return null; + } + + @Override + public void addDelayedPropertyReferenceHandler(DelayedPropertyReferenceHandler handler) { + + } + + @Override + public void addPropertyReference(String entityName, String propertyName) { + + } + + @Override + public void addUniquePropertyReference(String entityName, String propertyName) { + + } + + @Override + public void addPropertyReferencedAssociation(String s, String propertyName, String syntheticPropertyName) { + + } + + @Override + public String getPropertyReferencedAssociation(String entityName, String mappedBy) { + return ""; + } + + @Override + public void addMappedBy(String name, String mappedBy, String propertyName) { + + } + + @Override + public String getFromMappedBy(String ownerEntityName, String propertyName) { + return ""; + } + + @Override + public EntityTableXref getEntityTableXref(String entityName) { + return null; + } + + @Override + public EntityTableXref addEntityTableXref( + String entityName, + Identifier primaryTableLogicalName, + Table primaryTable, + EntityTableXref superEntityTableXref) { + return null; + } + + @Override + public Map getJoins(String entityName) { + return Map.of(); + } + + @Override + public SessionFactoryBuilder getSessionFactoryBuilder() { + return null; + } + + @Override + public SessionFactory buildSessionFactory() { + return null; + } + + @Override + public UUID getUUID() { + return null; + } + + @Override + public java.util.Collection getEntityBindings() { + return List.of(); + } + + @Override + public PersistentClass getEntityBinding(String entityName) { + return null; + } + + @Override + public java.util.Collection getCollectionBindings() { + return List.of(); + } + + @Override + public Collection getCollectionBinding(String role) { + return null; + } + + @Override + public Map getImports() { + return Map.of(); + } + + @Override + public NamedHqlQueryDefinition getNamedHqlQueryMapping(String name) { + return null; + } + + @Override + public void visitNamedHqlQueryDefinitions(Consumer> definitionConsumer) { + + } + + @Override + public NamedNativeQueryDefinition getNamedNativeQueryMapping(String name) { + return null; + } + + @Override + public void visitNamedNativeQueryDefinitions(Consumer> definitionConsumer) { + + } + + @Override + public NamedProcedureCallDefinition getNamedProcedureCallMapping(String name) { + return null; + } + + @Override + public void visitNamedProcedureCallDefinition(Consumer definitionConsumer) { + + } + + @Override + public NamedResultSetMappingDescriptor getResultSetMapping(String name) { + return null; + } + + @Override + public void visitNamedResultSetMappingDefinition(Consumer definitionConsumer) { + + } + + @Override + public TypeDefinition getTypeDefinition(String typeName) { + return null; + } + + @Override + public Map getFilterDefinitions() { + return Map.of(); + } + + @Override + public FilterDefinition getFilterDefinition(String name) { + return null; + } + + @Override + public FetchProfile getFetchProfile(String name) { + return null; + } + + @Override + public java.util.Collection getFetchProfiles() { + return List.of(); + } + + @Override + public NamedEntityGraphDefinition getNamedEntityGraph(String name) { + return null; + } + + @Override + public Map getNamedEntityGraphs() { + return Map.of(); + } + + @Override + public IdentifierGeneratorDefinition getIdentifierGenerator(String name) { + return null; + } + + @Override + public java.util.Collection collectTableMappings() { + return List.of(); + } + + @Override + public Map getSqlFunctionMap() { + return Map.of(); + } + + @Override + public Set getContributors() { + return Set.of(); + } + + @Override + public NamedObjectRepository buildNamedQueryRepository() { + return null; + } + + @Override + public void orderColumns(boolean forceOrdering) { + + } + + @Override + public void validate() throws MappingException { + + } + + @Override + public Set getMappedSuperclassMappingsCopy() { + return Set.of(); + } + + @Override + public void initSessionFactory(SessionFactoryImplementor sessionFactoryImplementor) { + + } + + @Override + public void visitRegisteredComponents(Consumer consumer) { + + } + + @Override + public Component getGenericComponent(Class componentClass) { + return null; + } + + @Override + public DiscriminatorType resolveEmbeddableDiscriminatorType( + Class embeddableClass, + Supplier> supplier) { + return null; + } + + @Override + public Type getIdentifierType(String className) throws MappingException { + return null; + } + + @Override + public String getIdentifierPropertyName(String className) throws MappingException { + return ""; + } + + @Override + public Type getReferencedPropertyType(String className, String propertyName) throws MappingException { + return null; + } + } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java index fb857ac3810c..864a39f5dac2 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java @@ -86,6 +86,8 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con matchingMicroVersion, dialect, requiresDialect.matchSubTypes() + ? VersionMatchMode.SAME_OR_NEWER + : VersionMatchMode.SAME ); } else { @@ -138,6 +140,21 @@ public static boolean versionsMatch( int matchingMicroVersion, Dialect dialect, boolean matchNewerVersions) { + return versionsMatch( + matchingMajorVersion, + matchingMinorVersion, + matchingMicroVersion, + dialect, + matchNewerVersions ? VersionMatchMode.SAME_OR_NEWER : VersionMatchMode.SAME + ); + } + + public static boolean versionsMatch( + int matchingMajorVersion, + int matchingMinorVersion, + int matchingMicroVersion, + Dialect dialect, + VersionMatchMode matchMode) { if ( matchingMajorVersion < 0 ) { return false; } @@ -150,12 +167,20 @@ public static boolean versionsMatch( matchingMicroVersion = 0; } - if ( matchNewerVersions ) { + if ( matchMode == VersionMatchMode.SAME_OR_NEWER ) { return dialect.getVersion().isSameOrAfter( matchingMajorVersion, matchingMinorVersion, matchingMicroVersion ); } - else { - return dialect.getVersion().isSame( matchingMajorVersion ); + if ( matchMode == VersionMatchMode.SAME_OR_OLDER + && dialect.getVersion().isBefore( matchingMajorVersion, matchingMinorVersion, matchingMicroVersion ) ) { + return true; } + return dialect.getVersion().isSame( matchingMajorVersion ); + } + + public enum VersionMatchMode { + SAME, + SAME_OR_NEWER, + SAME_OR_OLDER } private ConditionEvaluationResult evaluateSkipConditions(ExtensionContext context, Dialect dialect, String enabledResult) { @@ -196,6 +221,8 @@ private ConditionEvaluationResult evaluateSkipConditions(ExtensionContext contex effectiveSkipForDialect.microVersion(), dialect, effectiveSkipForDialect.matchSubTypes() + ? VersionMatchMode.SAME_OR_OLDER + : VersionMatchMode.SAME ); if ( versionsMatch ) {