From c63bd6f658674a120676619f922e41c1a558e815 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Sat, 17 Aug 2024 03:11:51 +0200 Subject: [PATCH 01/15] HHH-18496 Add json_value function --- .../chapters/query/hql/QueryLanguage.adoc | 80 ++ .../query/hql/extras/json_value_bnf.txt | 7 + .../dialect/CockroachLegacyDialect.java | 2 + .../community/dialect/DB2LegacyDialect.java | 4 + .../community/dialect/H2LegacyDialect.java | 4 + .../dialect/MariaDBLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 4 + .../dialect/OracleLegacyDialect.java | 4 + .../dialect/PostgreSQLLegacyDialect.java | 7 + .../dialect/SQLServerLegacyDialect.java | 3 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 2 + .../org/hibernate/grammars/hql/HqlParser.g4 | 17 + .../hibernate/dialect/CockroachDialect.java | 2 + .../org/hibernate/dialect/DB2Dialect.java | 4 + .../java/org/hibernate/dialect/H2Dialect.java | 4 + .../org/hibernate/dialect/HANADialect.java | 5 + .../org/hibernate/dialect/MariaDBDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 2 + .../org/hibernate/dialect/OracleDialect.java | 2 + .../hibernate/dialect/PostgreSQLDialect.java | 7 + .../hibernate/dialect/SQLServerDialect.java | 3 + .../function/CommonFunctionFactory.java | 63 ++ .../json/CastTargetReturnTypeResolver.java | 79 ++ .../json/CockroachDBJsonValueFunction.java | 93 ++ .../function/json/H2JsonValueFunction.java | 118 +++ .../dialect/function/json/JsonPathHelper.java | 61 ++ .../function/json/JsonValueFunction.java | 176 ++++ .../json/MariaDBJsonValueFunction.java | 53 ++ .../function/json/MySQLJsonValueFunction.java | 54 ++ .../json/PostgreSQLJsonValueFunction.java | 72 ++ .../json/SQLServerJsonValueFunction.java | 58 ++ .../criteria/HibernateCriteriaBuilder.java | 32 + .../criteria/JpaJsonValueExpression.java | 142 +++ .../spi/HibernateCriteriaBuilderDelegate.java | 31 + .../hql/internal/SemanticQueryBuilder.java | 40 + .../org/hibernate/query/sqm/NodeBuilder.java | 16 + .../sqm/internal/SqmCriteriaNodeBuilder.java | 42 + .../function/ArgumentTypesValidator.java | 4 + .../function/FunctionParameterType.java | 12 + .../expression/SqmJsonValueExpression.java | 287 ++++++ .../hibernate/sql/ast/SqlAstTranslator.java | 7 + .../sql/ast/spi/AbstractSqlAstTranslator.java | 5 + .../expression/JsonValueEmptyBehavior.java | 40 + .../expression/JsonValueErrorBehavior.java | 40 + .../java/org/hibernate/type/SqlTypes.java | 32 + .../type/descriptor/jdbc/JdbcType.java | 10 + .../hibernate/type/spi/TypeConfiguration.java | 2 + .../test/function/json/EntityWithJson.java | 40 + .../orm/test/function/json/JsonValueTest.java | 123 +++ .../orm/test/query/hql/JsonFunctionTests.java | 101 ++ .../orm/junit/DialectFeatureChecks.java | 862 +++++++++++++++++- 51 files changed, 2859 insertions(+), 1 deletion(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueEmptyBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonValueErrorBehavior.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/EntityWithJson.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index eee71c608528..61f531857391 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,85 @@ 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. + +[[hql-json-functions]] +|=== +| Function | Purpose + +| `json_value()` | Extracts a value from a JSON document by JSON path +|=== + +[[hql-json-value-function]] +===== `json_value()` + +Extracts a value by https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path] from a JSON document. + +[[hql-like-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. + +NOTE: It is recommended to only us the dot notation for JSON paths, 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 `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 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-error-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-on-error-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-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-user-defined-functions]] ==== Native and user-defined functions 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..b5ea83bf623e --- /dev/null +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt @@ -0,0 +1,7 @@ +"json_value(" expression, expression ("returning" castTarget)? onErrorClause? onEmptyClause? ")" + +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 ead1ea777801..b28540964e05 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 @@ -492,6 +492,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_cockroachdb(); functionFactory.arrayToString_postgresql(); + functionFactory.jsonValue_cockroachdb(); + // 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 e1cc4046878a..c2e18fc4034e 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 @@ -428,6 +428,10 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); if ( getDB2Version().isSameOrAfter( 9, 5 ) ) { functionFactory.listagg( null ); + + if ( getDB2Version().isSameOrAfter( 11 ) ) { + functionFactory.jsonValue(); + } } } 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 361661f62c3c..21d981c97f8a 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 @@ -397,6 +397,10 @@ 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(); + } } else { // Use group_concat until 2.x as listagg was buggy 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 0dbc1f986b6a..00d92fff92eb 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 @@ -89,6 +89,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio .getBasicTypeRegistry() .resolve( StandardBasicTypes.BOOLEAN ) ); + commonFunctionFactory.jsonValue_mariadb(); if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) 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 77e6710fa1ab..e19bf2599ebe 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 @@ -651,6 +651,10 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionRegistry.registerAlternateKey( "char", "chr" ); functionFactory.listagg_groupConcat(); + + if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) { + functionFactory.jsonValue_mysql(); + } } @Override 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 6302f3c8c98e..53e8c1d1c21f 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 @@ -318,6 +318,10 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayTrim_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); + + if ( getVersion().isSameOrAfter( 12 ) ) { + functionFactory.jsonValue_literal_path(); + } } @Override 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 ec88b5a78623..719b909efa59 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 @@ -618,6 +618,13 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_postgresql(); functionFactory.arrayToString_postgresql(); + if ( getVersion().isSameOrAfter( 17 ) ) { + functionFactory.jsonValue(); + } + else { + functionFactory.jsonValue_postgresql(); + } + if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 a5363f4fdb94..c847e77aa07d 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 @@ -400,6 +400,9 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); functionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); + if ( getVersion().isSameOrAfter( 13 ) ) { + functionFactory.jsonValue_sqlserver(); + } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); } 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..d72da023b69c 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 @@ -221,6 +221,7 @@ 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_VALUE : [jJ] [sS] [oO] [nN] '_' [vV] [aA] [lL] [uU] [eE]; KEY : [kK] [eE] [yY]; KEYS : [kK] [eE] [yY] [sS]; LAST : [lL] [aA] [sS] [tT]; @@ -277,6 +278,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]; 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..493cda12efb6 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 + | jsonValueFunction | genericFunction ; @@ -1620,6 +1621,20 @@ rollup : ROLLUP LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)* RIGHT_PAREN ; +/** + * The 'json_value()' function + */ +jsonValueFunction + : JSON_VALUE LEFT_PAREN expression COMMA expression jsonValueReturningClause? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? RIGHT_PAREN + ; + +jsonValueReturningClause + : RETURNING castTarget + ; + +jsonValueOnErrorOrEmptyClause + : ( ERROR | NULL | ( DEFAULT expression ) ) ON (ERROR|EMPTY); + /** * Support for "soft" keywords which may be used as identifiers * @@ -1714,6 +1729,7 @@ rollup | INTO | IS | JOIN + | JSON_VALUE | KEY | KEYS | LAST @@ -1771,6 +1787,7 @@ rollup | QUARTER | RANGE | RESPECT + | RETURNING // | RIGHT | ROLLUP | ROW 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 0af831ba5a04..f3fa3ae93be4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -463,6 +463,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_cockroachdb(); functionFactory.arrayToString_postgresql(); + functionFactory.jsonValue_cockroachdb(); + // 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 c414465e7962..ee840694488b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -414,6 +414,10 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); functionFactory.listagg( null ); + + if ( getDB2Version().isSameOrAfter( 11 ) ) { + functionFactory.jsonValue(); + } } @Override 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 d74c6d36e968..db3887b1a0b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -338,6 +338,10 @@ 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(); + } } /** 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 71fad2b71c89..c3fa966c45c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -488,6 +488,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio ANY, ANY, ANY, typeConfiguration ); + + if ( getVersion().isSameOrAfter(2, 0, 20) ) { + // Introduced in 2.0 SPS 02 + functionFactory.jsonValue(); + } } @Override 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 6eb4b6252db6..440d6dbceb0f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -92,6 +92,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio .getBasicTypeRegistry() .resolve( StandardBasicTypes.BOOLEAN ) ); + commonFunctionFactory.jsonValue_mariadb(); commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) .setInvariantType( functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) ) 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 698669d86128..d0ccf1f5760e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -632,6 +632,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } functionFactory.listagg_groupConcat(); + + functionFactory.jsonValue_mysql(); } @Override 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 74508089fde9..d7c2c581d181 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -386,6 +386,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayTrim_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); + + functionFactory.jsonValue_literal_path(); } @Override 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 e656257d2c7b..f0334383bf7f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -580,6 +580,13 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_postgresql(); functionFactory.arrayToString_postgresql(); + if ( getVersion().isSameOrAfter( 17 ) ) { + functionFactory.jsonValue(); + } + else { + functionFactory.jsonValue_postgresql(); + } + functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions functionFactory.inverseDistributionOrderedSetAggregates(); 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 4935b4137929..1d9ee31dadca 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -418,6 +418,9 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.windowFunctions(); functionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); + if ( getVersion().isSameOrAfter( 13 ) ) { + functionFactory.jsonValue_sqlserver(); + } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); } 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 eed38c929c8e..f13f0b3055d6 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 @@ -76,6 +76,13 @@ 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.CockroachDBJsonValueFunction; +import org.hibernate.dialect.function.json.H2JsonValueFunction; +import org.hibernate.dialect.function.json.JsonValueFunction; +import org.hibernate.dialect.function.json.MariaDBJsonValueFunction; +import org.hibernate.dialect.function.json.MySQLJsonValueFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; +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; @@ -3320,4 +3327,60 @@ 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 ) ); + } + + /** + * json_value() function that supports only literal json paths + */ + public void jsonValue_literal_path() { + functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, 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 ) ); + } } 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/CockroachDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java new file mode 100644 index 000000000000..ec61a58062e6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java @@ -0,0 +1,93 @@ +/* + * 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.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +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; + +/** + * CockroachDB json_value function. + */ +public class CockroachDBJsonValueFunction extends JsonValueFunction { + + public CockroachDBJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, 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 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 { + 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/H2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java new file mode 100644 index 000000000000..9ded156b7ff2 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -0,0 +1,118 @@ +/* + * 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.Expression; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * H2 json_value function. + */ +public class H2JsonValueFunction extends JsonValueFunction { + + public H2JsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false ); + } + + @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(" ); + } + renderJsonPath( sqlAppender, arguments.jsonDocument(), walker, jsonPath ); + 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( ')' ); + } + } + + private void renderJsonPath( + SqlAppender sqlAppender, + SqlAstNode jsonDocument, + SqlAstTranslator walker, + String jsonPath) { + sqlAppender.appendSql( "cast(" ); + + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute; + if ( needsWrapping ) { + sqlAppender.appendSql( '(' ); + } + jsonDocument.accept( walker ); + if ( needsWrapping ) { + 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(); + appendInDoubleQuotes( sqlAppender, attributeName ); + } + else { + sqlAppender.appendSql( '[' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) jsonPathElement ).index() + 1 ); + sqlAppender.appendSql( ']' ); + } + } + sqlAppender.appendSql( " as varchar)" ); + } + + private static void appendInDoubleQuotes(SqlAppender sqlAppender, String attributeName) { + sqlAppender.appendSql( ".\"" ); + for ( int j = 0; j < attributeName.length(); j++ ) { + final char c = attributeName.charAt( j ); + if ( c == '"' ) { + sqlAppender.appendSql( '"' ); + } + sqlAppender.appendSql( c ); + } + sqlAppender.appendSql( '"' ); + } +} 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..714cf2fd4f46 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.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 java.util.ArrayList; +import java.util.List; + +import org.hibernate.QueryException; + +public class JsonPathHelper { + + public static List parseJsonPathElements(String jsonPath) { + if ( jsonPath.charAt( 0 ) != '$' || jsonPath.charAt( 1 ) != '.' ) { + 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 = 2; + int dotIndex; + + try { + while ( ( dotIndex = jsonPath.indexOf( '.', startIndex ) ) != -1 ) { + parseAttribute( jsonPath, startIndex, dotIndex, jsonPathElements ); + startIndex = dotIndex + 1; + } + parseAttribute( jsonPath, startIndex, jsonPath.length(), jsonPathElements ); + } + catch (Exception ex) { + throw new QueryException( "Can't emulate non-simple json path expression: " + jsonPath, ex ); + } + return jsonPathElements; + } + + 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 index = Integer.parseInt( jsonPath, bracketStartIndex + 1, bracketEndIndex, 10 ); + jsonPathElements.add( new JsonIndexAccess( index ) ); + } + + public sealed interface JsonPathElement {} + public record JsonAttribute(String attribute) implements JsonPathElement {} + public record JsonIndexAccess(int index) implements JsonPathElement {} +} 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..14145141610b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java @@ -0,0 +1,176 @@ +/* + * 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.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.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; + + public JsonValueFunction(TypeConfiguration typeConfiguration, boolean supportsJsonPathExpression) { + super( + "json_value", + FunctionKind.NORMAL, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 5 ), IMPLICIT_JSON, STRING, ANY, ANY, ANY ) + ), + new CastTargetReturnTypeResolver( typeConfiguration ), + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) + ); + this.supportsJsonPathExpression = supportsJsonPathExpression; + } + + @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( ',' ); + if ( supportsJsonPathExpression ) { + arguments.jsonPath().accept( walker ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + + 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 CastTarget returningType, + @Nullable JsonValueErrorBehavior errorBehavior, + @Nullable JsonValueEmptyBehavior emptyBehavior) { + public static JsonValueArguments extract(List sqlAstArguments) { + int nextIndex = 2; + 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 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(), + castTarget, + errorBehavior, + emptyBehavior + ); + } + } +} 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..fbea1833868d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java @@ -0,0 +1,53 @@ +/* + * 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.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 ); + } + + @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( "," ); + arguments.jsonPath().accept( 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/MySQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java new file mode 100644 index 000000000000..2586d6d6d633 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.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.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +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 ); + } + + @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( "," ); + arguments.jsonPath().accept( 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/PostgreSQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java new file mode 100644 index 000000000000..acbf1d2e0d5d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java @@ -0,0 +1,72 @@ +/* + * 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.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +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 ); + } + + @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)" ); + } + // 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/SQLServerJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java new file mode 100644 index 000000000000..ec57b30944fc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java @@ -0,0 +1,58 @@ +/* + * 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.Expression; +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 ); + } + + @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( "varchar(max)" ); + } + sqlAppender.appendSql( ' ' ); + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + "strict " + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + else { + arguments.jsonPath().accept( walker ); + } + sqlAppender.appendSql( "))" ); + } +} 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 e411d9827917..42614533446d 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 @@ -3680,6 +3680,38 @@ 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); @Override JpaPredicate and(List restrictions); 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..b35c2a9772c6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java @@ -0,0 +1,142 @@ +/* + * 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); + + /** + * 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 108998808cef..70e9ccdb7c6e 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 @@ -38,6 +38,7 @@ import org.hibernate.query.criteria.JpaFunction; import org.hibernate.query.criteria.JpaInPredicate; import org.hibernate.query.criteria.JpaJoin; +import org.hibernate.query.criteria.JpaJsonValueExpression; import org.hibernate.query.criteria.JpaListJoin; import org.hibernate.query.criteria.JpaMapJoin; import org.hibernate.query.criteria.JpaOrder; @@ -3343,4 +3344,34 @@ 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 ); + } } 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 0c4b86a2c27f..b6f53b5359db 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 @@ -143,6 +143,7 @@ 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.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; @@ -2691,6 +2692,45 @@ public SqmPredicate visitContainsPredicate(HqlParser.ContainsPredicateContext ct return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); } + @Override + public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContext 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 ) ); + } + } + } + return jsonValue; + } + @Override public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { final boolean negated = ctx.NOT() != null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 6ad255e81abb..eac8efeeebc6 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 @@ -44,6 +44,7 @@ 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.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; @@ -613,6 +614,21 @@ 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); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides 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 c3b742123dd6..70c6322c2df8 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 @@ -121,6 +121,7 @@ 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.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; @@ -5290,4 +5291,45 @@ 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 + ); + } + } } 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 1764e642cc58..8115f635e2d3 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 @@ -248,6 +248,10 @@ private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcTyp case DATE -> jdbcType.hasDatePart(); case TIME -> jdbcType.hasTimePart(); case SPATIAL -> jdbcType.isSpatial(); + case JSON: + return jdbcType.isJson(); + case IMPLICIT_JSON: + return 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 24381e8bfb37..750e719343e8 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 @@ -86,6 +86,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/tree/expression/SqmJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java new file mode 100644 index 000000000000..637e8f5a3538 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java @@ -0,0 +1,287 @@ +/* + * 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 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.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.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +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 SelfRenderingSqmFunction implements JpaJsonValueExpression { + private @Nullable ErrorBehavior errorBehavior; + private SqmExpression errorDefaultExpression; + private @Nullable EmptyBehavior emptyBehavior; + 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 ErrorBehavior errorBehavior, + SqmExpression errorDefaultExpression, + @Nullable EmptyBehavior emptyBehavior, + SqmExpression emptyDefaultExpression) { + super( + descriptor, + renderer, + arguments, + impliedResultType, + argumentsValidator, + returnTypeResolver, + nodeBuilder, + name + ); + 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(), + 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 JpaJsonValueExpression unspecifiedOnError() { + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + this.errorDefaultExpression = null; + return this; + } + + @Override + public JpaJsonValueExpression errorOnError() { + this.errorBehavior = ErrorBehavior.ERROR; + this.errorDefaultExpression = null; + return this; + } + + @Override + public JpaJsonValueExpression nullOnError() { + this.errorBehavior = ErrorBehavior.NULL; + this.errorDefaultExpression = null; + return this; + } + + @Override + public JpaJsonValueExpression defaultOnError(jakarta.persistence.criteria.Expression expression) { + this.errorBehavior = ErrorBehavior.DEFAULT; + //noinspection unchecked + this.errorDefaultExpression = (SqmExpression) expression; + return this; + } + + @Override + public JpaJsonValueExpression unspecifiedOnEmpty() { + this.errorBehavior = ErrorBehavior.UNSPECIFIED; + this.errorDefaultExpression = null; + return this; + } + + @Override + public JpaJsonValueExpression errorOnEmpty() { + this.emptyBehavior = EmptyBehavior.ERROR; + this.emptyDefaultExpression = null; + return this; + } + + @Override + public JpaJsonValueExpression nullOnEmpty() { + this.emptyBehavior = EmptyBehavior.NULL; + this.emptyDefaultExpression = null; + return this; + } + + @Override + public JpaJsonValueExpression defaultOnEmpty(jakarta.persistence.criteria.Expression expression) { + this.emptyBehavior = EmptyBehavior.DEFAULT; + //noinspection unchecked + this.emptyDefaultExpression = (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() ); + } + if ( errorBehavior != null ) { + switch ( errorBehavior ) { + case NULL: + arguments.add( JsonValueErrorBehavior.NULL ); + break; + case ERROR: + arguments.add( JsonValueErrorBehavior.ERROR ); + break; + case DEFAULT: + arguments.add( JsonValueErrorBehavior.defaultOnError( + (Expression) errorDefaultExpression.accept( walker ) + ) ); + break; + } + } + if ( emptyBehavior != null ) { + switch ( emptyBehavior ) { + case NULL: + arguments.add( JsonValueEmptyBehavior.NULL ); + break; + case ERROR: + arguments.add( JsonValueEmptyBehavior.ERROR ); + break; + case DEFAULT: + arguments.add( JsonValueEmptyBehavior.defaultOnEmpty( + (Expression) emptyDefaultExpression.accept( walker ) + ) ); + break; + } + } + 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 ); + + if ( getArguments().size() > 2 ) { + sb.append( " returning " ); + getArguments().get( 2 ).appendHqlString( sb ); + } + switch ( errorBehavior ) { + case NULL: + sb.append( " null on error" ); + break; + case ERROR: + sb.append( " error on error" ); + break; + case DEFAULT: + sb.append( " default " ); + errorDefaultExpression.appendHqlString( sb ); + sb.append( " on error" ); + break; + } + switch ( emptyBehavior ) { + case NULL: + sb.append( " null on empty" ); + break; + case ERROR: + sb.append( " error on empty" ); + break; + case DEFAULT: + sb.append( " default " ); + emptyDefaultExpression.appendHqlString( sb ); + sb.append( " on empty" ); + break; + } + sb.append( ')' ); + } +} 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 e9e65e76f2aa..76a4812a5c22 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 @@ -10,6 +10,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; @@ -21,6 +22,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 4119f82ac23d..45c31e486b84 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 @@ -658,6 +658,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 ) { 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 a056505eea4b..d3b3ab441ec6 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java @@ -989,4 +989,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 e081f95a0fc5..2206c4040670 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 @@ -425,6 +425,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/spi/TypeConfiguration.java b/hibernate-core/src/main/java/org/hibernate/type/spi/TypeConfiguration.java index 11dac8173e26..32df8ad1430f 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 @@ -350,6 +350,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/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/JsonValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java new file mode 100644 index 000000000000..c532b025fe69 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java @@ -0,0 +1,123 @@ +/* + * 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.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.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.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 +@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 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 path"); + } + 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/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java new file mode 100644 index 000000000000..c63adc9db18f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.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.orm.test.query.hql; + +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +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.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = JsonFunctionTests.JsonHolder.class) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18496") +public class JsonFunctionTests { + + JsonHolder entity; + + @BeforeAll + 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 ) ); + em.persist(entity); + } + ); + } + + @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 ) ); + } + ); + } + + @Entity(name = "JsonHolder") + public static class JsonHolder { + @Id + Long id; + @JdbcTypeCode(SqlTypes.JSON) + Map json; + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 1cf7dcd827e8..a5fddf6493f0 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -5,8 +5,52 @@ package org.hibernate.testing.orm.junit; import java.sql.Types; - +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.hibernate.DuplicateMappingException; +import org.hibernate.MappingException; +import org.hibernate.SessionFactory; +import org.hibernate.annotations.CollectionTypeRegistration; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.internal.MetadataBuilderImpl; +import org.hibernate.boot.internal.NamedProcedureCallDefinitionImpl; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.IdentifierGeneratorDefinition; +import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.model.TruthValue; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.TypeDefinition; +import org.hibernate.boot.model.TypeDefinitionRegistry; +import org.hibernate.boot.model.convert.spi.ConverterAutoApplyHandler; +import org.hibernate.boot.model.convert.spi.ConverterDescriptor; +import org.hibernate.boot.model.convert.spi.ConverterRegistry; +import org.hibernate.boot.model.convert.spi.RegisteredConversion; +import org.hibernate.boot.model.internal.AnnotatedClassType; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ObjectNameNormalizer; +import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.models.spi.GlobalRegistrations; +import org.hibernate.boot.models.xml.spi.PersistenceUnitMetadata; +import org.hibernate.boot.query.NamedHqlQueryDefinition; +import org.hibernate.boot.query.NamedNativeQueryDefinition; +import org.hibernate.boot.query.NamedProcedureCallDefinition; +import org.hibernate.boot.query.NamedResultSetMappingDescriptor; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.boot.spi.EffectiveMappingDefaults; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.boot.spi.MetadataBuildingOptions; +import org.hibernate.boot.spi.NaturalIdUniqueKeyBinder; +import org.hibernate.boot.spi.PropertyData; +import org.hibernate.boot.spi.SecondPass; import org.hibernate.community.dialect.FirebirdDialect; import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.CockroachDialect; @@ -27,11 +71,39 @@ import org.hibernate.dialect.SybaseDriverKind; import org.hibernate.dialect.TiDBDialect; import org.hibernate.dialect.TimeZoneSupport; +import org.hibernate.engine.spi.FilterDefinition; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.mapping.AggregateColumn; +import org.hibernate.mapping.Collection; import org.hibernate.mapping.Column; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.FetchProfile; +import org.hibernate.mapping.Join; +import org.hibernate.mapping.MappedSuperclass; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Table; +import org.hibernate.metamodel.CollectionClassification; +import org.hibernate.metamodel.mapping.DiscriminatorType; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; +import org.hibernate.models.spi.ClassDetails; +import org.hibernate.models.spi.SourceModelBuildingContext; +import org.hibernate.query.named.NamedObjectRepository; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; import org.hibernate.type.SqlTypes; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.usertype.CompositeUserType; +import org.hibernate.usertype.UserType; + +import org.hibernate.testing.boot.BootstrapContextImpl; + +import jakarta.persistence.AttributeConverter; /** * Container class for different implementation of the {@link DialectFeatureCheck} interface. @@ -654,6 +726,26 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonValue implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_value" ); + } + } + + public static class SupportsJsonValueErrorBehavior implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_value" ) + // H2 emulation doesn't support error behavior + && !( dialect instanceof H2Dialect ) + // MariaDB simply doesn't support the on error and on empty clauses + && !( dialect instanceof MariaDBDialect ) + // Cockroach doesn't have a native json_value function + && !( dialect instanceof CockroachDialect ) + // PostgreSQL added support for native json_value in version 17 + && ( !( dialect instanceof PostgreSQLDialect ) || dialect.getVersion().isSameOrAfter( 17 ) ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; @@ -697,4 +789,772 @@ public boolean apply(Dialect dialect) { return dialect.getNationalizationSupport() == NationalizationSupport.EXPLICIT; } } + + private static final HashMap 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(SessionFactoryImplementor sessionFactory) { + 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; + } + } } From 3f86c7578031c62d38d8fa544f6caf5f427357ef Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 19 Aug 2024 20:01:14 +0200 Subject: [PATCH 02/15] HHH-18496 Add json_object and json_array functions --- .../chapters/query/hql/QueryLanguage.adoc | 72 +++++++- .../query/hql/extras/json_array_bnf.txt | 5 + .../query/hql/extras/json_object_bnf.txt | 15 ++ .../dialect/CockroachLegacyDialect.java | 11 ++ .../community/dialect/DB2LegacyDialect.java | 2 + .../community/dialect/H2LegacyDialect.java | 6 + .../community/dialect/HSQLLegacyDialect.java | 5 + .../dialect/MariaDBLegacyDialect.java | 3 + .../community/dialect/MySQLLegacyDialect.java | 3 + .../dialect/OracleLegacyDialect.java | 7 + .../dialect/PostgreSQLLegacyDialect.java | 19 ++ .../dialect/SQLServerLegacyDialect.java | 2 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 3 + .../org/hibernate/grammars/hql/HqlParser.g4 | 36 +++- .../process/spi/MetadataBuildingProcess.java | 2 + ...stractPostgreSQLJsonArrayPGObjectType.java | 106 +++++++++++ .../hibernate/dialect/CockroachDialect.java | 7 + .../org/hibernate/dialect/DB2Dialect.java | 2 + .../java/org/hibernate/dialect/H2Dialect.java | 5 + .../dialect/H2JsonArrayJdbcType.java | 55 ++++++ .../org/hibernate/dialect/H2JsonJdbcType.java | 2 +- .../org/hibernate/dialect/HANADialect.java | 5 + .../org/hibernate/dialect/HSQLDialect.java | 5 + .../org/hibernate/dialect/MariaDBDialect.java | 3 + .../MySQLCastingJsonArrayJdbcType.java | 30 ++++ .../org/hibernate/dialect/MySQLDialect.java | 8 + .../dialect/OracleBooleanJdbcType.java | 6 + .../org/hibernate/dialect/OracleDialect.java | 21 ++- .../org/hibernate/dialect/PgJdbcHelper.java | 8 + .../PostgreSQLCastingJsonArrayJdbcType.java | 41 +++++ .../hibernate/dialect/PostgreSQLDialect.java | 15 ++ .../PostgreSQLJsonArrayPGObjectJsonType.java | 16 ++ .../PostgreSQLJsonArrayPGObjectJsonbType.java | 16 ++ .../hibernate/dialect/SQLServerDialect.java | 2 + .../function/CommonFunctionFactory.java | 136 +++++++++++++++ .../function/json/DB2JsonArrayFunction.java | 30 ++++ .../function/json/DB2JsonObjectFunction.java | 30 ++++ .../function/json/ExpressionTypeHelper.java | 61 +++++++ .../function/json/HANAJsonArrayFunction.java | 159 +++++++++++++++++ .../function/json/HANAJsonObjectFunction.java | 149 ++++++++++++++++ .../function/json/HSQLJsonArrayFunction.java | 33 ++++ .../function/json/HSQLJsonObjectFunction.java | 33 ++++ .../function/json/JsonArrayFunction.java | 77 ++++++++ .../json/JsonObjectArgumentsValidator.java | 112 ++++++++++++ .../function/json/JsonObjectFunction.java | 88 ++++++++++ .../dialect/function/json/JsonPathHelper.java | 35 ++-- .../json/MariaDBJsonArrayFunction.java | 73 ++++++++ .../function/json/MySQLJsonArrayFunction.java | 79 +++++++++ .../json/MySQLJsonObjectFunction.java | 75 ++++++++ .../json/OracleJsonArrayFunction.java | 54 ++++++ .../json/OracleJsonObjectFunction.java | 54 ++++++ .../json/PostgreSQLJsonArrayFunction.java | 83 +++++++++ .../json/PostgreSQLJsonObjectFunction.java | 84 +++++++++ .../json/SQLServerJsonArrayFunction.java | 34 ++++ .../json/SQLServerJsonObjectFunction.java | 34 ++++ .../hibernate/internal/util/NumberHelper.java | 63 +++++++ .../criteria/HibernateCriteriaBuilder.java | 32 ++++ .../spi/HibernateCriteriaBuilderDelegate.java | 24 +++ .../hql/internal/SemanticQueryBuilder.java | 57 ++++++ .../org/hibernate/query/sqm/NodeBuilder.java | 12 ++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 54 ++++++ .../function/ArgumentTypesValidator.java | 5 +- .../tree/expression/SqmJsonNullBehavior.java | 63 +++++++ .../expression/SqmJsonValueExpression.java | 52 +++--- .../ast/tree/expression/JsonNullBehavior.java | 24 +++ .../jdbc/JsonArrayAsStringJdbcType.java | 164 ++++++++++++++++++ .../descriptor/jdbc/JsonArrayJdbcType.java | 113 ++++++++++++ .../dialect/function/JsonPathHelperTest.java | 89 ++++++++++ .../orm/test/function/json/JsonArrayTest.java | 49 ++++++ .../test/function/json/JsonObjectTest.java | 49 ++++++ .../orm/test/query/hql/JsonFunctionTests.java | 155 +++++++++++++++++ .../orm/junit/DialectFeatureChecks.java | 12 ++ 72 files changed, 3029 insertions(+), 45 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_array_bnf.txt create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_object_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectArgumentsValidator.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/internal/util/NumberHelper.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonNullBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonNullBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/dialect/function/JsonPathHelperTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 61f531857391..4821b60eb72f 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1629,15 +1629,81 @@ The following functions deal with SQL JSON types, which are not supported on eve |=== | 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 |=== + +[[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 value by https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path] from a JSON document. +Extracts a scalar value by https://www.ietf.org/archive/id/draft-goessner-dispatch-jsonpath-00.html[JSON path] from a JSON document. -[[hql-like-json-value-bnf]] +[[hql-json-value-bnf]] [source, antlrv4, indent=0] ---- include::{extrasdir}/json_value_bnf.txt[] @@ -1645,6 +1711,8 @@ 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 return non-scalar values. Beware that this behavior is not portable. + NOTE: It is recommended to only us the dot notation for JSON paths, since most databases support only that. [[hql-json-value-example]] 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_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/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 b28540964e05..24ac28d5280b 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 @@ -91,6 +91,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; @@ -261,9 +262,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 ) ); @@ -368,9 +371,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 { @@ -378,9 +383,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 ); } } } @@ -390,9 +397,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 ); } } @@ -493,6 +502,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_postgresql(); functionFactory.jsonValue_cockroachdb(); + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index c2e18fc4034e..d6c085a75a68 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 @@ -431,6 +431,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getDB2Version().isSameOrAfter( 11 ) ) { functionFactory.jsonValue(); + functionFactory.jsonObject_db2(); + functionFactory.jsonArray_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 21d981c97f8a..2a42abaf2319 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 @@ -96,6 +96,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; @@ -263,6 +264,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 ) ); @@ -293,6 +295,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 ); @@ -403,6 +406,9 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } } 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 30cb4f8cc5bc..1b958ec16d52 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 @@ -270,6 +270,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_hsql(); functionFactory.arrayToString_hsql(); + if ( getVersion().isSameOrAfter( 2, 7 ) ) { + functionFactory.jsonObject_hsqldb(); + functionFactory.jsonArray_hsqldb(); + } + //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 00d92fff92eb..10702701bba8 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java @@ -29,6 +29,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; @@ -90,6 +91,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio .resolve( StandardBasicTypes.BOOLEAN ) ); commonFunctionFactory.jsonValue_mariadb(); + commonFunctionFactory.jsonArray_mariadb(); if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) @@ -139,6 +141,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 e19bf2599ebe..c66160b66b11 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 @@ -654,6 +654,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) { functionFactory.jsonValue_mysql(); + functionFactory.jsonObject_mysql(); + functionFactory.jsonArray_mysql(); } } @@ -665,6 +667,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 53e8c1d1c21f..f0f1ce6d69f8 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 @@ -125,6 +125,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; @@ -133,6 +134,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; @@ -321,6 +323,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 12 ) ) { functionFactory.jsonValue_literal_path(); + functionFactory.jsonObject_oracle(); + functionFactory.jsonArray_oracle(); } } @@ -660,6 +664,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)"; @@ -718,9 +723,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 ) ); } } 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 719b909efa59..d3f0878c57d9 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 @@ -107,6 +107,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; @@ -256,9 +257,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,9 +623,19 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 17 ) ) { functionFactory.jsonValue(); + functionFactory.jsonObject(); + functionFactory.jsonArray(); } else { functionFactory.jsonValue_postgresql(); + if ( getVersion().isSameOrAfter( 16 ) ) { + functionFactory.jsonObject(); + functionFactory.jsonArray(); + } + else { + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); + } } if ( getVersion().isSameOrAfter( 9, 4 ) ) { @@ -1406,17 +1419,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 ); } } } @@ -1432,9 +1449,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 c847e77aa07d..b4306548298c 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,6 +402,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); + functionFactory.jsonObject_sqlserver(); + functionFactory.jsonArray_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 d72da023b69c..d5e8d404f080 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,6 +145,7 @@ 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]; @@ -221,6 +222,8 @@ 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_OBJECT : [jJ] [sS] [oO] [nN] '_' [oO] [bB] [jJ] [eE] [cC] [tT]; JSON_VALUE : [jJ] [sS] [oO] [nN] '_' [vV] [aA] [lL] [uU] [eE]; KEY : [kK] [eE] [yY]; KEYS : [kK] [eE] [yY] [sS]; 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 493cda12efb6..a8caeb932f4b 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,7 +1109,7 @@ function | collectionFunctionMisuse | jpaNonstandardFunction | columnFunction - | jsonValueFunction + | jsonFunction | genericFunction ; @@ -1621,6 +1621,12 @@ rollup : ROLLUP LEFT_PAREN expressionOrPredicate (COMMA expressionOrPredicate)* RIGHT_PAREN ; +jsonFunction + : jsonValueFunction + | jsonArrayFunction + | jsonObjectFunction + ; + /** * The 'json_value()' function */ @@ -1635,6 +1641,29 @@ jsonValueReturningClause jsonValueOnErrorOrEmptyClause : ( ERROR | NULL | ( DEFAULT expression ) ) ON (ERROR|EMPTY); +/** + * 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 + ; + /** * Support for "soft" keywords which may be used as identifiers * @@ -1651,7 +1680,8 @@ jsonValueOnErrorOrEmptyClause nakedIdentifier : IDENTIFIER | QUOTED_IDENTIFIER - | (ALL + | (ABSENT + | ALL | AND | ANY | AS @@ -1729,6 +1759,8 @@ jsonValueOnErrorOrEmptyClause | INTO | IS | JOIN + | JSON_ARRAY + | JSON_OBJECT | JSON_VALUE | KEY | KEYS 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 aea51bfc29d6..9cd882fea2f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/process/spi/MetadataBuildingProcess.java @@ -85,6 +85,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; @@ -823,6 +824,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/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 f3fa3ae93be4..3632998ce401 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -94,6 +94,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; @@ -258,6 +259,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 ) ); @@ -353,11 +355,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 { @@ -365,6 +369,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 @@ -464,6 +469,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_postgresql(); functionFactory.jsonValue_cockroachdb(); + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index ee840694488b..a7b01adeed74 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -417,6 +417,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getDB2Version().isSameOrAfter( 11 ) ) { functionFactory.jsonValue(); + functionFactory.jsonObject_db2(); + functionFactory.jsonArray_db2(); } } 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 db3887b1a0b2..5d6defa48cd9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -88,6 +88,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; @@ -227,6 +228,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 ) ); } @@ -243,6 +245,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 ); } @@ -339,6 +342,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_h2(); functionFactory.arrayToString_h2( getMaximumArraySize() ); + functionFactory.jsonObject(); + functionFactory.jsonArray(); if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { functionFactory.jsonValue_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 c6a70e312f39..b40b88efbef8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonJdbcType.java @@ -33,7 +33,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 c3fa966c45c4..3949caa71572 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -492,6 +492,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter(2, 0, 20) ) { // Introduced in 2.0 SPS 02 functionFactory.jsonValue(); + if ( getVersion().isSameOrAfter(2, 0, 40) ) { + // Introduced in 2.0 SPS 04 + functionFactory.jsonObject_hana(); + functionFactory.jsonArray_hana(); + } } } 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 5ff07f9d2a77..7b83750a31f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -205,6 +205,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_hsql(); functionFactory.arrayToString_hsql(); + if ( getVersion().isSameOrAfter( 2, 7 ) ) { + functionFactory.jsonObject_hsqldb(); + functionFactory.jsonArray_hsqldb(); + } + //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 440d6dbceb0f..d39ec3df9c76 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -30,6 +30,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; @@ -93,6 +94,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio .resolve( StandardBasicTypes.BOOLEAN ) ); commonFunctionFactory.jsonValue_mariadb(); + commonFunctionFactory.jsonArray_mariadb(); commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) .setInvariantType( functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) ) @@ -146,6 +148,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 d0ccf1f5760e..17040732e050 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -287,6 +287,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,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.listagg_groupConcat(); functionFactory.jsonValue_mysql(); + functionFactory.jsonObject_mysql(); + functionFactory.jsonArray_mysql(); } @Override @@ -643,6 +650,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 8a315de8f064..bee4f3ec68d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleBooleanJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleBooleanJdbcType.java @@ -4,6 +4,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; @@ -12,6 +13,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 d7c2c581d181..65f927d379cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -123,6 +123,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; @@ -131,6 +132,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; @@ -278,6 +280,16 @@ public int getPreferredSqlTypeCodeForBoolean() { 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); @@ -388,6 +400,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_oracle(); functionFactory.jsonValue_literal_path(); + functionFactory.jsonObject_oracle(); + functionFactory.jsonArray_oracle(); } @Override @@ -721,9 +735,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: @@ -777,9 +790,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 { ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "blob", this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON_ARRAY, "blob", this ) ); } ddlTypeRegistry.addDescriptor( new ArrayDdlTypeImpl( this, false ) ); 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 746af3e85342..20f8aa88bd0e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PgJdbcHelper.java @@ -52,6 +52,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 f0334383bf7f..fa59da4de2a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.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,6 +259,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,9 +584,19 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 17 ) ) { functionFactory.jsonValue(); + functionFactory.jsonObject(); + functionFactory.jsonArray(); } else { functionFactory.jsonValue_postgresql(); + if ( getVersion().isSameOrAfter( 16 ) ) { + functionFactory.jsonObject(); + functionFactory.jsonArray(); + } + else { + functionFactory.jsonObject_postgresql(); + functionFactory.jsonArray_postgresql(); + } } functionFactory.makeDateTimeTimestamp(); @@ -1362,12 +1374,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 { @@ -1375,6 +1389,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 1d9ee31dadca..1e7d8cb19154 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -420,6 +420,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); + functionFactory.jsonObject_sqlserver(); + functionFactory.jsonArray_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 f13f0b3055d6..7cde6bb7b444 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 @@ -77,11 +77,28 @@ import org.hibernate.dialect.function.array.PostgreSQLArrayPositionsFunction; import org.hibernate.dialect.function.array.PostgreSQLArrayTrimEmulation; import org.hibernate.dialect.function.json.CockroachDBJsonValueFunction; +import org.hibernate.dialect.function.json.DB2JsonArrayFunction; +import org.hibernate.dialect.function.json.DB2JsonObjectFunction; import org.hibernate.dialect.function.json.H2JsonValueFunction; +import org.hibernate.dialect.function.json.HANAJsonArrayFunction; +import org.hibernate.dialect.function.json.HANAJsonObjectFunction; +import org.hibernate.dialect.function.json.HSQLJsonArrayFunction; +import org.hibernate.dialect.function.json.HSQLJsonObjectFunction; +import org.hibernate.dialect.function.json.JsonArrayFunction; +import org.hibernate.dialect.function.json.JsonObjectFunction; import org.hibernate.dialect.function.json.JsonValueFunction; +import org.hibernate.dialect.function.json.MariaDBJsonArrayFunction; import org.hibernate.dialect.function.json.MariaDBJsonValueFunction; +import org.hibernate.dialect.function.json.MySQLJsonArrayFunction; +import org.hibernate.dialect.function.json.MySQLJsonObjectFunction; import org.hibernate.dialect.function.json.MySQLJsonValueFunction; +import org.hibernate.dialect.function.json.OracleJsonArrayFunction; +import org.hibernate.dialect.function.json.OracleJsonObjectFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; +import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; +import org.hibernate.dialect.function.json.SQLServerJsonObjectFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; @@ -3383,4 +3400,123 @@ public void jsonValue_sqlserver() { public void jsonValue_h2() { functionRegistry.register( "json_value", new H2JsonValueFunction( 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 ) ); + } } 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/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/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) { + // (? 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.appendSql( '"' ); + for ( int j = 0; j < literalValue.length(); j++ ) { + final char c = literalValue.charAt( j ); + if ( c == '"' ) { + sqlAppender.appendSql( '"' ); + } + sqlAppender.appendSql( c ); + } + sqlAppender.appendSql( '"' ); + 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) { + // (? 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/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/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 index 714cf2fd4f46..0eaa449524aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -14,22 +14,35 @@ public class JsonPathHelper { public static List parseJsonPathElements(String jsonPath) { - if ( jsonPath.charAt( 0 ) != '$' || jsonPath.charAt( 1 ) != '.' ) { - throw new QueryException( "Json path expression expression emulation only supports absolute paths i.e. must start with a '$.' but got: " + 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 = 2; + int startIndex; int dotIndex; - try { - while ( ( dotIndex = jsonPath.indexOf( '.', startIndex ) ) != -1 ) { - parseAttribute( jsonPath, startIndex, dotIndex, jsonPathElements ); - startIndex = dotIndex + 1; + 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 ); } - parseAttribute( jsonPath, startIndex, jsonPath.length(), jsonPathElements ); - } - catch (Exception ex) { - throw new QueryException( "Can't emulate non-simple json path expression: " + jsonPath, ex ); } return jsonPathElements; } 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/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/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/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/PostgreSQLJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayFunction.java new file mode 100644 index 000000000000..153288b19b2c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayFunction.java @@ -0,0 +1,83 @@ +/* + * 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 ); + 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/PostgreSQLJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectFunction.java new file mode 100644 index 000000000000..90b4c8779038 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectFunction.java @@ -0,0 +1,84 @@ +/* + * 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 ); + 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/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/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/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/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 42614533446d..5f99a2a4acdd 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 @@ -3713,6 +3713,38 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @Incubating JpaJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath, Class returningType); + /** + * 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); + @Override JpaPredicate and(List restrictions); 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 70e9ccdb7c6e..fd08ae1f2125 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 @@ -3374,4 +3374,28 @@ public JpaJsonValueExpression jsonValue( Class returningType) { return criteriaBuilder.jsonValue( jsonDocument, jsonPath, returningType ); } + + @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 ); + } } 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 b6f53b5359db..7db1d6e0b093 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 @@ -22,6 +22,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; @@ -143,6 +144,7 @@ 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.SqmJsonNullBehavior; import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralEntityType; @@ -2731,6 +2733,61 @@ public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContex return jsonValue; } + @Override + public SqmExpression visitJsonArrayFunction(HqlParser.JsonArrayFunctionContext 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) { + 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 SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { final boolean negated = ctx.NOT() != null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index eac8efeeebc6..2ed0cd683add 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 @@ -629,6 +629,18 @@ SqmJsonValueExpression jsonValue( @Override SqmJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath); + @Override + SqmExpression jsonArrayWithNulls(Expression... values); + + @Override + SqmExpression jsonArray(Expression... values); + + @Override + SqmExpression jsonObjectWithNulls(Map> keyValues); + + @Override + SqmExpression jsonObject(Map> keyValues); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides 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 70c6322c2df8..ce70889bcea3 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 @@ -121,6 +121,7 @@ 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.SqmJsonNullBehavior; import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmLiteralNull; @@ -5332,4 +5333,57 @@ public SqmJsonValueExpression jsonValue( ); } } + + @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 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 8115f635e2d3..744c80d6ad9a 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 @@ -193,7 +193,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() ); @@ -217,7 +217,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 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/SqmJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonValueExpression.java index 637e8f5a3538..5a7b561befd3 100644 --- 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 @@ -256,31 +256,35 @@ public void appendHqlString(StringBuilder sb) { sb.append( " returning " ); getArguments().get( 2 ).appendHqlString( sb ); } - switch ( errorBehavior ) { - case NULL: - sb.append( " null on error" ); - break; - case ERROR: - sb.append( " error on error" ); - break; - case DEFAULT: - sb.append( " default " ); - errorDefaultExpression.appendHqlString( sb ); - sb.append( " on error" ); - break; + if ( errorBehavior != null ) { + switch ( errorBehavior ) { + case NULL: + sb.append( " null on error" ); + break; + case ERROR: + sb.append( " error on error" ); + break; + case DEFAULT: + sb.append( " default " ); + errorDefaultExpression.appendHqlString( sb ); + sb.append( " on error" ); + break; + } } - switch ( emptyBehavior ) { - case NULL: - sb.append( " null on empty" ); - break; - case ERROR: - sb.append( " error on empty" ); - break; - case DEFAULT: - sb.append( " default " ); - emptyDefaultExpression.appendHqlString( sb ); - sb.append( " on empty" ); - break; + if ( emptyBehavior != null ) { + switch ( emptyBehavior ) { + case NULL: + sb.append( " null on empty" ); + break; + case ERROR: + sb.append( " error on empty" ); + break; + case DEFAULT: + sb.append( " default " ); + emptyDefaultExpression.appendHqlString( sb ); + sb.append( " on empty" ); + break; + } } sb.append( ')' ); } 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/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/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/function/json/JsonArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayTest.java new file mode 100644 index 000000000000..ab848ee18fd6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayTest.java @@ -0,0 +1,49 @@ +/* + * 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.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@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/JsonObjectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectTest.java new file mode 100644 index 000000000000..ccc72b1ecf92 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectTest.java @@ -0,0 +1,49 @@ +/* + * 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.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel +@SessionFactory +@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/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index c63adc9db18f..331c7aa64af6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -6,12 +6,16 @@ */ package org.hibernate.orm.test.query.hql; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.dialect.HSQLDialect; import org.hibernate.type.SqlTypes; +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; @@ -21,12 +25,18 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; 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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @DomainModel( annotatedClasses = JsonFunctionTests.JsonHolder.class) @SessionFactory @@ -91,6 +101,151 @@ public void testJsonValue(SessionFactoryScope scope) { ); } + @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 + 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" ) ); + } + ); + } + + 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 double[] parseDoubleArray( String s ) { + final List list = new ArrayList<>(); + int startIndex = 1; + int commaIndex; + while ( (commaIndex = s.indexOf(',', startIndex)) != -1 ) { + list.add( Double.parseDouble( s.substring( startIndex, commaIndex ) ) ); + startIndex = commaIndex + 1; + } + list.add( Double.parseDouble( s.substring( startIndex, s.length() - 1 ) ) ); + double[] array = new double[list.size()]; + for ( int i = 0; i < list.size(); i++ ) { + array[i] = list.get( i ); + } + return array; + } + @Entity(name = "JsonHolder") public static class JsonHolder { @Id diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index a5fddf6493f0..d3cb45978297 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -732,6 +732,18 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonArray implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_array" ); + } + } + + public static class SupportsJsonObject implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_object" ); + } + } + public static class SupportsJsonValueErrorBehavior implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return definesFunction( dialect, "json_value" ) From 72b8c6e5d837264131307dc337945388861e2254 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 22 Aug 2024 15:24:02 +0200 Subject: [PATCH 03/15] HHH-18496 Add json_exists and support the passing clause --- .../chapters/query/hql/QueryLanguage.adoc | 81 ++++++- .../query/hql/extras/json_exists_bnf.txt | 7 + .../query/hql/extras/json_value_bnf.txt | 5 +- .../dialect/CockroachLegacyDialect.java | 1 + .../community/dialect/DB2LegacyDialect.java | 3 +- .../community/dialect/H2LegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 3 +- .../dialect/PostgreSQLLegacyDialect.java | 2 + .../dialect/SQLServerLegacyDialect.java | 1 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 2 + .../org/hibernate/grammars/hql/HqlParser.g4 | 23 +- .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/DB2Dialect.java | 3 +- .../java/org/hibernate/dialect/Dialect.java | 10 +- .../java/org/hibernate/dialect/H2Dialect.java | 1 + .../org/hibernate/dialect/HANADialect.java | 3 +- .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 3 +- .../hibernate/dialect/PostgreSQLDialect.java | 2 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 77 ++++++- .../json/CockroachDBJsonValueFunction.java | 19 +- .../function/json/H2JsonExistsFunction.java | 54 +++++ .../function/json/H2JsonValueFunction.java | 63 ++++-- .../function/json/HANAJsonExistsFunction.java | 61 ++++++ .../function/json/HANAJsonObjectFunction.java | 10 +- .../function/json/JsonExistsFunction.java | 190 ++++++++++++++++ .../dialect/function/json/JsonPathHelper.java | 143 +++++++++++- .../function/json/JsonValueFunction.java | 57 ++++- .../json/MariaDBJsonValueFunction.java | 15 +- .../json/MySQLJsonExistsFunction.java | 46 ++++ .../function/json/MySQLJsonValueFunction.java | 15 +- .../json/PostgreSQLJsonExistsFunction.java | 52 +++++ .../json/PostgreSQLJsonValueFunction.java | 19 +- .../json/SQLServerJsonExistsFunction.java | 88 ++++++++ .../json/SQLServerJsonValueFunction.java | 32 ++- .../internal/util/QuotingHelper.java | 23 ++ .../criteria/HibernateCriteriaBuilder.java | 20 +- .../criteria/JpaJsonExistsExpression.java | 79 +++++++ .../criteria/JpaJsonValueExpression.java | 7 + .../spi/HibernateCriteriaBuilderDelegate.java | 13 ++ .../hql/internal/SemanticQueryBuilder.java | 45 ++++ .../query/internal/QueryLiteralHelper.java | 33 --- .../org/hibernate/query/sqm/NodeBuilder.java | 8 + .../sqm/function/SqmFunctionDescriptor.java | 9 + .../sqm/internal/SqmCriteriaNodeBuilder.java | 15 ++ .../sqm/sql/BaseSqmToSqlAstConverter.java | 36 ++- .../AbstractSqmJsonPathExpression.java | 128 +++++++++++ .../expression/SqmJsonExistsExpression.java | 207 ++++++++++++++++++ .../expression/SqmJsonValueExpression.java | 39 +++- .../query/sqm/tree/expression/SqmLiteral.java | 4 +- .../sql/ast/spi/AbstractSqlAstTranslator.java | 26 +++ .../hibernate/sql/ast/spi/SqlAppender.java | 14 ++ .../expression/JsonExistsErrorBehavior.java | 25 +++ .../expression/JsonPathPassingClause.java | 34 +++ .../test/function/json/JsonExistsTest.java | 102 +++++++++ .../orm/test/function/json/JsonValueTest.java | 13 +- .../orm/test/query/hql/JsonFunctionTests.java | 54 +++-- .../internal/tools/query/QueryBuilder.java | 6 +- .../orm/junit/DialectFeatureChecks.java | 6 + 61 files changed, 1888 insertions(+), 154 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_exists_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java delete mode 100644 hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/AbstractSqmJsonPathExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonExistsErrorBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonPathPassingClause.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 4821b60eb72f..8232f46e7345 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1632,6 +1632,7 @@ The following functions deal with SQL JSON types, which are not supported on eve | `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 |=== @@ -1713,7 +1714,8 @@ The first argument is an expression to a JSON document. The second argument is a WARNING: Some databases might also return non-scalar values. Beware that this behavior is not portable. -NOTE: It is recommended to only us the dot notation for JSON paths, since most databases support only that. +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]] ==== @@ -1723,24 +1725,23 @@ include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-example] ---- ==== -The `returning` clause allows to specify the <> i.e. the type of value to extract. +The `passing` clause allows to reuse the same JSON path but pass different values for evaluation. -[[hql-json-value-returning-example]] +[[hql-json-value-passing-example]] ==== [source, java, indent=0] ---- -include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-returning-example] +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-passing-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. +The `returning` clause allows to specify the <> i.e. the type of value to extract. -[[hql-json-value-on-error-example]] +[[hql-json-value-returning-example]] ==== [source, java, indent=0] ---- -include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-on-error-example] +include::{json-example-dir-hql}/JsonValueTest.java[tags=hql-json-value-returning-example] ---- ==== @@ -1754,6 +1755,17 @@ Conditions that classify as errors are database dependent, but usual errors whic 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] @@ -1767,6 +1779,59 @@ Depending on the database, an error might still be thrown even without that, but 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-user-defined-functions]] ==== Native and user-defined functions 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..e91cab8b3da4 --- /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_value_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_value_bnf.txt index b5ea83bf623e..c6f90535c6c4 100644 --- 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 @@ -1,4 +1,7 @@ -"json_value(" expression, expression ("returning" castTarget)? onErrorClause? onEmptyClause? ")" +"json_value(" expression, expression passingClause? ("returning" castTarget)? onErrorClause? onEmptyClause? ")" + +passingClause + : "passing" expression "as" identifier ("," expression "as" identifier)* onErrorClause : ( "error" | "null" | ( "default" expression ) ) "on error"; 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 24ac28d5280b..23a95630ff7d 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 @@ -503,6 +503,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonValue_cockroachdb(); functionFactory.jsonObject_postgresql(); + functionFactory.jsonExists_postgresql(); functionFactory.jsonArray_postgresql(); // Postgres uses # instead of ^ for XOR 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 d6c085a75a68..00a0d8166546 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,7 +430,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.listagg( null ); if ( getDB2Version().isSameOrAfter( 11 ) ) { - functionFactory.jsonValue(); + functionFactory.jsonValue_no_passing(); + functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_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 2a42abaf2319..3483ec6949f7 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 @@ -403,6 +403,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { functionFactory.jsonValue_h2(); + functionFactory.jsonExists_h2(); } } else { 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 c66160b66b11..706d1de457b4 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 @@ -654,6 +654,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) { functionFactory.jsonValue_mysql(); + functionFactory.jsonExists_mysql(); functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); } 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 f0f1ce6d69f8..cac0bcacb3c3 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 @@ -322,7 +322,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_oracle(); if ( getVersion().isSameOrAfter( 12 ) ) { - functionFactory.jsonValue_literal_path(); + functionFactory.jsonValue_oracle(); + functionFactory.jsonExists_oracle(); functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); } 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 d3f0878c57d9..55b399ee02c8 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 @@ -623,11 +623,13 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 17 ) ) { functionFactory.jsonValue(); + functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); } else { functionFactory.jsonValue_postgresql(); + functionFactory.jsonExists_postgresql(); if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); functionFactory.jsonArray(); 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 b4306548298c..51e6a4857871 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,6 +402,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); + functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); } diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index d5e8d404f080..8162269e757f 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 @@ -223,6 +223,7 @@ 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_EXISTS : [jJ] [sS] [oO] [nN] '_' [eE] [xX] [iI] [sS] [tT] [sS]; JSON_OBJECT : [jJ] [sS] [oO] [nN] '_' [oO] [bB] [jJ] [eE] [cC] [tT]; JSON_VALUE : [jJ] [sS] [oO] [nN] '_' [vV] [aA] [lL] [uU] [eE]; KEY : [kK] [eE] [yY]; @@ -274,6 +275,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]; 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 a8caeb932f4b..1e63f6a46163 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 @@ -1622,16 +1622,21 @@ rollup ; jsonFunction - : jsonValueFunction - | jsonArrayFunction + : jsonArrayFunction + | jsonExistsFunction | jsonObjectFunction + | jsonValueFunction ; /** * The 'json_value()' function */ jsonValueFunction - : JSON_VALUE LEFT_PAREN expression COMMA expression jsonValueReturningClause? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? RIGHT_PAREN + : JSON_VALUE LEFT_PAREN expression COMMA expression jsonPassingClause? jsonValueReturningClause? jsonValueOnErrorOrEmptyClause? jsonValueOnErrorOrEmptyClause? RIGHT_PAREN + ; + +jsonPassingClause + : PASSING expressionOrPredicate AS identifier (COMMA expressionOrPredicate AS identifier)* ; jsonValueReturningClause @@ -1641,6 +1646,16 @@ jsonValueReturningClause jsonValueOnErrorOrEmptyClause : ( ERROR | NULL | ( DEFAULT expression ) ) 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 */ @@ -1760,6 +1775,7 @@ jsonNullClause | IS | JOIN | JSON_ARRAY + | JSON_EXISTS | JSON_OBJECT | JSON_VALUE | KEY @@ -1812,6 +1828,7 @@ jsonNullClause | OVERLAY | PAD | PARTITION + | PASSING | PERCENT | PLACING | POSITION diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 3632998ce401..972a2fe8803c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -469,6 +469,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_postgresql(); functionFactory.jsonValue_cockroachdb(); + functionFactory.jsonExists_postgresql(); functionFactory.jsonObject_postgresql(); functionFactory.jsonArray_postgresql(); 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 a7b01adeed74..c63ed4f4b432 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -416,7 +416,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.listagg( null ); if ( getDB2Version().isSameOrAfter( 11 ) ) { - functionFactory.jsonValue(); + functionFactory.jsonValue_no_passing(); + functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_db2(); } 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 a7efa68d55bc..7761e91d6dde 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -4545,15 +4545,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 5d6defa48cd9..7e374c110287 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -346,6 +346,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray(); if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { functionFactory.jsonValue_h2(); + functionFactory.jsonExists_h2(); } } 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 3949caa71572..6663eba2f28a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -491,7 +491,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter(2, 0, 20) ) { // Introduced in 2.0 SPS 02 - functionFactory.jsonValue(); + functionFactory.jsonValue_no_passing(); + functionFactory.jsonExists_hana(); if ( getVersion().isSameOrAfter(2, 0, 40) ) { // Introduced in 2.0 SPS 04 functionFactory.jsonObject_hana(); 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 17040732e050..2eca2baf4b79 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -639,6 +639,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.listagg_groupConcat(); functionFactory.jsonValue_mysql(); + functionFactory.jsonExists_mysql(); functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); } 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 65f927d379cc..4fe3a2c80ad9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -399,7 +399,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); - functionFactory.jsonValue_literal_path(); + functionFactory.jsonValue_oracle(); + functionFactory.jsonExists_oracle(); functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); } 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 fa59da4de2a0..12e112a5db5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -584,11 +584,13 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 17 ) ) { functionFactory.jsonValue(); + functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); } else { functionFactory.jsonValue_postgresql(); + functionFactory.jsonExists_postgresql(); if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); functionFactory.jsonArray(); 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 1e7d8cb19154..4a0df2924575 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -420,6 +420,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); + functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); } 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 7cde6bb7b444..0194cc69529c 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 @@ -79,25 +79,31 @@ import org.hibernate.dialect.function.json.CockroachDBJsonValueFunction; import org.hibernate.dialect.function.json.DB2JsonArrayFunction; import org.hibernate.dialect.function.json.DB2JsonObjectFunction; +import org.hibernate.dialect.function.json.H2JsonExistsFunction; import org.hibernate.dialect.function.json.H2JsonValueFunction; import org.hibernate.dialect.function.json.HANAJsonArrayFunction; +import org.hibernate.dialect.function.json.HANAJsonExistsFunction; import org.hibernate.dialect.function.json.HANAJsonObjectFunction; import org.hibernate.dialect.function.json.HSQLJsonArrayFunction; import org.hibernate.dialect.function.json.HSQLJsonObjectFunction; import org.hibernate.dialect.function.json.JsonArrayFunction; +import org.hibernate.dialect.function.json.JsonExistsFunction; import org.hibernate.dialect.function.json.JsonObjectFunction; import org.hibernate.dialect.function.json.JsonValueFunction; import org.hibernate.dialect.function.json.MariaDBJsonArrayFunction; import org.hibernate.dialect.function.json.MariaDBJsonValueFunction; import org.hibernate.dialect.function.json.MySQLJsonArrayFunction; +import org.hibernate.dialect.function.json.MySQLJsonExistsFunction; import org.hibernate.dialect.function.json.MySQLJsonObjectFunction; import org.hibernate.dialect.function.json.MySQLJsonValueFunction; import org.hibernate.dialect.function.json.OracleJsonArrayFunction; import org.hibernate.dialect.function.json.OracleJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; +import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; import org.hibernate.dialect.function.json.SQLServerJsonObjectFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; @@ -3349,14 +3355,21 @@ public void arrayToString_oracle() { * json_value() function */ public void jsonValue() { - functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true ) ); + functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, true, true ) ); } /** - * json_value() function that supports only literal json paths + * json_value() function that doesn't support the passing clause */ - public void jsonValue_literal_path() { - functionRegistry.register( "json_value", new JsonValueFunction( typeConfiguration, false ) ); + 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 ) ); } /** @@ -3401,6 +3414,62 @@ public void jsonValue_h2() { functionRegistry.register( "json_value", new H2JsonValueFunction( 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 ) ); + } + + /** + * 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 */ diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java index ec61a58062e6..ada19353b3bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java @@ -13,11 +13,11 @@ 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; /** @@ -26,7 +26,7 @@ public class CockroachDBJsonValueFunction extends JsonValueFunction { public CockroachDBJsonValueFunction(TypeConfiguration typeConfiguration) { - super( typeConfiguration, true ); + super( typeConfiguration, true, false ); } @Override @@ -75,6 +75,19 @@ protected void render( 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() ); 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/H2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java index 9ded156b7ff2..1777a2375607 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -10,21 +10,26 @@ 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 ); + super( typeConfiguration, false, true ); } @Override @@ -58,7 +63,16 @@ protected void render( if ( defaultExpression != null ) { sqlAppender.appendSql( "coalesce(" ); } - renderJsonPath( sqlAppender, arguments.jsonDocument(), walker, jsonPath ); + 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 ); @@ -73,27 +87,45 @@ protected void render( } } - private void renderJsonPath( + public static void renderJsonPath( SqlAppender sqlAppender, - SqlAstNode jsonDocument, + Expression jsonDocument, + boolean isJson, SqlAstTranslator walker, - String jsonPath) { - sqlAppender.appendSql( "cast(" ); - + String jsonPath, + @Nullable JsonPathPassingClause passingClause) { final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); - final boolean needsWrapping = jsonPathElements.get( 0 ) instanceof JsonPathHelper.JsonAttribute; + 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(); - appendInDoubleQuotes( sqlAppender, attributeName ); + 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( '[' ); @@ -101,18 +133,5 @@ private void renderJsonPath( sqlAppender.appendSql( ']' ); } } - sqlAppender.appendSql( " as varchar)" ); - } - - private static void appendInDoubleQuotes(SqlAppender sqlAppender, String attributeName) { - sqlAppender.appendSql( ".\"" ); - for ( int j = 0; j < attributeName.length(); j++ ) { - final char c = attributeName.charAt( j ); - if ( c == '"' ) { - sqlAppender.appendSql( '"' ); - } - sqlAppender.appendSql( c ); - } - sqlAppender.appendSql( '"' ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java new file mode 100644 index 000000000000..6ce661e428f8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.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.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SAP HANA json_exists function. + */ +public class HANAJsonExistsFunction extends JsonExistsFunction { + + public HANAJsonExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType 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/HANAJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectFunction.java index 556b372139b6..1c971e830bbd 100644 --- 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 @@ -106,15 +106,7 @@ private static void replaceJsonArgumentsEscaping( value.accept( walker ); sqlAppender.appendSql( ' ' ); final String literalValue = walker.getLiteralValue( (Expression) key ); - sqlAppender.appendSql( '"' ); - for ( int j = 0; j < literalValue.length(); j++ ) { - final char c = literalValue.charAt( j ); - if ( c == '"' ) { - sqlAppender.appendSql( '"' ); - } - sqlAppender.appendSql( c ); - } - sqlAppender.appendSql( '"' ); + sqlAppender.appendDoubleQuoteEscapedString( literalValue ); separator = ','; } sqlAppender.appendSql( " from sys.dummy for json('arraywrap'='no'" ); 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..83497b09f610 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java @@ -0,0 +1,190 @@ +/* + * 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, + StandardArgumentsValidators.composite( + new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY ) + ), + 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/JsonPathHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java index 0eaa449524aa..743b9af997ec 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -10,6 +10,10 @@ 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 { @@ -47,6 +51,112 @@ public static List parseJsonPathElements(String jsonPath) { 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 ) { @@ -64,11 +174,40 @@ private static void parseBracket(String jsonPath, int bracketStartIndex, int end if ( bracketEndIndex < bracketStartIndex ) { throw new QueryException( "Can't emulate non-simple json path expression: " + jsonPath ); } - final int index = Integer.parseInt( jsonPath, bracketStartIndex + 1, bracketEndIndex, 10 ); - jsonPathElements.add( new JsonIndexAccess( index ) ); + 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/JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java index 14145141610b..88706ce9e6f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java @@ -6,7 +6,9 @@ */ 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; @@ -23,6 +25,7 @@ import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.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; @@ -40,8 +43,12 @@ public class JsonValueFunction extends AbstractSqmSelfRenderingFunctionDescriptor { protected final boolean supportsJsonPathExpression; + protected final boolean supportsJsonPathPassingClause; - public JsonValueFunction(TypeConfiguration typeConfiguration, boolean supportsJsonPathExpression) { + public JsonValueFunction( + TypeConfiguration typeConfiguration, + boolean supportsJsonPathExpression, + boolean supportsJsonPathPassingClause) { super( "json_value", FunctionKind.NORMAL, @@ -52,6 +59,7 @@ public JsonValueFunction(TypeConfiguration typeConfiguration, boolean supportsJs StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) ); this.supportsJsonPathExpression = supportsJsonPathExpression; + this.supportsJsonPathPassingClause = supportsJsonPathPassingClause; } @Override @@ -88,16 +96,43 @@ protected void render( sqlAppender.appendSql( "json_value(" ); arguments.jsonDocument().accept( walker ); sqlAppender.appendSql( ',' ); - if ( supportsJsonPathExpression ) { - arguments.jsonPath().accept( walker ); + 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 { - walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( sqlAppender, - walker.getLiteralValue( arguments.jsonPath() ) + "", + arguments.jsonPath(), + passingClause, + walker ); } - if ( arguments.returningType() != null ) { sqlAppender.appendSql( " returning " ); arguments.returningType().accept( walker ); @@ -133,11 +168,13 @@ 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; @@ -148,6 +185,13 @@ public static JsonValueArguments extract(List sqlAstArgume 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 ) { @@ -167,6 +211,7 @@ public static JsonValueArguments extract(List sqlAstArgume (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/MariaDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java index fbea1833868d..c144dee92a96 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java @@ -10,6 +10,7 @@ import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; import org.hibernate.type.spi.TypeConfiguration; @@ -20,7 +21,7 @@ public class MariaDBJsonValueFunction extends JsonValueFunction { public MariaDBJsonValueFunction(TypeConfiguration typeConfiguration) { - super( typeConfiguration, true ); + super( typeConfiguration, true, false ); } @Override @@ -42,7 +43,17 @@ protected void render( sqlAppender.appendSql( "json_unquote(nullif(json_extract(" ); arguments.jsonDocument().accept( walker ); sqlAppender.appendSql( "," ); - arguments.jsonPath().accept( walker ); + 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 " ); 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/MySQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java index 2586d6d6d633..138c536ce1e4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java @@ -9,6 +9,7 @@ import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.JsonPathPassingClause; import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; import org.hibernate.type.spi.TypeConfiguration; @@ -19,7 +20,7 @@ public class MySQLJsonValueFunction extends JsonValueFunction { public MySQLJsonValueFunction(TypeConfiguration typeConfiguration) { - super( typeConfiguration, true ); + super( typeConfiguration, true, false ); } @Override @@ -42,7 +43,17 @@ protected void render( sqlAppender.appendSql( "json_unquote(nullif(json_extract(" ); arguments.jsonDocument().accept( walker ); sqlAppender.appendSql( "," ); - arguments.jsonPath().accept( walker ); + 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 " ); 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/PostgreSQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java index acbf1d2e0d5d..f6b7f8b63ad1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java @@ -6,12 +6,16 @@ */ 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; @@ -23,7 +27,7 @@ public class PostgreSQLJsonValueFunction extends JsonValueFunction { public PostgreSQLJsonValueFunction(TypeConfiguration typeConfiguration) { - super( typeConfiguration, true ); + super( typeConfiguration, true, true ); } @Override @@ -61,6 +65,19 @@ protected void render( jsonPath.accept( walker ); sqlAppender.appendSql( " as jsonpath)" ); } + final JsonPathPassingClause passingClause = arguments.passingClause(); + if ( passingClause != null ) { + sqlAppender.append( ",jsonb_build_object" ); + char separator = '('; + 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 ) { 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/SQLServerJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonValueFunction.java index ec57b30944fc..687fbaaaa1fe 100644 --- 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 @@ -10,7 +10,7 @@ import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; -import org.hibernate.sql.ast.tree.expression.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; @@ -21,7 +21,7 @@ public class SQLServerJsonValueFunction extends JsonValueFunction { public SQLServerJsonValueFunction(TypeConfiguration typeConfiguration) { - super( typeConfiguration, true ); + super( typeConfiguration, true, false ); } @Override @@ -36,7 +36,7 @@ protected void render( } sqlAppender.appendSql( "(select v from openjson(" ); arguments.jsonDocument().accept( walker ); - sqlAppender.appendSql( ",'$') with (v " ); + sqlAppender.appendSql( ") with (v " ); if ( arguments.returningType() != null ) { arguments.returningType().accept( walker ); } @@ -44,10 +44,32 @@ protected void render( sqlAppender.appendSql( "varchar(max)" ); } sqlAppender.appendSql( ' ' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { - walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + // 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, - "strict " + walker.getLiteralValue( arguments.jsonPath() ) + "", + arguments.jsonPath(), + passingClause, + walker ); } else { 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 472613b4144e..9153315a7910 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 @@ -4,6 +4,8 @@ */ package org.hibernate.internal.util; +import org.hibernate.sql.ast.spi.SqlAppender; + public final class QuotingHelper { private QuotingHelper() { /* static methods only - hide constructor */ @@ -150,4 +152,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/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 5f99a2a4acdd..3c13abb6bd81 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 @@ -3691,7 +3691,7 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E JpaJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath); /** - * Extracts a value by JSON path from a json document. + * Extracts a value by JSON path from a JSON document. * * @since 7.0 */ @@ -3706,13 +3706,29 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E JpaJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath); /** - * Extracts a value by JSON path from a json document. + * Extracts a value by JSON path from a JSON document. * * @since 7.0 */ @Incubating JpaJsonValueExpression jsonValue(Expression jsonDocument, Expression jsonPath, Class returningType); + /** + * 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. * 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..77186f7e7db6 --- /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 value 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/JpaJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java index b35c2a9772c6..87e3991621ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java @@ -97,6 +97,13 @@ public interface JpaJsonValueExpression extends JpaExpression { */ 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. */ 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 fd08ae1f2125..ea53f3e79f13 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 @@ -38,6 +38,7 @@ 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.JpaJsonValueExpression; import org.hibernate.query.criteria.JpaListJoin; import org.hibernate.query.criteria.JpaMapJoin; @@ -3375,6 +3376,18 @@ public JpaJsonValueExpression jsonValue( return criteriaBuilder.jsonValue( jsonDocument, jsonPath, returningType ); } + @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) { 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 7db1d6e0b093..6c909bb5f14b 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 @@ -144,6 +144,7 @@ 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.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmLiteral; @@ -2730,9 +2731,53 @@ public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContex } } } + final HqlParser.JsonPassingClauseContext passingClause = ctx.jsonPassingClause(); + if ( passingClause != null ) { + final List expressionContexts = passingClause.expressionOrPredicate(); + final List identifierContexts = passingClause.identifier(); + for ( int i = 0; i < expressionContexts.size(); i++ ) { + jsonValue.passing( + visitIdentifier( identifierContexts.get( i ) ), + (SqmExpression) expressionContexts.get( i ).accept( this ) + ); + } + } return jsonValue; } + @Override + public SqmExpression visitJsonExistsFunction(HqlParser.JsonExistsFunctionContext 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) { final HqlParser.JsonNullClauseContext subCtx = ctx.jsonNullClause(); 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 190524908e75..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryLiteralHelper.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-License-Identifier: LGPL-2.1-or-later - * Copyright Red Hat Inc. and Hibernate Authors - */ -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/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 2ed0cd683add..318297645923 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 @@ -26,6 +26,7 @@ import org.hibernate.query.criteria.JpaCoalesce; import org.hibernate.query.criteria.JpaCompoundSelection; import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaJsonExistsExpression; import org.hibernate.query.criteria.JpaOrder; import org.hibernate.query.criteria.JpaParameterExpression; import org.hibernate.query.criteria.JpaPredicate; @@ -44,6 +45,7 @@ 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.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmModifiedSubQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmTuple; @@ -629,6 +631,12 @@ SqmJsonValueExpression jsonValue( @Override SqmJsonValueExpression jsonValue(Expression jsonDocument, String jsonPath); + @Override + SqmJsonExistsExpression jsonExists(Expression jsonDocument, Expression jsonPath); + + @Override + SqmJsonExistsExpression jsonExists(Expression jsonDocument, String jsonPath); + @Override SqmExpression jsonArrayWithNulls(Expression... values); 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 1092f73aba78..926b9ec812d0 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 @@ -220,4 +220,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/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index ce70889bcea3..fbc7da473c05 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 @@ -121,6 +121,7 @@ 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.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmLiteral; @@ -5334,6 +5335,20 @@ public SqmJsonValueExpression jsonValue( } } + @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 ); 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 5212a591a53e..7a9854b87e79 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 @@ -289,6 +289,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; @@ -297,6 +298,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; @@ -6439,7 +6441,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(); @@ -8220,14 +8237,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..32e29fd4929b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonExistsExpression.java @@ -0,0 +1,207 @@ +/* + * 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 @Nullable ErrorBehavior errorBehavior; + + 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, + @Nullable 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 ); + } + if ( errorBehavior != null ) { + switch ( errorBehavior ) { + case ERROR: + arguments.add( JsonExistsErrorBehavior.ERROR ); + break; + case TRUE: + arguments.add( JsonExistsErrorBehavior.TRUE ); + break; + case FALSE: + arguments.add( JsonExistsErrorBehavior.FALSE ); + break; + } + } + 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 ); + if ( errorBehavior != null ) { + switch ( errorBehavior ) { + case ERROR: + sb.append( " error on error" ); + break; + case TRUE: + sb.append( " true on error" ); + break; + case FALSE: + sb.append( " false on error" ); + break; + } + } + 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 index 5a7b561befd3..7b66ec67eeeb 100644 --- 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 @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.hibernate.Incubating; import org.hibernate.query.ReturnableType; @@ -16,7 +17,6 @@ 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.SelfRenderingSqmFunction; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.query.sqm.produce.function.ArgumentsValidator; import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; @@ -25,6 +25,7 @@ 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; @@ -36,7 +37,7 @@ * @since 7.0 */ @Incubating -public class SqmJsonValueExpression extends SelfRenderingSqmFunction implements JpaJsonValueExpression { +public class SqmJsonValueExpression extends AbstractSqmJsonPathExpression implements JpaJsonValueExpression { private @Nullable ErrorBehavior errorBehavior; private SqmExpression errorDefaultExpression; private @Nullable EmptyBehavior emptyBehavior; @@ -72,6 +73,7 @@ private SqmJsonValueExpression( FunctionReturnTypeResolver returnTypeResolver, NodeBuilder nodeBuilder, String name, + @Nullable Map> passingExpressions, @Nullable ErrorBehavior errorBehavior, SqmExpression errorDefaultExpression, @Nullable EmptyBehavior emptyBehavior, @@ -84,7 +86,8 @@ private SqmJsonValueExpression( argumentsValidator, returnTypeResolver, nodeBuilder, - name + name, + passingExpressions ); this.errorBehavior = errorBehavior; this.errorDefaultExpression = errorDefaultExpression; @@ -112,6 +115,7 @@ public SqmJsonValueExpression copy(SqmCopyContext context) { getReturnTypeResolver(), nodeBuilder(), getFunctionName(), + copyPassingExpressions( context ), errorBehavior, errorDefaultExpression == null ? null : errorDefaultExpression.copy( context ), emptyBehavior, @@ -141,28 +145,28 @@ public EmptyBehavior getEmptyBehavior() { } @Override - public JpaJsonValueExpression unspecifiedOnError() { + public SqmJsonValueExpression unspecifiedOnError() { this.errorBehavior = ErrorBehavior.UNSPECIFIED; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression errorOnError() { + public SqmJsonValueExpression errorOnError() { this.errorBehavior = ErrorBehavior.ERROR; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression nullOnError() { + public SqmJsonValueExpression nullOnError() { this.errorBehavior = ErrorBehavior.NULL; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression defaultOnError(jakarta.persistence.criteria.Expression expression) { + public SqmJsonValueExpression defaultOnError(jakarta.persistence.criteria.Expression expression) { this.errorBehavior = ErrorBehavior.DEFAULT; //noinspection unchecked this.errorDefaultExpression = (SqmExpression) expression; @@ -170,34 +174,42 @@ public JpaJsonValueExpression defaultOnError(jakarta.persistence.criteria.Exp } @Override - public JpaJsonValueExpression unspecifiedOnEmpty() { + public SqmJsonValueExpression unspecifiedOnEmpty() { this.errorBehavior = ErrorBehavior.UNSPECIFIED; this.errorDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression errorOnEmpty() { + public SqmJsonValueExpression errorOnEmpty() { this.emptyBehavior = EmptyBehavior.ERROR; this.emptyDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression nullOnEmpty() { + public SqmJsonValueExpression nullOnEmpty() { this.emptyBehavior = EmptyBehavior.NULL; this.emptyDefaultExpression = null; return this; } @Override - public JpaJsonValueExpression defaultOnEmpty(jakarta.persistence.criteria.Expression expression) { + 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 ); @@ -206,6 +218,10 @@ public Expression convertToSqlAst(SqmToSqlAstConverter walker) { if ( validator != null ) { validator.validateSqlTypes( arguments, getFunctionName() ); } + final JsonPathPassingClause jsonPathPassingClause = createJsonPathPassingClause( walker ); + if ( jsonPathPassingClause != null ) { + arguments.add( jsonPathPassingClause ); + } if ( errorBehavior != null ) { switch ( errorBehavior ) { case NULL: @@ -252,6 +268,7 @@ public void appendHqlString(StringBuilder sb) { sb.append( ',' ); getArguments().get( 1 ).appendHqlString( sb ); + appendPassingExpressionHqlString( sb ); if ( getArguments().size() > 2 ) { sb.append( " returning " ); getArguments().get( 2 ).appendHqlString( sb ); 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 737897c096c4..7dac8205b1fd 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 @@ -4,7 +4,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; @@ -82,7 +82,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/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 45c31e486b84..62114de1aa3c 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 @@ -38,6 +38,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; @@ -119,6 +120,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; @@ -548,6 +550,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 ); @@ -680,6 +692,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 14ea197660cb..215e21e78a2f 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 @@ -4,6 +4,8 @@ */ package org.hibernate.sql.ast.spi; +import org.hibernate.internal.util.QuotingHelper; + /** * Access to appending SQL fragments to an in-flight buffer * @@ -44,6 +46,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/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/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..d35207e9dfba --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.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 . + */ +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.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.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = EntityWithJson.class) +@SessionFactory +@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 + 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/JsonValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonValueTest.java index c532b025fe69..14929dc4b715 100644 --- 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 @@ -71,6 +71,17 @@ public void testSimple(SessionFactoryScope scope) { } ); } + @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 -> { @@ -91,7 +102,7 @@ public void testOnError(SessionFactoryScope scope) { 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 path"); + fail("error clause should fail because of invalid json document"); } catch ( HibernateException e ) { if ( !( e instanceof JDBCException ) && !( e instanceof ExecutionException ) ) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 331c7aa64af6..337dc55d152c 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -6,7 +6,6 @@ */ package org.hibernate.orm.test.query.hql; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -101,6 +100,20 @@ public void testJsonValue(SessionFactoryScope scope) { ); } + @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.SupportsJsonArray.class) public void testJsonArray(SessionFactoryScope scope) { @@ -209,6 +222,29 @@ public void testJsonObjectAndArray(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonExists.class) + 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 ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { @@ -230,22 +266,6 @@ private static Object[] parseArray(String json) { } } - private static double[] parseDoubleArray( String s ) { - final List list = new ArrayList<>(); - int startIndex = 1; - int commaIndex; - while ( (commaIndex = s.indexOf(',', startIndex)) != -1 ) { - list.add( Double.parseDouble( s.substring( startIndex, commaIndex ) ) ); - startIndex = commaIndex + 1; - } - list.add( Double.parseDouble( s.substring( startIndex, s.length() - 1 ) ) ); - double[] array = new double[list.size()]; - for ( int i = 0; i < list.size(); i++ ) { - array[i] = list.get( i ); - } - return array; - } - @Entity(name = "JsonHolder") public static class JsonHolder { @Id 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 14f684c11c89..50d16363d85e 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 @@ -30,8 +30,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; /** @@ -365,10 +365,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/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index d3cb45978297..4f82a757e6e7 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -732,6 +732,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonExists implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_exists" ); + } + } + public static class SupportsJsonArray implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return definesFunction( dialect, "json_array" ); From c4269725a17d0ec37d1adbef0783b5b22a08c287 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 9 Sep 2024 19:09:31 +0200 Subject: [PATCH 04/15] HHH-18496 Add json_query --- .../chapters/query/hql/QueryLanguage.adoc | 89 +++++- .../query/hql/extras/json_exists_bnf.txt | 2 +- .../query/hql/extras/json_query_bnf.txt | 14 + .../query/hql/extras/json_value_bnf.txt | 2 +- .../community/dialect/DB2LegacyDialect.java | 1 + .../community/dialect/H2LegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 2 + .../dialect/SQLServerLegacyDialect.java | 1 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 5 + .../org/hibernate/grammars/hql/HqlParser.g4 | 21 ++ .../org/hibernate/dialect/DB2Dialect.java | 1 + .../java/org/hibernate/dialect/H2Dialect.java | 1 + .../org/hibernate/dialect/HANADialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 1 + .../hibernate/dialect/PostgreSQLDialect.java | 2 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 54 ++++ .../function/json/H2JsonQueryFunction.java | 67 ++++ .../function/json/JsonExistsFunction.java | 4 +- .../function/json/JsonQueryFunction.java | 222 +++++++++++++ .../function/json/JsonValueFunction.java | 2 +- .../function/json/MySQLJsonQueryFunction.java | 57 ++++ .../json/PostgreSQLJsonQueryFunction.java | 81 +++++ .../json/SQLServerJsonQueryFunction.java | 164 ++++++++++ .../json/SQLServerJsonValueFunction.java | 2 +- .../criteria/HibernateCriteriaBuilder.java | 14 + .../criteria/JpaJsonExistsExpression.java | 2 +- .../criteria/JpaJsonQueryExpression.java | 204 ++++++++++++ .../spi/HibernateCriteriaBuilderDelegate.java | 13 + .../hql/internal/SemanticQueryBuilder.java | 74 +++++ .../org/hibernate/query/sqm/NodeBuilder.java | 8 +- .../sqm/internal/SqmCriteriaNodeBuilder.java | 15 + .../expression/SqmJsonExistsExpression.java | 36 +-- .../expression/SqmJsonQueryExpression.java | 295 ++++++++++++++++++ .../expression/SqmJsonValueExpression.java | 88 ++---- .../expression/JsonQueryEmptyBehavior.java | 26 ++ .../expression/JsonQueryErrorBehavior.java | 26 ++ .../tree/expression/JsonQueryWrapMode.java | 25 ++ .../orm/test/fetching/FetchingTest.java | 12 +- .../orm/test/function/json/JsonQueryTest.java | 134 ++++++++ .../generated/always/GeneratedAlwaysTest.java | 1 - .../orm/test/query/hql/JsonFunctionTests.java | 121 ++++++- .../testing/junit4/CustomRunner.java | 4 + .../orm/junit/DialectFeatureChecks.java | 15 + .../orm/junit/DialectFilterExtension.java | 33 +- 48 files changed, 1837 insertions(+), 110 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_query_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonQueryFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonQueryFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonQueryFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonQueryExpression.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryEmptyBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryErrorBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonQueryWrapMode.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonQueryTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 8232f46e7345..5c3c764aa0e8 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1633,6 +1633,7 @@ The following functions deal with SQL JSON types, which are not supported on eve | `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 |=== @@ -1712,7 +1713,7 @@ 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 return non-scalar values. Beware that this behavior is not portable. +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. @@ -1832,6 +1833,92 @@ include::{json-example-dir-hql}/JsonExistsTest.java[tags=hql-json-exists-on-erro 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-user-defined-functions]] ==== Native and user-defined functions 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 index e91cab8b3da4..3921a527788b 100644 --- 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 @@ -1,4 +1,4 @@ -"json_exists(" expression, expression passingClause? onErrorClause? ")" +"json_exists(" expression "," expression passingClause? onErrorClause? ")" passingClause : "passing" expression "as" identifier ("," expression "as" identifier)* 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 index c6f90535c6c4..fe2c284d92cc 100644 --- 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 @@ -1,4 +1,4 @@ -"json_value(" expression, expression passingClause? ("returning" castTarget)? onErrorClause? onEmptyClause? ")" +"json_value(" expression "," expression passingClause? ("returning" castTarget)? onErrorClause? onEmptyClause? ")" passingClause : "passing" expression "as" identifier ("," expression "as" identifier)* 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 00a0d8166546..df4ceba75404 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 @@ -431,6 +431,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getDB2Version().isSameOrAfter( 11 ) ) { functionFactory.jsonValue_no_passing(); + functionFactory.jsonQuery_no_passing(); functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_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 3483ec6949f7..9ae161523965 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 @@ -403,6 +403,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { functionFactory.jsonValue_h2(); + functionFactory.jsonQuery_h2(); functionFactory.jsonExists_h2(); } } 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 706d1de457b4..d0c3112e78f1 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 @@ -654,6 +654,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getMySQLVersion().isSameOrAfter( 5, 7 ) ) { functionFactory.jsonValue_mysql(); + functionFactory.jsonQuery_mysql(); functionFactory.jsonExists_mysql(); functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); 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 cac0bcacb3c3..a1beebb068aa 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 @@ -323,6 +323,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 12 ) ) { functionFactory.jsonValue_oracle(); + functionFactory.jsonQuery_oracle(); functionFactory.jsonExists_oracle(); functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); 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 55b399ee02c8..eb681ef396a9 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 @@ -623,12 +623,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 17 ) ) { functionFactory.jsonValue(); + functionFactory.jsonQuery(); functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); } else { functionFactory.jsonValue_postgresql(); + functionFactory.jsonQuery_postgresql(); functionFactory.jsonExists_postgresql(); if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); 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 51e6a4857871..af8f19e64ec1 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,6 +402,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); + functionFactory.jsonQuery_sqlserver(); functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index 8162269e757f..37317d6fd86e 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 @@ -149,6 +149,7 @@ 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]; @@ -160,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]; @@ -225,6 +227,7 @@ JOIN : [jJ] [oO] [iI] [nN]; JSON_ARRAY : [jJ] [sS] [oO] [nN] '_' [aA] [rR] [rR] [aA] [yY]; 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_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]; @@ -310,6 +313,7 @@ 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]; UPDATE : [uU] [pP] [dD] [aA] [tT] [eE]; USING : [uU] [sS] [iI] [nN] [gG]; @@ -321,6 +325,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 1e63f6a46163..e7cd44a7d363 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 @@ -1625,6 +1625,7 @@ jsonFunction : jsonArrayFunction | jsonExistsFunction | jsonObjectFunction + | jsonQueryFunction | jsonValueFunction ; @@ -1646,6 +1647,21 @@ jsonValueReturningClause 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 */ @@ -1699,6 +1715,7 @@ jsonNullClause | ALL | AND | ANY + | ARRAY | AS | ASC | AVG @@ -1710,6 +1727,7 @@ jsonNullClause | CAST | COLLATE | COLUMN + | CONDITIONAL | CONFLICT | CONSTRAINT | CONTAINS @@ -1777,6 +1795,7 @@ jsonNullClause | JSON_ARRAY | JSON_EXISTS | JSON_OBJECT + | JSON_QUERY | JSON_VALUE | KEY | KEYS @@ -1863,6 +1882,7 @@ jsonNullClause | TRUNCATE | TYPE | UNBOUNDED + | UNCONDITIONAL | UNION | UPDATE | USING @@ -1876,6 +1896,7 @@ jsonNullClause | WITH | WITHIN | WITHOUT + | WRAPPER | YEAR | ZONED) { logUseOfReservedWordAsIdentifier( getCurrentToken() ); 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 c63ed4f4b432..ca819eef7b32 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -417,6 +417,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getDB2Version().isSameOrAfter( 11 ) ) { functionFactory.jsonValue_no_passing(); + functionFactory.jsonQuery_no_passing(); functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_db2(); 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 7e374c110287..8d903bbe465f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -346,6 +346,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray(); if ( getVersion().isSameOrAfter( 2, 2, 220 ) ) { functionFactory.jsonValue_h2(); + functionFactory.jsonQuery_h2(); functionFactory.jsonExists_h2(); } } 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 6663eba2f28a..28f578596431 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -492,6 +492,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio 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 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 2eca2baf4b79..e014eb69f42e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -639,6 +639,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.listagg_groupConcat(); functionFactory.jsonValue_mysql(); + functionFactory.jsonQuery_mysql(); functionFactory.jsonExists_mysql(); functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); 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 4fe3a2c80ad9..aef4908d82d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -400,6 +400,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_oracle(); functionFactory.jsonValue_oracle(); + functionFactory.jsonQuery_oracle(); functionFactory.jsonExists_oracle(); functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); 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 12e112a5db5a..fa1f194f98a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -584,12 +584,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 17 ) ) { functionFactory.jsonValue(); + functionFactory.jsonQuery(); functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); } else { functionFactory.jsonValue_postgresql(); + functionFactory.jsonQuery_postgresql(); functionFactory.jsonExists_postgresql(); if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); 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 4a0df2924575..1848d80abba4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -420,6 +420,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); + functionFactory.jsonQuery_sqlserver(); functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); 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 0194cc69529c..651b4cd7e37f 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 @@ -80,6 +80,7 @@ import org.hibernate.dialect.function.json.DB2JsonArrayFunction; import org.hibernate.dialect.function.json.DB2JsonObjectFunction; import org.hibernate.dialect.function.json.H2JsonExistsFunction; +import org.hibernate.dialect.function.json.H2JsonQueryFunction; import org.hibernate.dialect.function.json.H2JsonValueFunction; import org.hibernate.dialect.function.json.HANAJsonArrayFunction; import org.hibernate.dialect.function.json.HANAJsonExistsFunction; @@ -89,22 +90,26 @@ import org.hibernate.dialect.function.json.JsonArrayFunction; import org.hibernate.dialect.function.json.JsonExistsFunction; import org.hibernate.dialect.function.json.JsonObjectFunction; +import org.hibernate.dialect.function.json.JsonQueryFunction; import org.hibernate.dialect.function.json.JsonValueFunction; import org.hibernate.dialect.function.json.MariaDBJsonArrayFunction; import org.hibernate.dialect.function.json.MariaDBJsonValueFunction; import org.hibernate.dialect.function.json.MySQLJsonArrayFunction; import org.hibernate.dialect.function.json.MySQLJsonExistsFunction; 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.OracleJsonArrayFunction; import org.hibernate.dialect.function.json.OracleJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; 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.SQLServerJsonArrayFunction; import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; 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; @@ -3414,6 +3419,55 @@ 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 ) ); + } + + /** + * MySQL json_query() function + */ + public void jsonQuery_mysql() { + functionRegistry.register( "json_query", new MySQLJsonQueryFunction( 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 */ 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/JsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonExistsFunction.java index 83497b09f610..0d5c31c70edc 100644 --- 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 @@ -51,9 +51,7 @@ public JsonExistsFunction( super( "json_exists", FunctionKind.NORMAL, - StandardArgumentsValidators.composite( - new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY ) - ), + new ArgumentTypesValidator( null, IMPLICIT_JSON, STRING ), StandardFunctionReturnTypeResolvers.invariant( typeConfiguration.standardBasicTypeForJavaType( Boolean.class ) ), StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) ); 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 index 88706ce9e6f2..f057613286bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java @@ -53,7 +53,7 @@ public JsonValueFunction( "json_value", FunctionKind.NORMAL, StandardArgumentsValidators.composite( - new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 5 ), IMPLICIT_JSON, STRING, ANY, ANY, ANY ) + new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), IMPLICIT_JSON, STRING, ANY ) ), new CastTargetReturnTypeResolver( typeConfiguration ), StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, JSON, STRING ) 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..bcc6ee8e0f88 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonQueryFunction.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 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.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 { + 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))" ); + } + } +} 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..ad56753f5b5d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.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 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.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_first errors 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" ); + } + sqlAppender.appendSql( "jsonb_path_query_array(" ); + 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( ")#>>'{}'" ); + } +} 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 index 687fbaaaa1fe..7a0663b82b9e 100644 --- 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 @@ -41,7 +41,7 @@ protected void render( arguments.returningType().accept( walker ); } else { - sqlAppender.appendSql( "varchar(max)" ); + sqlAppender.appendSql( "nvarchar(max)" ); } sqlAppender.appendSql( ' ' ); final JsonPathPassingClause passingClause = arguments.passingClause(); 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 3c13abb6bd81..43c398fc1cc3 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 @@ -3713,6 +3713,20 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @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. * diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java index 77186f7e7db6..cd91ffc10dab 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java @@ -56,7 +56,7 @@ public interface JpaJsonExistsExpression extends JpaExpression { JpaJsonExistsExpression passing(String parameterName, Expression expression); /** - * The behavior of the json value expression when a JSON processing error occurs. + * The behavior of the json exists expression when a JSON processing error occurs. */ enum ErrorBehavior { /** 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/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index ea53f3e79f13..f3729e818140 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 @@ -39,6 +39,7 @@ 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; @@ -3376,6 +3377,18 @@ public JpaJsonValueExpression jsonValue( 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) { 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 6c909bb5f14b..a13490c4f114 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 @@ -146,6 +146,7 @@ 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.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; @@ -2745,6 +2746,79 @@ public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContex return jsonValue; } + @Override + public SqmExpression visitJsonQueryFunction(HqlParser.JsonQueryFunctionContext 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) { final SqmExpression jsonDocument = (SqmExpression) ctx.expression( 0 ).accept( this ); 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 318297645923..ecbe32db526d 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 @@ -26,7 +26,6 @@ import org.hibernate.query.criteria.JpaCoalesce; import org.hibernate.query.criteria.JpaCompoundSelection; import org.hibernate.query.criteria.JpaExpression; -import org.hibernate.query.criteria.JpaJsonExistsExpression; import org.hibernate.query.criteria.JpaOrder; import org.hibernate.query.criteria.JpaParameterExpression; import org.hibernate.query.criteria.JpaPredicate; @@ -46,6 +45,7 @@ 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; @@ -631,6 +631,12 @@ SqmJsonValueExpression jsonValue( @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); 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 fbc7da473c05..1b970a23997a 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,7 @@ 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.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; @@ -5335,6 +5336,20 @@ public SqmJsonValueExpression jsonValue( } } + @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 ) ); 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 index 32e29fd4929b..b56e3a0c9136 100644 --- 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 @@ -36,7 +36,7 @@ */ @Incubating public class SqmJsonExistsExpression extends AbstractSqmJsonPathExpression implements JpaJsonExistsExpression { - private @Nullable ErrorBehavior errorBehavior; + private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; public SqmJsonExistsExpression( SqmFunctionDescriptor descriptor, @@ -69,7 +69,7 @@ private SqmJsonExistsExpression( NodeBuilder nodeBuilder, String name, @Nullable Map> passingExpressions, - @Nullable ErrorBehavior errorBehavior) { + ErrorBehavior errorBehavior) { super( descriptor, renderer, @@ -159,18 +159,10 @@ public Expression convertToSqlAst(SqmToSqlAstConverter walker) { if ( jsonPathPassingClause != null ) { arguments.add( jsonPathPassingClause ); } - if ( errorBehavior != null ) { - switch ( errorBehavior ) { - case ERROR: - arguments.add( JsonExistsErrorBehavior.ERROR ); - break; - case TRUE: - arguments.add( JsonExistsErrorBehavior.TRUE ); - break; - case FALSE: - arguments.add( JsonExistsErrorBehavior.FALSE ); - break; - } + switch ( errorBehavior ) { + case ERROR -> arguments.add( JsonExistsErrorBehavior.ERROR ); + case TRUE -> arguments.add( JsonExistsErrorBehavior.TRUE ); + case FALSE -> arguments.add( JsonExistsErrorBehavior.FALSE ); } return new SelfRenderingFunctionSqlAstExpression( getFunctionName(), @@ -189,18 +181,10 @@ public void appendHqlString(StringBuilder sb) { getArguments().get( 1 ).appendHqlString( sb ); appendPassingExpressionHqlString( sb ); - if ( errorBehavior != null ) { - switch ( errorBehavior ) { - case ERROR: - sb.append( " error on error" ); - break; - case TRUE: - sb.append( " true on error" ); - break; - case FALSE: - sb.append( " false on error" ); - break; - } + 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/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 index 7b66ec67eeeb..ab07af0dedc9 100644 --- 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 @@ -38,9 +38,9 @@ */ @Incubating public class SqmJsonValueExpression extends AbstractSqmJsonPathExpression implements JpaJsonValueExpression { - private @Nullable ErrorBehavior errorBehavior; + private ErrorBehavior errorBehavior = ErrorBehavior.UNSPECIFIED; private SqmExpression errorDefaultExpression; - private @Nullable EmptyBehavior emptyBehavior; + private EmptyBehavior emptyBehavior = EmptyBehavior.UNSPECIFIED; private SqmExpression emptyDefaultExpression; public SqmJsonValueExpression( @@ -74,9 +74,9 @@ private SqmJsonValueExpression( NodeBuilder nodeBuilder, String name, @Nullable Map> passingExpressions, - @Nullable ErrorBehavior errorBehavior, + ErrorBehavior errorBehavior, SqmExpression errorDefaultExpression, - @Nullable EmptyBehavior emptyBehavior, + EmptyBehavior emptyBehavior, SqmExpression emptyDefaultExpression) { super( descriptor, @@ -222,35 +222,19 @@ public Expression convertToSqlAst(SqmToSqlAstConverter walker) { if ( jsonPathPassingClause != null ) { arguments.add( jsonPathPassingClause ); } - if ( errorBehavior != null ) { - switch ( errorBehavior ) { - case NULL: - arguments.add( JsonValueErrorBehavior.NULL ); - break; - case ERROR: - arguments.add( JsonValueErrorBehavior.ERROR ); - break; - case DEFAULT: - arguments.add( JsonValueErrorBehavior.defaultOnError( - (Expression) errorDefaultExpression.accept( walker ) - ) ); - break; - } + switch ( errorBehavior ) { + case NULL -> arguments.add( JsonValueErrorBehavior.NULL ); + case ERROR -> arguments.add( JsonValueErrorBehavior.ERROR ); + case DEFAULT -> arguments.add( JsonValueErrorBehavior.defaultOnError( + (Expression) errorDefaultExpression.accept( walker ) + ) ); } - if ( emptyBehavior != null ) { - switch ( emptyBehavior ) { - case NULL: - arguments.add( JsonValueEmptyBehavior.NULL ); - break; - case ERROR: - arguments.add( JsonValueEmptyBehavior.ERROR ); - break; - case DEFAULT: - arguments.add( JsonValueEmptyBehavior.defaultOnEmpty( - (Expression) emptyDefaultExpression.accept( walker ) - ) ); - break; - } + 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(), @@ -273,34 +257,22 @@ public void appendHqlString(StringBuilder sb) { sb.append( " returning " ); getArguments().get( 2 ).appendHqlString( sb ); } - if ( errorBehavior != null ) { - switch ( errorBehavior ) { - case NULL: - sb.append( " null on error" ); - break; - case ERROR: - sb.append( " error on error" ); - break; - case DEFAULT: - sb.append( " default " ); - errorDefaultExpression.appendHqlString( sb ); - sb.append( " on error" ); - break; + 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" ); } } - if ( emptyBehavior != null ) { - switch ( emptyBehavior ) { - case NULL: - sb.append( " null on empty" ); - break; - case ERROR: - sb.append( " error on empty" ); - break; - case DEFAULT: - sb.append( " default " ); - emptyDefaultExpression.appendHqlString( sb ); - sb.append( " on empty" ); - break; + 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/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/test/java/org/hibernate/orm/test/fetching/FetchingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/fetching/FetchingTest.java index 34e79889706e..0dd68e4f9bf9 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 @@ -25,7 +25,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; @@ -41,7 +40,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 @@ -168,17 +166,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/JsonQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonQueryTest.java new file mode 100644 index 000000000000..adaa5114aae4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonQueryTest.java @@ -0,0 +1,134 @@ +/* + * 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.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.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.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 +@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/mapping/generated/always/GeneratedAlwaysTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java index 40cef4b0e802..27b212258f3d 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 @@ -34,7 +34,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 index 337dc55d152c..bd63dcc9fc8d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -6,12 +6,15 @@ */ package org.hibernate.orm.test.query.hql; +import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.dialect.HSQLDialect; +import org.hibernate.dialect.OracleDialect; import org.hibernate.type.SqlTypes; import org.hibernate.testing.orm.junit.DialectContext; @@ -21,11 +24,21 @@ import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; -import org.junit.jupiter.api.BeforeAll; +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; @@ -44,7 +57,7 @@ public class JsonFunctionTests { JsonHolder entity; - @BeforeAll + @BeforeEach public void prepareData(SessionFactoryScope scope) { scope.inTransaction( em -> { @@ -58,11 +71,26 @@ public void prepareData(SessionFactoryScope scope) { 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); } ); } + @AfterEach + public void cleanupData(SessionFactoryScope scope) { + scope.inTransaction( + em -> em.createMutationQuery( "delete from JsonHolder" ).executeUpdate() + ); + } + @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonValue.class) public void testJsonValue(SessionFactoryScope scope) { @@ -114,6 +142,48 @@ public void testJsonValueExpression(SessionFactoryScope scope) { ); } + @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) { @@ -224,6 +294,7 @@ public void testJsonObjectAndArray(SessionFactoryScope scope) { @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 -> { @@ -266,6 +337,52 @@ private static Object[] parseArray(String json) { } } + 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 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 da5587f1d965..eaaf737e923a 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 @@ -322,6 +322,8 @@ protected Ignore convertSkipToIgnore(FrameworkMethod frameworkMethod) { effectiveSkipForDialect.microVersion(), dialect, effectiveSkipForDialect.matchSubTypes() + ? DialectFilterExtension.VersionMatchMode.SAME_OR_OLDER + : DialectFilterExtension.VersionMatchMode.SAME ); if ( versionsMatch ) { @@ -474,6 +476,8 @@ private boolean isDialectMatchingRequired2(Collection Date: Tue, 10 Sep 2024 19:30:58 +0200 Subject: [PATCH 05/15] HHH-18496 Add json_arrayagg --- .../chapters/query/hql/QueryLanguage.adoc | 44 ++++++ .../query/hql/extras/json_arrayagg_bnf.txt | 5 + .../community/dialect/DB2LegacyDialect.java | 1 + .../community/dialect/H2LegacyDialect.java | 1 + .../community/dialect/HSQLLegacyDialect.java | 1 + .../dialect/MariaDBLegacyDialect.java | 2 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 3 + .../dialect/SQLServerLegacyDialect.java | 1 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 1 + .../org/hibernate/grammars/hql/HqlParser.g4 | 12 +- .../org/hibernate/dialect/DB2Dialect.java | 1 + .../java/org/hibernate/dialect/H2Dialect.java | 1 + .../org/hibernate/dialect/HANADialect.java | 1 + .../org/hibernate/dialect/HSQLDialect.java | 1 + .../org/hibernate/dialect/MariaDBDialect.java | 2 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 11 +- .../hibernate/dialect/PostgreSQLDialect.java | 3 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 88 +++++++++++ .../json/DB2JsonArrayAggFunction.java | 99 ++++++++++++ .../function/json/H2JsonArrayAggFunction.java | 27 ++++ .../json/HANAJsonArrayAggFunction.java | 109 +++++++++++++ .../json/HSQLJsonArrayAggFunction.java | 27 ++++ .../function/json/JsonArrayAggFunction.java | 143 ++++++++++++++++++ .../json/MariaDBJsonArrayAggFunction.java | 98 ++++++++++++ .../json/MariaDBJsonQueryFunction.java | 107 +++++++++++++ .../json/MySQLJsonArrayAggFunction.java | 98 ++++++++++++ .../function/json/MySQLJsonQueryFunction.java | 50 ++++++ .../json/OracleJsonArrayAggFunction.java | 39 +++++ .../json/PostgreSQLJsonArrayAggFunction.java | 102 +++++++++++++ .../json/PostgreSQLJsonQueryFunction.java | 24 ++- .../json/SQLServerJsonArrayAggFunction.java | 101 +++++++++++++ .../util/config/ConfigurationHelper.java | 17 +++ .../criteria/HibernateCriteriaBuilder.java | 72 +++++++++ .../spi/HibernateCriteriaBuilderDelegate.java | 48 ++++++ .../hql/internal/SemanticQueryBuilder.java | 24 +++ .../org/hibernate/query/sqm/NodeBuilder.java | 24 +++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 73 +++++++++ .../function/ArgumentTypesValidator.java | 6 +- .../function/json/JsonArrayAggregateTest.java | 52 +++++++ .../test/function/json/JsonExistsTest.java | 2 + .../orm/test/query/hql/JsonFunctionTests.java | 56 ++++++- .../orm/junit/DialectFeatureChecks.java | 8 +- 46 files changed, 1573 insertions(+), 16 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_arrayagg_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonQueryFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAggregateTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 5c3c764aa0e8..b9d2eb38a193 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1634,6 +1634,7 @@ The following functions deal with SQL JSON types, which are not supported on eve | `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 |=== @@ -1919,6 +1920,49 @@ Depending on the database, an error might still be thrown even without that, but 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-user-defined-functions]] ==== Native and user-defined functions 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/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 df4ceba75404..25b07d7c8764 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 @@ -435,6 +435,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_db2(); + functionFactory.jsonArrayAgg_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 9ae161523965..ec4c43ab35e1 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 @@ -405,6 +405,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonValue_h2(); functionFactory.jsonQuery_h2(); functionFactory.jsonExists_h2(); + functionFactory.jsonArrayAgg_h2(); } } else { 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 1b958ec16d52..468dd3d289ca 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 @@ -273,6 +273,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 2, 7 ) ) { functionFactory.jsonObject_hsqldb(); functionFactory.jsonArray_hsqldb(); + functionFactory.jsonArrayAgg_hsqldb(); } //trim() requires parameters to be cast when used as trim character 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 10702701bba8..24ac410afb5c 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 @@ -92,6 +92,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio ); commonFunctionFactory.jsonValue_mariadb(); commonFunctionFactory.jsonArray_mariadb(); + commonFunctionFactory.jsonQuery_mariadb(); + commonFunctionFactory.jsonArrayAgg_mariadb(); if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) 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 d0c3112e78f1..993f702dd8c6 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 @@ -658,6 +658,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_mysql(); functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); + functionFactory.jsonArrayAgg_mysql(); } } 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 a1beebb068aa..896d2e05851b 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 @@ -327,6 +327,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_oracle(); functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); + functionFactory.jsonArrayAgg_oracle(); } } 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 eb681ef396a9..71161de9b6b2 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 @@ -627,6 +627,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); + functionFactory.jsonArrayAgg(); } else { functionFactory.jsonValue_postgresql(); @@ -635,10 +636,12 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); functionFactory.jsonArray(); + functionFactory.jsonArrayAgg(); } else { functionFactory.jsonObject_postgresql(); functionFactory.jsonArray_postgresql(); + functionFactory.jsonArrayAgg_postgresql(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index af8f19e64ec1..dcc37ca6b968 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 @@ -409,6 +409,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); + functionFactory.jsonArrayAgg_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 37317d6fd86e..7de06724d599 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 @@ -225,6 +225,7 @@ 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_QUERY : [jJ] [sS] [oO] [nN] '_' [qQ] [uU] [eE] [rR] [yY]; 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 e7cd44a7d363..8630a73a56cf 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 @@ -1627,6 +1627,7 @@ jsonFunction | jsonObjectFunction | jsonQueryFunction | jsonValueFunction + | jsonArrayAggFunction ; /** @@ -1645,7 +1646,8 @@ jsonValueReturningClause ; jsonValueOnErrorOrEmptyClause - : ( ERROR | NULL | ( DEFAULT expression ) ) ON (ERROR|EMPTY); + : ( ERROR | NULL | ( DEFAULT expression ) ) ON (ERROR|EMPTY) + ; /** * The 'json_query()' function @@ -1695,6 +1697,13 @@ jsonNullClause : (ABSENT|NULL) ON NULL ; +/** + * The 'json_arrayagg()' function + */ +jsonArrayAggFunction + : JSON_ARRAYAGG LEFT_PAREN expressionOrPredicate jsonNullClause? orderByClause? RIGHT_PAREN filterClause? + ; + /** * Support for "soft" keywords which may be used as identifiers * @@ -1793,6 +1802,7 @@ jsonNullClause | IS | JOIN | JSON_ARRAY + | JSON_ARRAYAGG | JSON_EXISTS | JSON_OBJECT | JSON_QUERY 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 ca819eef7b32..8aba99647829 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -421,6 +421,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_no_passing(); functionFactory.jsonObject_db2(); functionFactory.jsonArray_db2(); + functionFactory.jsonArrayAgg_db2(); } } 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 8d903bbe465f..316f0fd3b9a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -348,6 +348,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonValue_h2(); functionFactory.jsonQuery_h2(); functionFactory.jsonExists_h2(); + functionFactory.jsonArrayAgg_h2(); } } 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 28f578596431..8898e9eb3ea6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -498,6 +498,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio // Introduced in 2.0 SPS 04 functionFactory.jsonObject_hana(); functionFactory.jsonArray_hana(); + functionFactory.jsonArrayAgg_hana(); } } } 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 7b83750a31f9..62c060e789c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -208,6 +208,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 2, 7 ) ) { functionFactory.jsonObject_hsqldb(); functionFactory.jsonArray_hsqldb(); + functionFactory.jsonArrayAgg_hsqldb(); } //trim() requires parameters to be cast when used as trim character 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 d39ec3df9c76..53f0397c9443 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -95,6 +95,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio ); commonFunctionFactory.jsonValue_mariadb(); commonFunctionFactory.jsonArray_mariadb(); + commonFunctionFactory.jsonQuery_mariadb(); + commonFunctionFactory.jsonArrayAgg_mariadb(); commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) .setInvariantType( functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) ) 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 e014eb69f42e..4a4850935bd4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -643,6 +643,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_mysql(); functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); + functionFactory.jsonArrayAgg_mysql(); } @Override 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 aef4908d82d2..049352b9a41e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -277,6 +277,7 @@ protected DatabaseVersion getMinimumSupportedVersion() { @Override public int getPreferredSqlTypeCodeForBoolean() { + // starting 23c we support Boolean type natively return getVersion().isSameOrAfter( 23 ) ? super.getPreferredSqlTypeCodeForBoolean() : Types.BIT; } @@ -404,6 +405,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_oracle(); functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); + functionFactory.jsonArrayAgg_oracle(); } @Override @@ -802,10 +804,12 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR 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 @@ -945,8 +949,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 ); 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 fa1f194f98a8..80f52accc901 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -588,6 +588,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); + functionFactory.jsonArrayAgg(); } else { functionFactory.jsonValue_postgresql(); @@ -596,10 +597,12 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); functionFactory.jsonArray(); + functionFactory.jsonArrayAgg(); } else { functionFactory.jsonObject_postgresql(); functionFactory.jsonArray_postgresql(); + functionFactory.jsonArrayAgg_postgresql(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 1848d80abba4..97a271f89f0b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -427,6 +427,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); + functionFactory.jsonArrayAgg_sqlserver(); } if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.leastGreatest(); 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 651b4cd7e37f..b70f685b0f6d 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 @@ -77,35 +77,46 @@ import org.hibernate.dialect.function.array.PostgreSQLArrayPositionsFunction; import org.hibernate.dialect.function.array.PostgreSQLArrayTrimEmulation; 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.DB2JsonObjectFunction; +import org.hibernate.dialect.function.json.H2JsonArrayAggFunction; import org.hibernate.dialect.function.json.H2JsonExistsFunction; 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.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.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.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.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.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.SQLServerJsonObjectFunction; @@ -3454,6 +3465,13 @@ 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 */ @@ -3642,4 +3660,74 @@ public void jsonArray_mariadb() { public void jsonArray_postgresql() { functionRegistry.register( "json_array", new PostgreSQLJsonArrayFunction( typeConfiguration ) ); } + + /** + * Standard json_arrayagg() function + */ + public void jsonArrayAgg() { + functionRegistry.register( "json_arrayagg", new JsonArrayAggFunction( true, 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() { + functionRegistry.register( "json_arrayagg", new PostgreSQLJsonArrayAggFunction( 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 ) ); + } } 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/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/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/HSQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayAggFunction.java new file mode 100644 index 000000000000..e5f8393c5406 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayAggFunction.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; + +/** + * HSQLDB json_arrayagg function. + */ +public class HSQLJsonArrayAggFunction extends JsonArrayAggFunction { + + public HSQLJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + protected void renderReturningClause(SqlAppender sqlAppender, Expression arg, SqlAstTranslator translator) { + // No returning clause needed + } +} 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/MariaDBJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAggFunction.java new file mode 100644 index 000000000000..a6e8ff3af35a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAggFunction.java @@ -0,0 +1,98 @@ +/* + * 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 + sqlAppender.appendSql( "json_extract(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( "),'$[0]')" ); + } +} 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/MySQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayAggFunction.java new file mode 100644 index 000000000000..e162f623e0aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayAggFunction.java @@ -0,0 +1,98 @@ +/* + * 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 + sqlAppender.appendSql( "json_extract(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( "),'$[0]')" ); + } +} 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 index bcc6ee8e0f88..e66a75970474 100644 --- 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 @@ -12,6 +12,7 @@ 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; /** @@ -37,6 +38,15 @@ protected void render( 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( "," ); @@ -52,6 +62,46 @@ protected void render( ); } 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/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/PostgreSQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAggFunction.java new file mode 100644 index 000000000000..a15d14492a7d --- /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 { + + public PostgreSQLJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( true, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !supportsFilter; + 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; + } + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "_strict" ); + } + 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; + } + 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( ')' ); + + if ( !caseWrapper && filter != null ) { + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + translator.getCurrentClauseStack().pop(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java index ad56753f5b5d..4782d9912a90 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java @@ -18,6 +18,7 @@ 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; @@ -36,14 +37,24 @@ protected void render( JsonQueryArguments arguments, ReturnableType returnType, SqlAstTranslator walker) { - // jsonb_path_query_first errors by default + // 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" ); } - sqlAppender.appendSql( "jsonb_path_query_array(" ); + 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(" ); @@ -75,7 +86,12 @@ protected void render( } sqlAppender.append( ')' ); } - // Unquote the value - sqlAppender.appendSql( ")#>>'{}'" ); + + 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/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/internal/util/config/ConfigurationHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java index a257acfb6923..6c81301b9dd8 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 @@ -15,10 +15,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; @@ -509,6 +511,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 43c398fc1cc3..f4d956a2f357 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 @@ -3775,6 +3775,78 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @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); + @Override JpaPredicate and(List restrictions); 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 f3729e818140..f6cf4a041972 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 @@ -3424,4 +3424,52 @@ public JpaExpression jsonArray(Expression... values) { 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 ); + } } 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 a13490c4f114..ef985b44047b 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 @@ -2907,6 +2907,30 @@ public SqmExpression visitJsonObjectFunction(HqlParser.JsonObjectFunctionCont ); } + @Override + public Object visitJsonArrayAggFunction(HqlParser.JsonArrayAggFunctionContext 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 SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { final boolean negated = ctx.NOT() != null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index ecbe32db526d..97e121349b8a 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 @@ -655,6 +655,30 @@ SqmJsonValueExpression jsonValue( @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); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides 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 1b970a23997a..be69acd85483 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 @@ -191,6 +191,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; @@ -5388,6 +5389,78 @@ public SqmExpression jsonArray(Expression... values) { ); } + @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 ) ); + } + + 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 ); 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 744c80d6ad9a..ecf85aaf50cf 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 @@ -249,10 +249,8 @@ private static boolean isCompatible(FunctionParameterType type, JdbcType jdbcTyp case DATE -> jdbcType.hasDatePart(); case TIME -> jdbcType.hasTimePart(); case SPATIAL -> jdbcType.isSpatial(); - case JSON: - return jdbcType.isJson(); - case IMPLICIT_JSON: - return jdbcType.isImplicitJson(); + case JSON -> jdbcType.isJson(); + case IMPLICIT_JSON -> jdbcType.isImplicitJson(); default -> true; // TODO: should we throw here? }; } 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..169b4843c876 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAggregateTest.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 . + */ +package org.hibernate.orm.test.function.json; + +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.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@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/JsonExistsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonExistsTest.java index d35207e9dfba..834ebef32772 100644 --- 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 @@ -12,6 +12,7 @@ import org.hibernate.HibernateException; import org.hibernate.JDBCException; import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.OracleDialect; import org.hibernate.sql.exec.ExecutionException; import org.hibernate.testing.orm.junit.DialectFeatureChecks; @@ -70,6 +71,7 @@ public void testSimple(SessionFactoryScope scope) { } @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[] diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index bd63dcc9fc8d..44fd74bc0197 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -7,6 +7,7 @@ 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; @@ -17,6 +18,7 @@ import org.hibernate.dialect.OracleDialect; 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; @@ -50,7 +52,10 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -@DomainModel( annotatedClasses = JsonFunctionTests.JsonHolder.class) +@DomainModel( annotatedClasses = { + JsonFunctionTests.JsonHolder.class, + EntityOfBasics.class +}) @SessionFactory @Jira("https://hibernate.atlassian.net/browse/HHH-18496") public class JsonFunctionTests { @@ -80,6 +85,16 @@ public void prepareData(SessionFactoryScope scope) { ) ); em.persist(entity); + + EntityOfBasics e1 = new EntityOfBasics(); + e1.setId( 1 ); + e1.setTheString( "Dog" ); + EntityOfBasics e2 = new EntityOfBasics(); + e2.setId( 2 ); + e2.setTheString( "Cat" ); + + em.persist( e1 ); + em.persist( e2 ); } ); } @@ -87,7 +102,10 @@ public void prepareData(SessionFactoryScope scope) { @AfterEach public void cleanupData(SessionFactoryScope scope) { scope.inTransaction( - em -> em.createMutationQuery( "delete from JsonHolder" ).executeUpdate() + em -> { + em.createMutationQuery( "delete from EntityOfBasics" ).executeUpdate(); + em.createMutationQuery( "delete from JsonHolder" ).executeUpdate(); + } ); } @@ -316,6 +334,40 @@ public void testJsonExists(SessionFactoryScope scope) { ); } + @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 ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 3bb6afed760f..6e39b3df28ce 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -779,6 +779,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonArrayAgg implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_arrayagg" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; @@ -1534,7 +1540,7 @@ public Set getContributors() { } @Override - public NamedObjectRepository buildNamedQueryRepository(SessionFactoryImplementor sessionFactory) { + public NamedObjectRepository buildNamedQueryRepository() { return null; } From 55ca0a9a9b2dedcd730a43e07cffabcf5c5bdebe Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 11 Sep 2024 20:01:49 +0200 Subject: [PATCH 06/15] HHH-18496 Add json_objectagg --- .../chapters/query/hql/QueryLanguage.adoc | 47 +++++ .../query/hql/extras/json_objectagg_bnf.txt | 9 + .../dialect/CockroachLegacyDialect.java | 5 +- .../community/dialect/DB2LegacyDialect.java | 1 + .../community/dialect/H2LegacyDialect.java | 1 + .../community/dialect/HSQLLegacyDialect.java | 1 + .../dialect/MariaDBLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 9 +- .../dialect/SQLServerLegacyDialect.java | 1 + .../org/hibernate/grammars/hql/HqlLexer.g4 | 2 + .../org/hibernate/grammars/hql/HqlParser.g4 | 14 ++ .../hibernate/dialect/CockroachDialect.java | 5 +- .../org/hibernate/dialect/DB2Dialect.java | 1 + .../java/org/hibernate/dialect/H2Dialect.java | 1 + .../org/hibernate/dialect/HANADialect.java | 1 + .../org/hibernate/dialect/HSQLDialect.java | 1 + .../org/hibernate/dialect/MariaDBDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../hibernate/dialect/PostgreSQLDialect.java | 9 +- .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 84 ++++++++- .../json/CockroachDBJsonExistsFunction.java | 88 +++++++++ .../json/CockroachDBJsonQueryFunction.java | 107 +++++++++++ .../json/DB2JsonObjectAggFunction.java | 91 +++++++++ .../json/H2JsonObjectAggFunction.java | 29 +++ .../json/HANAJsonObjectAggFunction.java | 88 +++++++++ .../function/json/JsonObjectAggFunction.java | 172 ++++++++++++++++++ .../json/MariaDBJsonArrayAggFunction.java | 6 + .../json/MariaDBJsonObjectAggFunction.java | 41 +++++ .../json/MySQLJsonArrayAggFunction.java | 6 + .../json/MySQLJsonObjectAggFunction.java | 81 +++++++++ .../json/PostgreSQLJsonArrayAggFunction.java | 106 +++++------ .../json/PostgreSQLJsonArrayFunction.java | 4 + .../json/PostgreSQLJsonObjectAggFunction.java | 82 +++++++++ .../json/PostgreSQLJsonObjectFunction.java | 4 + .../json/SQLServerJsonObjectAggFunction.java | 80 ++++++++ .../criteria/HibernateCriteriaBuilder.java | 64 +++++++ .../spi/HibernateCriteriaBuilderDelegate.java | 51 ++++++ .../hql/internal/SemanticQueryBuilder.java | 33 ++++ .../org/hibernate/query/sqm/NodeBuilder.java | 24 +++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 67 +++++++ .../SqmJsonObjectAggUniqueKeysBehavior.java | 63 +++++++ .../JsonObjectAggUniqueKeysBehavior.java | 24 +++ .../json/JsonObjectAggregateTest.java | 66 +++++++ .../orm/test/query/hql/JsonFunctionTests.java | 96 +++++++++- .../orm/junit/DialectFeatureChecks.java | 8 + 47 files changed, 1607 insertions(+), 71 deletions(-) create mode 100644 documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/json_objectagg_bnf.txt create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/DB2JsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJsonObjectAggUniqueKeysBehavior.java create mode 100644 hibernate-core/src/main/java/org/hibernate/sql/ast/tree/expression/JsonObjectAggUniqueKeysBehavior.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectAggregateTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index b9d2eb38a193..939b72d8bc4a 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1635,6 +1635,7 @@ The following functions deal with SQL JSON types, which are not supported on eve | `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 |=== @@ -1963,6 +1964,52 @@ include::{json-example-dir-hql}/JsonArrayAggregateTest.java[tags=hql-json-arraya ---- ==== +[[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_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/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 23a95630ff7d..35ceee43dd65 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 @@ -502,9 +502,12 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_postgresql(); functionFactory.jsonValue_cockroachdb(); + functionFactory.jsonQuery_cockroachdb(); + functionFactory.jsonExists_cockroachdb(); functionFactory.jsonObject_postgresql(); - functionFactory.jsonExists_postgresql(); functionFactory.jsonArray_postgresql(); + functionFactory.jsonArrayAgg_postgresql( false ); + functionFactory.jsonObjectAgg_postgresql( false ); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index 25b07d7c8764..6a414d6d7204 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 @@ -436,6 +436,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio 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 ec4c43ab35e1..768d5aa9481c 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 @@ -406,6 +406,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonQuery_h2(); functionFactory.jsonExists_h2(); functionFactory.jsonArrayAgg_h2(); + functionFactory.jsonObjectAgg_h2(); } } else { 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 468dd3d289ca..07c4ee8e16a7 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 @@ -274,6 +274,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_hsqldb(); functionFactory.jsonArray_hsqldb(); functionFactory.jsonArrayAgg_hsqldb(); + functionFactory.jsonObjectAgg_h2(); } //trim() requires parameters to be cast when used as trim character 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 24ac410afb5c..e7666c0bf8e2 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 @@ -94,6 +94,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio 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 ()" ) 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 993f702dd8c6..a3b0bb1f6ed9 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 @@ -659,6 +659,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); functionFactory.jsonArrayAgg_mysql(); + functionFactory.jsonObjectAgg_mysql(); } } 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 71161de9b6b2..14f6fc74ac3c 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 @@ -627,7 +627,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); - functionFactory.jsonArrayAgg(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); } else { functionFactory.jsonValue_postgresql(); @@ -636,12 +637,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); functionFactory.jsonArray(); - functionFactory.jsonArrayAgg(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); } else { functionFactory.jsonObject_postgresql(); functionFactory.jsonArray_postgresql(); - functionFactory.jsonArrayAgg_postgresql(); + functionFactory.jsonArrayAgg_postgresql( false ); + functionFactory.jsonObjectAgg_postgresql( false ); } } 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 dcc37ca6b968..5d2ab5fd3a9d 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 @@ -410,6 +410,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio 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 7de06724d599..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 @@ -228,6 +228,7 @@ 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]; @@ -316,6 +317,7 @@ 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]; 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 8630a73a56cf..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 @@ -1628,6 +1628,7 @@ jsonFunction | jsonQueryFunction | jsonValueFunction | jsonArrayAggFunction + | jsonObjectAggFunction ; /** @@ -1704,6 +1705,17 @@ 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 * @@ -1805,6 +1817,7 @@ jsonArrayAggFunction | JSON_ARRAYAGG | JSON_EXISTS | JSON_OBJECT + | JSON_OBJECTAGG | JSON_QUERY | JSON_VALUE | KEY @@ -1894,6 +1907,7 @@ jsonArrayAggFunction | UNBOUNDED | UNCONDITIONAL | UNION + | UNIQUE | UPDATE | USING | VALUE 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 972a2fe8803c..dc1edf76b333 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -469,9 +469,12 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayToString_postgresql(); functionFactory.jsonValue_cockroachdb(); - functionFactory.jsonExists_postgresql(); + 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)" ) 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 8aba99647829..edd7bac4f248 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -422,6 +422,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_db2(); functionFactory.jsonArray_db2(); functionFactory.jsonArrayAgg_db2(); + functionFactory.jsonObjectAgg_db2(); } } 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 316f0fd3b9a4..3d4fda3d4765 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -349,6 +349,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonQuery_h2(); functionFactory.jsonExists_h2(); functionFactory.jsonArrayAgg_h2(); + functionFactory.jsonObjectAgg_h2(); } } 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 8898e9eb3ea6..04d2581bcf21 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANADialect.java @@ -499,6 +499,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_hana(); functionFactory.jsonArray_hana(); functionFactory.jsonArrayAgg_hana(); + functionFactory.jsonObjectAgg_hana(); } } } 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 62c060e789c5..5c2dccb5924b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -209,6 +209,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_hsqldb(); functionFactory.jsonArray_hsqldb(); functionFactory.jsonArrayAgg_hsqldb(); + functionFactory.jsonObjectAgg_h2(); } //trim() requires parameters to be cast when used as trim character 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 53f0397c9443..53d624a16e0d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -97,6 +97,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio 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 ) ) 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 4a4850935bd4..3e060ed6a690 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -644,6 +644,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_mysql(); functionFactory.jsonArray_mysql(); functionFactory.jsonArrayAgg_mysql(); + functionFactory.jsonObjectAgg_mysql(); } @Override 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 80f52accc901..a93f77be5215 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -588,7 +588,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); - functionFactory.jsonArrayAgg(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); } else { functionFactory.jsonValue_postgresql(); @@ -597,12 +598,14 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.jsonObject(); functionFactory.jsonArray(); - functionFactory.jsonArrayAgg(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); } else { functionFactory.jsonObject_postgresql(); functionFactory.jsonArray_postgresql(); - functionFactory.jsonArrayAgg_postgresql(); + functionFactory.jsonArrayAgg_postgresql( false ); + functionFactory.jsonObjectAgg_postgresql( false ); } } 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 97a271f89f0b..4bf64348b559 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -428,6 +428,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio 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/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index b70f685b0f6d..6c019dcc84a4 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 @@ -76,17 +76,22 @@ 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; @@ -94,16 +99,19 @@ 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; @@ -113,12 +121,14 @@ 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; @@ -3458,6 +3468,13 @@ 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 */ @@ -3528,6 +3545,13 @@ 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 */ @@ -3661,13 +3685,6 @@ public void jsonArray_postgresql() { functionRegistry.register( "json_array", new PostgreSQLJsonArrayFunction( typeConfiguration ) ); } - /** - * Standard json_arrayagg() function - */ - public void jsonArrayAgg() { - functionRegistry.register( "json_arrayagg", new JsonArrayAggFunction( true, typeConfiguration ) ); - } - /** * H2 json_arrayagg() function */ @@ -3692,8 +3709,8 @@ public void jsonArrayAgg_oracle() { /** * PostgreSQL json_arrayagg() function */ - public void jsonArrayAgg_postgresql() { - functionRegistry.register( "json_arrayagg", new PostgreSQLJsonArrayAggFunction( typeConfiguration ) ); + public void jsonArrayAgg_postgresql(boolean supportsStandard) { + functionRegistry.register( "json_arrayagg", new PostgreSQLJsonArrayAggFunction( supportsStandard, typeConfiguration ) ); } /** @@ -3730,4 +3747,53 @@ public void jsonArrayAgg_db2() { 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/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/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/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/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/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/MariaDBJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAggFunction.java index a6e8ff3af35a..6c822bd8a332 100644 --- 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 @@ -91,8 +91,14 @@ protected void renderArgument( 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/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/MySQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonArrayAggFunction.java index e162f623e0aa..4e568127df79 100644 --- 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 @@ -91,8 +91,14 @@ protected void renderArgument( 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/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/PostgreSQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAggFunction.java index a15d14492a7d..1e11f35dbe66 100644 --- 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 @@ -27,8 +27,11 @@ */ public class PostgreSQLJsonArrayAggFunction extends JsonArrayAggFunction { - public PostgreSQLJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsStandard; + + public PostgreSQLJsonArrayAggFunction(boolean supportsStandard, TypeConfiguration typeConfiguration) { super( true, typeConfiguration ); + this.supportsStandard = supportsStandard; } @Override @@ -39,64 +42,61 @@ public void render( List withinGroup, ReturnableType returnType, SqlAstTranslator translator) { - final boolean caseWrapper = filter != null && !supportsFilter; - 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; - } - if ( nullBehavior != JsonNullBehavior.NULL ) { - sqlAppender.appendSql( "_strict" ); - } - sqlAppender.appendSql( '(' ); - final SqlAstNode firstArg = sqlAstArguments.get( 0 ); - final Expression arg; - if ( firstArg instanceof Distinct ) { - sqlAppender.appendSql( "distinct " ); - arg = ( (Distinct) firstArg ).getExpression(); + if ( supportsStandard ) { + super.render( sqlAppender, sqlAstArguments, filter, withinGroup, returnType, translator ); } 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." ); + 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; } - 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 ); + 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(); } - translator.getCurrentClauseStack().pop(); - } - sqlAppender.appendSql( ')' ); - - if ( !caseWrapper && filter != null ) { - translator.getCurrentClauseStack().push( Clause.WHERE ); - sqlAppender.appendSql( " filter (where " ); - filter.accept( translator ); sqlAppender.appendSql( ')' ); - translator.getCurrentClauseStack().pop(); + + 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 index 153288b19b2c..9a37181f2441 100644 --- 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 @@ -61,6 +61,10 @@ public void render( 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( ')' ); 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 index 90b4c8779038..3bd07316f386 100644 --- 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 @@ -62,6 +62,10 @@ public void render( 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( ')' ); 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/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index f4d956a2f357..3a21e7380c19 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 @@ -3847,6 +3847,70 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @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/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index f6cf4a041972..6b36b87ebe34 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 @@ -3472,4 +3472,55 @@ public JpaExpression jsonArrayAggWithNulls(Expression value, Predicat 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 ef985b44047b..e68e776473ae 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 @@ -146,6 +146,7 @@ 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; @@ -2931,6 +2932,38 @@ public Object visitJsonArrayAggFunction(HqlParser.JsonArrayAggFunctionContext ct ); } + @Override + public Object visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext 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() + ); + } + @Override public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { final boolean negated = ctx.NOT() != null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 97e121349b8a..262dd33a5828 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 @@ -679,6 +679,30 @@ SqmJsonValueExpression jsonValue( @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/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index be69acd85483..9c7f1eb079f5 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,7 @@ 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; @@ -5434,6 +5435,72 @@ public SqmExpression jsonArrayAggWithNulls(Expression value, JpaOrder 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; 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/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/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..5f9c49392499 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonObjectAggregateTest.java @@ -0,0 +1,66 @@ +/* + * 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.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.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@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/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 44fd74bc0197..2d110961d903 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -12,10 +12,19 @@ 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.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; @@ -49,8 +58,10 @@ 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, @@ -89,9 +100,12 @@ public void prepareData(SessionFactoryScope scope) { 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 ); @@ -266,7 +280,7 @@ public void testJsonObject(SessionFactoryScope scope) { 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 + // HSQLDB bug: https://sourceforge.net/p/hsqldb/bugs/1720/ if ( !( DialectContext.getDialect() instanceof HSQLDialect ) ) { assertFalse( nested.containsKey( "theNull" ) ); } @@ -368,6 +382,86 @@ public void testJsonArrayAggOrderBy(SessionFactoryScope scope) { ); } + @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) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 6e39b3df28ce..39fae56b0487 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -785,6 +785,14 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonObjectAgg implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_objectagg" ) + // Bug in HSQL: https://sourceforge.net/p/hsqldb/bugs/1718/ + && !( dialect instanceof HSQLDialect ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; From 2aaab7307709f95b86a418e2dabf375c7bf887ab Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 11 Sep 2024 20:19:17 +0200 Subject: [PATCH 07/15] HHH-18496 Hide JSON functions behind feature flag --- .../chapters/query/hql/QueryLanguage.adoc | 3 +++ .../internal/SessionFactoryOptionsBuilder.java | 11 +++++++++++ ...bstractDelegatingSessionFactoryOptions.java | 5 +++++ .../boot/spi/SessionFactoryOptions.java | 8 ++++++++ .../java/org/hibernate/cfg/QuerySettings.java | 9 +++++++++ .../hql/internal/SemanticQueryBuilder.java | 18 ++++++++++++++++++ .../query/hql/spi/SqmCreationOptions.java | 7 +++++++ .../query/spi/QueryEngineOptions.java | 5 +++++ .../internal/SqmCreationOptionsStandard.java | 5 +++++ .../function/json/JsonArrayAggregateTest.java | 5 +++++ .../orm/test/function/json/JsonArrayTest.java | 5 +++++ .../orm/test/function/json/JsonExistsTest.java | 4 ++++ .../function/json/JsonObjectAggregateTest.java | 4 ++++ .../orm/test/function/json/JsonObjectTest.java | 5 +++++ .../orm/test/function/json/JsonQueryTest.java | 4 ++++ .../orm/test/function/json/JsonValueTest.java | 4 ++++ .../orm/test/lob/SerializableTypeTest.java | 2 +- .../orm/test/query/hql/JsonFunctionTests.java | 4 ++++ 18 files changed, 107 insertions(+), 1 deletion(-) 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 939b72d8bc4a..40f67107c5aa 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1625,6 +1625,9 @@ include::{array-example-dir-hql}/ArrayToStringTest.java[tags=hql-array-to-string 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 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 57aa2d76f454..f8c9fe7c604a 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 @@ -130,6 +130,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; @@ -276,6 +277,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { private final boolean inClauseParameterPaddingEnabled; private final boolean portableIntegerDivisionEnabled; + private final boolean jsonFunctionsEnabled; private final int queryStatisticsMaxSize; @@ -616,6 +618,10 @@ else if ( jdbcTimeZoneValue != null ) { PORTABLE_INTEGER_DIVISION, configurationSettings ); + this.jsonFunctionsEnabled = getBoolean( + JSON_FUNCTIONS_ENABLED, + configurationSettings + ); this.queryStatisticsMaxSize = getInt( QUERY_STATISTICS_MAX_SIZE, @@ -1246,6 +1252,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/spi/AbstractDelegatingSessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java index 820a10c286bf..496a0f9083e4 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 @@ -421,6 +421,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 8b14e9069a45..74f6fa97ac69 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 @@ -266,6 +266,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 90f29a5643e5..e135523cf51e 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/QuerySettings.java @@ -4,6 +4,7 @@ */ package org.hibernate.cfg; +import org.hibernate.Incubating; import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.query.spi.QueryPlan; @@ -15,6 +16,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/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index e68e776473ae..e87e69c81f9d 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 @@ -32,6 +32,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; @@ -2699,6 +2700,7 @@ public SqmPredicate visitContainsPredicate(HqlParser.ContainsPredicateContext ct @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(); @@ -2749,6 +2751,7 @@ public SqmExpression visitJsonValueFunction(HqlParser.JsonValueFunctionContex @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( @@ -2822,6 +2825,7 @@ public SqmExpression visitJsonQueryFunction(HqlParser.JsonQueryFunctionContex @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 ); @@ -2855,6 +2859,7 @@ public SqmExpression visitJsonExistsFunction(HqlParser.JsonExistsFunctionCont @Override public SqmExpression visitJsonArrayFunction(HqlParser.JsonArrayFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); final HqlParser.JsonNullClauseContext subCtx = ctx.jsonNullClause(); final List argumentContexts = ctx.expressionOrPredicate(); int count = argumentContexts.size(); @@ -2879,6 +2884,7 @@ public SqmExpression visitJsonArrayFunction(HqlParser.JsonArrayFunctionContex @Override public SqmExpression visitJsonObjectFunction(HqlParser.JsonObjectFunctionContext ctx) { + checkJsonFunctionsEnabled( ctx ); final HqlParser.JsonObjectFunctionEntriesContext entries = ctx.jsonObjectFunctionEntries(); final List> arguments; if ( entries == null ) { @@ -2910,6 +2916,7 @@ public SqmExpression visitJsonObjectFunction(HqlParser.JsonObjectFunctionCont @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 ) ); @@ -2934,6 +2941,7 @@ public Object visitJsonArrayAggFunction(HqlParser.JsonArrayAggFunctionContext ct @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 ); @@ -2964,6 +2972,16 @@ public Object visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext ); } + 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 bca297bf1362..26b14ee3a60c 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 @@ -24,6 +24,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/spi/QueryEngineOptions.java b/hibernate-core/src/main/java/org/hibernate/query/spi/QueryEngineOptions.java index a30189d2edc2..11fd6276525b 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 @@ -76,6 +76,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/internal/SqmCreationOptionsStandard.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCreationOptionsStandard.java index 3e34e81be427..b2c93af76900 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 @@ -22,6 +22,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/test/java/org/hibernate/orm/test/function/json/JsonArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAggregateTest.java index 169b4843c876..3cb15e705627 100644 --- 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 @@ -6,12 +6,16 @@ */ 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; /** @@ -19,6 +23,7 @@ */ @DomainModel(standardModels = StandardDomainModel.GAMBIT) @SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonArrayAgg.class) public class JsonArrayAggregateTest { 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 index ab848ee18fd6..7edd9ee8483f 100644 --- 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 @@ -6,11 +6,15 @@ */ 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; /** @@ -18,6 +22,7 @@ */ @DomainModel @SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonArray.class) public class JsonArrayTest { 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 index 834ebef32772..84a373cdf209 100644 --- 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 @@ -11,6 +11,7 @@ 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; @@ -18,8 +19,10 @@ 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; @@ -33,6 +36,7 @@ */ @DomainModel(annotatedClasses = EntityWithJson.class) @SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonExists.class) public class JsonExistsTest { 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 index 5f9c49392499..7b1a9f276bd2 100644 --- 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 @@ -6,6 +6,7 @@ */ 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; @@ -17,8 +18,10 @@ 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; @@ -27,6 +30,7 @@ */ @DomainModel(standardModels = StandardDomainModel.GAMBIT) @SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonObjectAgg.class) public class JsonObjectAggregateTest { 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 index ccc72b1ecf92..e5dfa2f4997d 100644 --- 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 @@ -6,11 +6,15 @@ */ 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; /** @@ -18,6 +22,7 @@ */ @DomainModel @SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonObject.class) public class JsonObjectTest { 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 index adaa5114aae4..c9f7b301df03 100644 --- 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 @@ -11,14 +11,17 @@ 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; @@ -34,6 +37,7 @@ */ @DomainModel(annotatedClasses = EntityWithJson.class) @SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonQuery.class) public class JsonQueryTest { 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 index 14929dc4b715..231d983f1d0b 100644 --- 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 @@ -11,14 +11,17 @@ 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; @@ -34,6 +37,7 @@ */ @DomainModel(annotatedClasses = EntityWithJson.class) @SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonValue.class) public class JsonValueTest { 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 90af3c2742cc..d228da404128 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 @@ -28,7 +28,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/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 2d110961d903..3b904a1f2557 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -17,6 +17,7 @@ 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; @@ -33,8 +34,10 @@ 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; @@ -67,6 +70,7 @@ 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 { From 694680ad4e5be2c3ad8210b4edb131b6bc3e87cb Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 12 Sep 2024 07:00:29 +0200 Subject: [PATCH 08/15] HHH-18496 Fixup some CI issues --- .../dialect/OracleLegacyDialect.java | 20 +--- .../org/hibernate/dialect/OracleDialect.java | 3 + .../dialect/OracleJsonArrayJdbcType.java | 37 ++++++ .../aggregate/OracleAggregateSupport.java | 11 ++ .../jdbc/OracleJsonArrayBlobJdbcType.java | 109 ++++++++++++++++++ .../jdbc/OracleJsonBlobJdbcType.java | 61 +--------- 6 files changed, 167 insertions(+), 74 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java 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 896d2e05851b..e72149d12fd0 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 @@ -21,20 +21,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; @@ -102,9 +89,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; @@ -900,9 +888,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-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 049352b9a41e..74735f241d39 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -89,6 +89,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; @@ -968,9 +969,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/aggregate/OracleAggregateSupport.java b/hibernate-core/src/main/java/org/hibernate/dialect/aggregate/OracleAggregateSupport.java index 86596595208c..78c6a6f31951 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 @@ -41,6 +41,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; @@ -132,6 +133,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( @@ -266,6 +272,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" ) ) { @@ -274,6 +282,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/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 3e34b24809b5..f4be54c890ca 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 @@ -26,16 +26,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 @@ -43,22 +41,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"; @@ -72,50 +59,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 ) { From 5e2232a01aba4e95b6596af59bd20508fcc3361f Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 16 Sep 2024 16:36:19 +0200 Subject: [PATCH 09/15] HHH-18604 Add json_set function --- .../chapters/query/hql/QueryLanguage.adoc | 17 ++++ .../dialect/CockroachLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 1 + .../dialect/SQLServerLegacyDialect.java | 1 + .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 1 + .../hibernate/dialect/PostgreSQLDialect.java | 1 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 43 +++++++++ .../json/AbstractJsonSetFunction.java | 40 +++++++++ .../function/json/OracleJsonSetFunction.java | 44 +++++++++ .../json/PostgreSQLJsonSetFunction.java | 90 +++++++++++++++++++ .../json/SQLServerJsonSetFunction.java | 44 +++++++++ .../criteria/HibernateCriteriaBuilder.java | 32 +++++++ .../spi/HibernateCriteriaBuilderDelegate.java | 24 +++++ .../org/hibernate/query/sqm/NodeBuilder.java | 12 +++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 25 ++++++ .../orm/test/function/json/JsonSetTest.java | 46 ++++++++++ .../orm/test/query/hql/JsonFunctionTests.java | 32 +++++++ .../orm/junit/DialectFeatureChecks.java | 6 ++ 23 files changed, 465 insertions(+) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 40f67107c5aa..0e2ffd6228b8 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1639,6 +1639,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_query()` | Queries non-scalar values by JSON path in a JSON document | `json_arrayagg()` | Creates a JSON array by aggregating values | `json_objectagg()` | Creates a JSON object by aggregating values +| `json_set()` | Inserts/Replaces a value by JSON path within a JSON document |=== @@ -2013,6 +2014,22 @@ include::{json-example-dir-hql}/JsonObjectAggregateTest.java[tags=hql-json-objec WARNING: Some databases like e.g. MySQL, SAP HANA, DB2 and SQL Server do not support raising an error on duplicate keys. +[[hql-json-set-function]] +===== `json_set()` + +Inserts/Replaces a value by JSON path within a JSON document. +The function takes 3 arguments, the json document, the json path and the new value to set/insert. + +[[hql-json-set-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonSetTest.java[tags=hql-json-set-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions 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 35ceee43dd65..8f4704251d02 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 @@ -508,6 +508,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray_postgresql(); functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); + functionFactory.jsonSet_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index a3b0bb1f6ed9..5a188b55e92e 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 @@ -660,6 +660,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray_mysql(); functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); + functionFactory.jsonSet_mysql(); } } 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 e72149d12fd0..a7c0e8cc6a3d 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 @@ -316,6 +316,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonSet_oracle(); } } 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 14f6fc74ac3c..9ff3b2ea7127 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 @@ -647,6 +647,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObjectAgg_postgresql( false ); } } + functionFactory.jsonSet_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); 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 5d2ab5fd3a9d..5c2d74b55b6e 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 @@ -406,6 +406,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); + functionFactory.jsonSet_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 dc1edf76b333..b18965483c83 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -475,6 +475,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray_postgresql(); functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); + functionFactory.jsonSet_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 3e060ed6a690..e5921f472395 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -645,6 +645,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray_mysql(); functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); + functionFactory.jsonSet_mysql(); } @Override 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 74735f241d39..ac41fe71a898 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -407,6 +407,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonSet_oracle(); } @Override 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 a93f77be5215..7092c9806731 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -608,6 +608,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObjectAgg_postgresql( false ); } } + functionFactory.jsonSet_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 4bf64348b559..2fb7e72c7309 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -424,6 +424,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); + functionFactory.jsonSet_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 6c019dcc84a4..857243b6e9f4 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 @@ -118,12 +118,14 @@ 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.OracleJsonSetFunction; 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.PostgreSQLJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; @@ -131,15 +133,18 @@ 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.SQLServerJsonSetFunction; 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.FunctionParameterType; 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.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.type.BasicType; import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -3796,4 +3801,42 @@ public void jsonObjectAgg_hana() { public void jsonObjectAgg_db2() { functionRegistry.register( "json_objectagg", new DB2JsonObjectAggFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_set() function + */ + public void jsonSet_postgresql() { + functionRegistry.register( "json_set", new PostgreSQLJsonSetFunction( typeConfiguration ) ); + } + + /** + * MySQL json_set() function + */ + public void jsonSet_mysql() { + functionRegistry.namedDescriptorBuilder( "json_set" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_set() function + */ + public void jsonSet_oracle() { + functionRegistry.register( "json_set", new OracleJsonSetFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_set() function + */ + public void jsonSet_sqlserver() { + functionRegistry.register( "json_set", new SQLServerJsonSetFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java new file mode 100644 index 000000000000..b03274d18845 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.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.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_set function. + */ +public abstract class AbstractJsonSetFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonSetFunction(TypeConfiguration typeConfiguration) { + super( + "json_set", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java new file mode 100644 index 000000000000..60e906bcbedd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java @@ -0,0 +1,44 @@ +/* + * 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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_set function. + */ +public class OracleJsonSetFunction extends AbstractJsonSetFunction { + + public OracleJsonSetFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",set " ); + jsonPath.accept( translator ); + sqlAppender.appendSql( '=' ); + value.accept( translator ); + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java new file mode 100644 index 000000000000..33e952a72fcd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java @@ -0,0 +1,90 @@ +/* + * 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.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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_set function. + */ +public class PostgreSQLJsonSetFunction extends AbstractJsonSetFunction { + + public PostgreSQLJsonSetFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "jsonb_set(" ); + final boolean needsCast = !isJsonType( json ) && json instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[]," ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( translator ); + 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( ",true)" ); + } + + private boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java new file mode 100644 index 000000000000..b9660ca2cee7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java @@ -0,0 +1,44 @@ +/* + * 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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_set function. + */ +public class SQLServerJsonSetFunction extends AbstractJsonSetFunction { + + public SQLServerJsonSetFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_modify(" ); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ',' ); + value.accept( translator ); + sqlAppender.appendSql( ')' ); + } +} 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 3a21e7380c19..c3229e7c7c7d 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 @@ -3911,6 +3911,38 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @Incubating JpaExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value, Predicate filter); + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value); + + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value); + + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Object value); + + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value); + @Override JpaPredicate and(List restrictions); 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 6b36b87ebe34..c4a024e4e122 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 @@ -3523,4 +3523,28 @@ public JpaExpression jsonObjectAggWithUniqueKeysAndNulls( Predicate filter) { return criteriaBuilder.jsonObjectAggWithUniqueKeysAndNulls( key, value, filter ); } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Object value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } } 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 262dd33a5828..02392cc95a66 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 @@ -703,6 +703,18 @@ SqmJsonValueExpression jsonValue( @Override SqmExpression jsonObjectAgg(Expression key, Expression value, Predicate filter); + @Override + SqmExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value); + + @Override + SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Object value); + + @Override + SqmExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value); + + @Override + SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides 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 9c7f1eb079f5..02c22ad089a4 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 @@ -5556,4 +5556,29 @@ private ArrayList> keyValuesAsAlternatingList(Map jsonSet(Expression jsonDocument, Expression jsonPath, Object value) { + return jsonSet( jsonDocument, jsonPath, value( value ) ); + } + + @Override + public SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Object value) { + return jsonSet( jsonDocument, value( jsonPath ), value( value ) ); + } + + @Override + public SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value) { + return jsonSet( jsonDocument, value( jsonPath ), value ); + } + + @Override + public SqmExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value) { + //noinspection unchecked + return getFunctionDescriptor( "json_set" ).generateSqmExpression( + (List>) (List) asList( jsonDocument, jsonPath, value ), + null, + queryEngine + ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java new file mode 100644 index 000000000000..dd5dc00d9a78 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.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 . + */ +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.SupportsJsonSet.class) +public class JsonSetTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-set-example[] + em.createQuery( "select json_set('{\"a\":1}', '$.a', 2)" ).getResultList(); + //end::hql-json-set-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 3b904a1f2557..2dce0a2d0eab 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -466,6 +466,38 @@ public void testJsonObjectAggUniqueKeys(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonSet.class) + public void testJsonSet(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_set('{}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 123, object.get( "a" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonSet.class) + public void testJsonSetReplace(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_set('{\"a\":456}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 123, object.get( "a" ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 39fae56b0487..00722a2d62e3 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -793,6 +793,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonSet implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_set" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; From 0cc804858478509d9a929218844afa61604e7ed5 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 16 Sep 2024 17:27:17 +0200 Subject: [PATCH 10/15] HHH-18604 Add json_remove function and fix some Oracle functions --- .../chapters/query/hql/QueryLanguage.adoc | 17 +++++ .../dialect/CockroachLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 2 + .../dialect/PostgreSQLLegacyDialect.java | 1 + .../dialect/SQLServerLegacyDialect.java | 1 + .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 2 + .../hibernate/dialect/PostgreSQLDialect.java | 1 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 48 ++++++++++++ .../json/AbstractJsonRemoveFunction.java | 39 ++++++++++ .../json/OracleJsonArrayAggFunction.java | 35 ++++++++- .../json/OracleJsonArrayFunction.java | 8 ++ .../json/OracleJsonObjectAggFunction.java | 66 ++++++++++++++++ .../json/OracleJsonObjectFunction.java | 8 ++ .../json/OracleJsonRemoveFunction.java | 41 ++++++++++ .../json/PostgreSQLJsonRemoveFunction.java | 76 +++++++++++++++++++ .../json/SQLServerJsonRemoveFunction.java | 41 ++++++++++ .../criteria/HibernateCriteriaBuilder.java | 16 ++++ .../spi/HibernateCriteriaBuilderDelegate.java | 12 +++ .../org/hibernate/query/sqm/NodeBuilder.java | 6 ++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 15 ++++ .../test/function/json/JsonRemoveTest.java | 39 ++++++++++ .../orm/test/query/hql/JsonFunctionTests.java | 46 +++++++++++ .../orm/junit/DialectFeatureChecks.java | 6 ++ 27 files changed, 527 insertions(+), 4 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 0e2ffd6228b8..740de8062105 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1640,6 +1640,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_arrayagg()` | Creates a JSON array by aggregating values | `json_objectagg()` | Creates a JSON object by aggregating values | `json_set()` | Inserts/Replaces a value by JSON path within a JSON document +| `json_remove()` | Removes a value by JSON path within a JSON document |=== @@ -2030,6 +2031,22 @@ include::{json-example-dir-hql}/JsonSetTest.java[tags=hql-json-set-example] WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-json-remove-function]] +===== `json_remove()` + +Removes a value by JSON path within a JSON document. +The function takes 2 arguments, the json document and the json path representing what to remove. + +[[hql-json-remove-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonRemoveTest.java[tags=hql-json-remove-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions 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 8f4704251d02..eadb73c0945a 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 @@ -509,6 +509,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 5a188b55e92e..de3883d97e8a 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 @@ -661,6 +661,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); functionFactory.jsonSet_mysql(); + functionFactory.jsonRemove_mysql(); } } 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 a7c0e8cc6a3d..d112ae0cb11c 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 @@ -316,7 +316,9 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonObjectAgg_oracle(); functionFactory.jsonSet_oracle(); + functionFactory.jsonRemove_oracle(); } } 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 9ff3b2ea7127..139a4cdf5413 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 @@ -648,6 +648,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } } functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); 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 5c2d74b55b6e..5a55001a9910 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 @@ -407,6 +407,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); functionFactory.jsonSet_sqlserver(); + functionFactory.jsonRemove_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 b18965483c83..28a338587fd1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -476,6 +476,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 e5921f472395..3c6fb7f024b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -646,6 +646,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); functionFactory.jsonSet_mysql(); + functionFactory.jsonRemove_mysql(); } @Override 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 ac41fe71a898..dab87a1da7b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -407,7 +407,9 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonObjectAgg_oracle(); functionFactory.jsonSet_oracle(); + functionFactory.jsonRemove_oracle(); } @Override 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 7092c9806731..66a3f0f75a36 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -609,6 +609,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } } functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 2fb7e72c7309..d2177d0aec98 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -425,6 +425,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); functionFactory.jsonSet_sqlserver(); + functionFactory.jsonRemove_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 857243b6e9f4..6d87238987c7 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 @@ -117,7 +117,9 @@ 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.OracleJsonObjectAggFunction; import org.hibernate.dialect.function.json.OracleJsonObjectFunction; +import org.hibernate.dialect.function.json.OracleJsonRemoveFunction; import org.hibernate.dialect.function.json.OracleJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; @@ -125,6 +127,7 @@ import org.hibernate.dialect.function.json.PostgreSQLJsonObjectAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonRemoveFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; @@ -133,6 +136,7 @@ import org.hibernate.dialect.function.json.SQLServerJsonObjectAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonObjectFunction; import org.hibernate.dialect.function.json.SQLServerJsonQueryFunction; +import org.hibernate.dialect.function.json.SQLServerJsonRemoveFunction; import org.hibernate.dialect.function.json.SQLServerJsonSetFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; @@ -3753,6 +3757,13 @@ public void jsonArrayAgg_hana() { functionRegistry.register( "json_arrayagg", new HANAJsonArrayAggFunction( typeConfiguration ) ); } + /** + * Oracle json_objectagg() function + */ + public void jsonObjectAgg_oracle() { + functionRegistry.register( "json_objectagg", new OracleJsonObjectAggFunction( typeConfiguration ) ); + } + /** * json_objectagg() function for H2 and HSQLDB */ @@ -3839,4 +3850,41 @@ public void jsonSet_oracle() { public void jsonSet_sqlserver() { functionRegistry.register( "json_set", new SQLServerJsonSetFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_remove() function + */ + public void jsonRemove_postgresql() { + functionRegistry.register( "json_remove", new PostgreSQLJsonRemoveFunction( typeConfiguration ) ); + } + + /** + * MySQL json_remove() function + */ + public void jsonRemove_mysql() { + functionRegistry.namedDescriptorBuilder( "json_remove" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 2 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_remove() function + */ + public void jsonRemove_oracle() { + functionRegistry.register( "json_remove", new OracleJsonRemoveFunction( typeConfiguration ) ); + } + + /** + * SQL server json_remove() function + */ + public void jsonRemove_sqlserver() { + functionRegistry.register( "json_remove", new SQLServerJsonRemoveFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java new file mode 100644 index 000000000000..c42ef85ac98a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.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.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_remove function. + */ +public abstract class AbstractJsonRemoveFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( + "json_remove", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 2 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} 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 index 8926ff0ad753..66b7f8c0e60c 100644 --- 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 @@ -6,9 +6,14 @@ */ package org.hibernate.dialect.function.json; +import java.util.List; + +import org.hibernate.dialect.function.CastFunction; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; import org.hibernate.type.SqlTypes; @@ -19,8 +24,12 @@ */ public class OracleJsonArrayAggFunction extends JsonArrayAggFunction { + private final CastTarget stringCastTarget; + private CastFunction castFunction; + public OracleJsonArrayAggFunction(TypeConfiguration typeConfiguration) { super( false, typeConfiguration ); + this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); } @Override @@ -29,11 +38,29 @@ protected void renderArgument( 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() ) ) { + if ( ExpressionTypeHelper.isNonNativeBoolean( arg ) ) { + CastFunction castFunction = this.castFunction; + if ( castFunction == null ) { + castFunction = this.castFunction = (CastFunction) translator.getSessionFactory() + .getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( "cast" ); + } + castFunction.render( + sqlAppender, + List.of( arg, stringCastTarget ), + (ReturnableType) stringCastTarget.getJdbcMapping(), + translator + ); sqlAppender.appendSql( " format json" ); } + else { + 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 index 3a56bc71877f..2d182789d1fc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java @@ -9,11 +9,14 @@ import java.util.List; import org.hibernate.dialect.function.CastFunction; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -49,6 +52,11 @@ protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTran } else { value.accept( walker ); + final JdbcMappingContainer expressionType = ( (Expression) value ).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/OracleJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java new file mode 100644 index 000000000000..fa58ed52ea27 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java @@ -0,0 +1,66 @@ +/* + * 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.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_objectagg function. + */ +public class OracleJsonObjectAggFunction extends JsonObjectAggFunction { + + private final CastTarget stringCastTarget; + private CastFunction castFunction; + + public OracleJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( " value ", false, typeConfiguration ); + this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + if ( ExpressionTypeHelper.isNonNativeBoolean( arg ) ) { + CastFunction castFunction = this.castFunction; + if ( castFunction == null ) { + castFunction = this.castFunction = (CastFunction) translator.getSessionFactory() + .getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( "cast" ); + } + castFunction.render( + sqlAppender, + List.of( arg, stringCastTarget ), + (ReturnableType) stringCastTarget.getJdbcMapping(), + translator + ); + sqlAppender.appendSql( " format json" ); + } + else { + 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/OracleJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java index 049df279c481..eaad48b202a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java @@ -9,11 +9,14 @@ import java.util.List; import org.hibernate.dialect.function.CastFunction; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -49,6 +52,11 @@ protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTran } else { value.accept( walker ); + final JdbcMappingContainer expressionType = ( (Expression) value ).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/OracleJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java new file mode 100644 index 000000000000..5e4e5d1fdc60 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.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 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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_remove function. + */ +public class OracleJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public OracleJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",remove " ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java new file mode 100644 index 000000000000..73229b6b053a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java @@ -0,0 +1,76 @@ +/* + * 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.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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_set function. + */ +public class PostgreSQLJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public PostgreSQLJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + sqlAppender.appendSql( "jsonb_set_lax(" ); + final boolean needsCast = !isJsonType( json ) && json instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[],null,true,'delete_key')" ); + } + + private boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java new file mode 100644 index 000000000000..0936a24bfd9c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.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 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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_remove function. + */ +public class SQLServerJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public SQLServerJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + sqlAppender.appendSql( "json_modify(" ); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ",null)" ); + } +} 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 c3229e7c7c7d..30136487f5f5 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 @@ -3943,6 +3943,22 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @Incubating JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value); + /** + * Removes a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonRemove(Expression jsonDocument, String jsonPath); + + /** + * Removes a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonRemove(Expression jsonDocument, Expression jsonPath); + @Override JpaPredicate and(List restrictions); 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 c4a024e4e122..d8088f26dbe4 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 @@ -3547,4 +3547,16 @@ public JpaExpression jsonSet(Expression jsonDocument, String jsonPath public JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value) { return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); } + + @Override + @Incubating + public JpaExpression jsonRemove(Expression jsonDocument, String jsonPath) { + return criteriaBuilder.jsonRemove( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaExpression jsonRemove(Expression jsonDocument, Expression jsonPath) { + return criteriaBuilder.jsonRemove( jsonDocument, jsonPath ); + } } 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 02392cc95a66..a58783d7efb5 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 @@ -715,6 +715,12 @@ SqmJsonValueExpression jsonValue( @Override SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value); + @Override + SqmExpression jsonRemove(Expression jsonDocument, String jsonPath); + + @Override + SqmExpression jsonRemove(Expression jsonDocument, Expression jsonPath); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides 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 02c22ad089a4..783a172b78fa 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 @@ -5581,4 +5581,19 @@ public SqmExpression jsonSet(Expression jsonDocument, Expression jsonRemove(Expression jsonDocument, String jsonPath) { + return jsonRemove( jsonDocument, value( jsonPath ) ); + } + + @Override + public SqmExpression jsonRemove(Expression jsonDocument, Expression jsonPath) { + //noinspection unchecked + return getFunctionDescriptor( "json_remove" ).generateSqmExpression( + (List>) (List) asList( jsonDocument, jsonPath ), + null, + queryEngine + ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java new file mode 100644 index 000000000000..590651c5897b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.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 . + */ +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.SupportsJsonRemove.class) +public class JsonRemoveTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-remove-example[] + em.createQuery( "select json_remove('{\"a\":1,\"b\":2}', '$.a')" ).getResultList(); + //end::hql-json-remove-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 2dce0a2d0eab..6d07bc3e17a6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -498,6 +498,52 @@ public void testJsonSetReplace(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonRemove.class) + public void testJsonRemove(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_remove('{\"a\":123,\"b\":456}', '$.a')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 456, object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonRemove.class) + public void testJsonRemoveToEmpty(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_remove('{\"a\":123}', '$.a')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 0, object.size() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonRemove.class) + public void testJsonRemoveNonExisting(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_remove('{}', '$.a')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 0, object.size() ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 00722a2d62e3..1a1b9d6048b8 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -799,6 +799,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonRemove implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_remove" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; From af94b71dae2e09c868dcbbfd607a887ff5bf11a3 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 16 Sep 2024 18:11:57 +0200 Subject: [PATCH 11/15] HHH-18604 Add json_insert and json_replace function --- .../chapters/query/hql/QueryLanguage.adoc | 36 ++++++++ .../dialect/CockroachLegacyDialect.java | 4 +- .../community/dialect/MySQLLegacyDialect.java | 2 + .../dialect/OracleLegacyDialect.java | 2 + .../dialect/PostgreSQLLegacyDialect.java | 2 + .../dialect/SQLServerLegacyDialect.java | 2 + .../hibernate/dialect/CockroachDialect.java | 4 +- .../org/hibernate/dialect/MySQLDialect.java | 2 + .../org/hibernate/dialect/OracleDialect.java | 2 + .../hibernate/dialect/PostgreSQLDialect.java | 2 + .../hibernate/dialect/SQLServerDialect.java | 2 + .../function/CommonFunctionFactory.java | 90 +++++++++++++++++++ .../json/AbstractJsonInsertFunction.java | 40 +++++++++ .../json/AbstractJsonReplaceFunction.java | 40 +++++++++ .../json/CockroachDBJsonRemoveFunction.java | 75 ++++++++++++++++ .../json/OracleJsonInsertFunction.java | 44 +++++++++ .../json/OracleJsonReplaceFunction.java | 44 +++++++++ .../json/PostgreSQLJsonInsertFunction.java | 90 +++++++++++++++++++ .../json/PostgreSQLJsonRemoveFunction.java | 2 +- .../json/PostgreSQLJsonReplaceFunction.java | 90 +++++++++++++++++++ .../json/SQLServerJsonInsertFunction.java | 55 ++++++++++++ .../json/SQLServerJsonReplaceFunction.java | 55 ++++++++++++ .../criteria/HibernateCriteriaBuilder.java | 64 +++++++++++++ .../spi/HibernateCriteriaBuilderDelegate.java | 54 +++++++++++ .../org/hibernate/query/sqm/NodeBuilder.java | 24 +++++ .../sqm/internal/SqmCriteriaNodeBuilder.java | 56 ++++++++++++ .../test/function/json/JsonInsertTest.java | 39 ++++++++ .../test/function/json/JsonReplaceTest.java | 39 ++++++++ .../orm/test/query/hql/JsonFunctionTests.java | 63 +++++++++++++ .../orm/junit/DialectFeatureChecks.java | 12 +++ 30 files changed, 1033 insertions(+), 3 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 740de8062105..39f7cd265507 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -2047,6 +2047,42 @@ include::{json-example-dir-hql}/JsonRemoveTest.java[tags=hql-json-remove-example WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-json-replace-function]] +===== `json_replace()` + +Replaces a value by JSON path within a JSON document. +The function takes 3 arguments, the json document, the json path and the new value to set. +A value will not be inserted if the key is missing, only the values of existing keys are replaced. + +[[hql-json-replace-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonReplaceTest.java[tags=hql-json-replace-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + +[[hql-json-insert-function]] +===== `json_insert()` + +Inserts a value by JSON path in a JSON document. +The function takes 3 arguments, the json document, the json path and the value to insert. +When the JSON document contains a value for a JSON path, no insertion happens, +unless the value is an array, in which case the value will be appended to that array. +If no value exists for a JSON path, the value will be inserted under the key as specified through the JSON path. + +[[hql-json-insert-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonInsertTest.java[tags=hql-json-insert-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions 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 eadb73c0945a..140e935d047f 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 @@ -509,7 +509,9 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); functionFactory.jsonSet_postgresql(); - functionFactory.jsonRemove_postgresql(); + functionFactory.jsonRemove_cockroachdb(); + functionFactory.jsonReplace_postgresql(); + functionFactory.jsonInsert_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index de3883d97e8a..97e8d3ff263d 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 @@ -662,6 +662,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObjectAgg_mysql(); functionFactory.jsonSet_mysql(); functionFactory.jsonRemove_mysql(); + functionFactory.jsonReplace_mysql(); + functionFactory.jsonInsert_mysql(); } } 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 d112ae0cb11c..6ac68632848b 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 @@ -319,6 +319,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObjectAgg_oracle(); functionFactory.jsonSet_oracle(); functionFactory.jsonRemove_oracle(); + functionFactory.jsonReplace_oracle(); + functionFactory.jsonInsert_oracle(); } } 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 139a4cdf5413..a5bd61c1e9e2 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 @@ -649,6 +649,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } functionFactory.jsonSet_postgresql(); functionFactory.jsonRemove_postgresql(); + functionFactory.jsonReplace_postgresql(); + functionFactory.jsonInsert_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); 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 5a55001a9910..c93cc0086a25 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 @@ -408,6 +408,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray_sqlserver(); functionFactory.jsonSet_sqlserver(); functionFactory.jsonRemove_sqlserver(); + functionFactory.jsonReplace_sqlserver(); + functionFactory.jsonInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 28a338587fd1..a8da335cda11 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -476,7 +476,9 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); functionFactory.jsonSet_postgresql(); - functionFactory.jsonRemove_postgresql(); + functionFactory.jsonRemove_cockroachdb(); + functionFactory.jsonReplace_postgresql(); + functionFactory.jsonInsert_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 3c6fb7f024b4..56024d72fca3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -647,6 +647,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObjectAgg_mysql(); functionFactory.jsonSet_mysql(); functionFactory.jsonRemove_mysql(); + functionFactory.jsonReplace_mysql(); + functionFactory.jsonInsert_mysql(); } @Override 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 dab87a1da7b7..95dc5f9f06d0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -410,6 +410,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonObjectAgg_oracle(); functionFactory.jsonSet_oracle(); functionFactory.jsonRemove_oracle(); + functionFactory.jsonReplace_oracle(); + functionFactory.jsonInsert_oracle(); } @Override 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 66a3f0f75a36..268a5cba249f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -610,6 +610,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio } functionFactory.jsonSet_postgresql(); functionFactory.jsonRemove_postgresql(); + functionFactory.jsonReplace_postgresql(); + functionFactory.jsonInsert_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 d2177d0aec98..55dd455e5d78 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -426,6 +426,8 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonArray_sqlserver(); functionFactory.jsonSet_sqlserver(); functionFactory.jsonRemove_sqlserver(); + functionFactory.jsonReplace_sqlserver(); + functionFactory.jsonInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 6d87238987c7..84bf02f35a47 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,7 @@ 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.CockroachDBJsonRemoveFunction; import org.hibernate.dialect.function.json.CockroachDBJsonValueFunction; import org.hibernate.dialect.function.json.DB2JsonArrayAggFunction; import org.hibernate.dialect.function.json.DB2JsonArrayFunction; @@ -117,26 +118,32 @@ 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.OracleJsonInsertFunction; import org.hibernate.dialect.function.json.OracleJsonObjectAggFunction; import org.hibernate.dialect.function.json.OracleJsonObjectFunction; import org.hibernate.dialect.function.json.OracleJsonRemoveFunction; +import org.hibernate.dialect.function.json.OracleJsonReplaceFunction; import org.hibernate.dialect.function.json.OracleJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonInsertFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonRemoveFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonReplaceFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; +import org.hibernate.dialect.function.json.SQLServerJsonInsertFunction; import org.hibernate.dialect.function.json.SQLServerJsonObjectAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonObjectFunction; import org.hibernate.dialect.function.json.SQLServerJsonQueryFunction; import org.hibernate.dialect.function.json.SQLServerJsonRemoveFunction; +import org.hibernate.dialect.function.json.SQLServerJsonReplaceFunction; import org.hibernate.dialect.function.json.SQLServerJsonSetFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; @@ -3858,6 +3865,13 @@ public void jsonRemove_postgresql() { functionRegistry.register( "json_remove", new PostgreSQLJsonRemoveFunction( typeConfiguration ) ); } + /** + * CockroachDB json_remove() function + */ + public void jsonRemove_cockroachdb() { + functionRegistry.register( "json_remove", new CockroachDBJsonRemoveFunction( typeConfiguration ) ); + } + /** * MySQL json_remove() function */ @@ -3887,4 +3901,80 @@ public void jsonRemove_oracle() { public void jsonRemove_sqlserver() { functionRegistry.register( "json_remove", new SQLServerJsonRemoveFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_replace() function + */ + public void jsonReplace_postgresql() { + functionRegistry.register( "json_replace", new PostgreSQLJsonReplaceFunction( typeConfiguration ) ); + } + + /** + * MySQL json_replace() function + */ + public void jsonReplace_mysql() { + functionRegistry.namedDescriptorBuilder( "json_replace" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_replace() function + */ + public void jsonReplace_oracle() { + functionRegistry.register( "json_replace", new OracleJsonReplaceFunction( typeConfiguration ) ); + } + + /** + * SQL server json_replace() function + */ + public void jsonReplace_sqlserver() { + functionRegistry.register( "json_replace", new SQLServerJsonReplaceFunction( typeConfiguration ) ); + } + + /** + * PostgreSQL json_insert() function + */ + public void jsonInsert_postgresql() { + functionRegistry.register( "json_insert", new PostgreSQLJsonInsertFunction( typeConfiguration ) ); + } + + /** + * MySQL json_insert() function + */ + public void jsonInsert_mysql() { + functionRegistry.namedDescriptorBuilder( "json_insert" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_insert() function + */ + public void jsonInsert_oracle() { + functionRegistry.register( "json_insert", new OracleJsonInsertFunction( typeConfiguration ) ); + } + + /** + * SQL server json_insert() function + */ + public void jsonInsert_sqlserver() { + functionRegistry.register( "json_insert", new SQLServerJsonInsertFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.java new file mode 100644 index 000000000000..d41263c9a376 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.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.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_insert function. + */ +public abstract class AbstractJsonInsertFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonInsertFunction(TypeConfiguration typeConfiguration) { + super( + "json_insert", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.java new file mode 100644 index 000000000000..4f8808d04536 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.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.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_replace function. + */ +public abstract class AbstractJsonReplaceFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonReplaceFunction(TypeConfiguration typeConfiguration) { + super( + "json_replace", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.java new file mode 100644 index 000000000000..c50d4683d828 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.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.QueryException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +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.type.spi.TypeConfiguration; + +/** + * CockroachDB json_remove function. + */ +public class CockroachDBJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public CockroachDBJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + sqlAppender.appendSql( "json_remove_path(" ); + final boolean needsCast = !isJsonType( json ) && json instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() + 1 ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[])" ); + } + + private boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java new file mode 100644 index 000000000000..851049a57370 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java @@ -0,0 +1,44 @@ +/* + * 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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_insert function. + */ +public class OracleJsonInsertFunction extends AbstractJsonInsertFunction { + + public OracleJsonInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",insert " ); + jsonPath.accept( translator ); + sqlAppender.appendSql( '=' ); + value.accept( translator ); + sqlAppender.appendSql( " ignore on existing)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java new file mode 100644 index 000000000000..63ad699053f4 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java @@ -0,0 +1,44 @@ +/* + * 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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_replace function. + */ +public class OracleJsonReplaceFunction extends AbstractJsonReplaceFunction { + + public OracleJsonReplaceFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",replace " ); + jsonPath.accept( translator ); + sqlAppender.appendSql( '=' ); + value.accept( translator ); + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java new file mode 100644 index 000000000000..068c484473e5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java @@ -0,0 +1,90 @@ +/* + * 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.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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_insert function. + */ +public class PostgreSQLJsonInsertFunction extends AbstractJsonInsertFunction { + + public PostgreSQLJsonInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "(select case when t.d#>>t.p is not null then t.d else jsonb_insert(t.d,t.p," ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( translator ); + 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( ",true) end from (values(" ); + final boolean needsCast = !isJsonType( json ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[]" ); + sqlAppender.appendSql( ")) t(d,p))" ); + } + + private static boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java index 73229b6b053a..59ec9b10e07d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java @@ -20,7 +20,7 @@ import org.hibernate.type.spi.TypeConfiguration; /** - * PostgreSQL json_set function. + * PostgreSQL json_remove function. */ public class PostgreSQLJsonRemoveFunction extends AbstractJsonRemoveFunction { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java new file mode 100644 index 000000000000..e8c97c40d345 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java @@ -0,0 +1,90 @@ +/* + * 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.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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_replace function. + */ +public class PostgreSQLJsonReplaceFunction extends AbstractJsonReplaceFunction { + + public PostgreSQLJsonReplaceFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "jsonb_set(" ); + final boolean needsCast = !isJsonType( json ) && json instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[]," ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( translator ); + 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( ",false)" ); + } + + private boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java new file mode 100644 index 000000000000..1bce8a451bae --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.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.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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_insert function. + */ +public class SQLServerJsonInsertFunction extends AbstractJsonInsertFunction { + + public SQLServerJsonInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + sqlAppender.appendSql( "(select case when coalesce(json_query(t.d,t.p),json_value(t.d,t.p)) is not null then t.d else json_modify(t.d,t.p," ); + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + renderValue( sqlAppender, value, translator ); + sqlAppender.appendSql( ") end from (values("); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ")) t(d,p))" ); + } + + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator translator) { + if ( ExpressionTypeHelper.isBoolean( value ) ) { + sqlAppender.appendSql( "cast(" ); + value.accept( translator ); + sqlAppender.appendSql( " as bit)" ); + } + else { + value.accept( translator ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java new file mode 100644 index 000000000000..35ed15262607 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.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.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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_replace function. + */ +public class SQLServerJsonReplaceFunction extends AbstractJsonReplaceFunction { + + public SQLServerJsonReplaceFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + sqlAppender.appendSql( "(select case when coalesce(json_query(t.d,t.p),json_value(t.d,t.p)) is null then t.d else json_modify(t.d,t.p," ); + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + renderValue( sqlAppender, value, translator ); + sqlAppender.appendSql( ") end from (values("); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ")) t(d,p))" ); + } + + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator translator) { + if ( ExpressionTypeHelper.isBoolean( value ) ) { + sqlAppender.appendSql( "cast(" ); + value.accept( translator ); + sqlAppender.appendSql( " as bit)" ); + } + else { + value.accept( translator ); + } + } +} 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 30136487f5f5..b1ab1ba010af 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 @@ -3959,6 +3959,70 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @Incubating JpaExpression jsonRemove(Expression jsonDocument, Expression jsonPath); + /** + * Inserts a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonInsert(Expression jsonDocument, String jsonPath, Expression value); + + /** + * Inserts a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonInsert(Expression jsonDocument, Expression jsonPath, Expression value); + + /** + * Inserts a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonInsert(Expression jsonDocument, String jsonPath, Object value); + + /** + * Inserts a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonInsert(Expression jsonDocument, Expression jsonPath, Object value); + + /** + * Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonReplace(Expression jsonDocument, String jsonPath, Expression value); + + /** + * Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Expression value); + + /** + * Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonReplace(Expression jsonDocument, String jsonPath, Object value); + + /** + * Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Object value); + @Override JpaPredicate and(List restrictions); 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 d8088f26dbe4..a400ce2e56d9 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 @@ -3559,4 +3559,58 @@ public JpaExpression jsonRemove(Expression jsonDocument, String jsonP public JpaExpression jsonRemove(Expression jsonDocument, Expression jsonPath) { return criteriaBuilder.jsonRemove( jsonDocument, jsonPath ); } + + @Override + @Incubating + public JpaExpression jsonInsert(Expression jsonDocument, String jsonPath, Expression value) { + return criteriaBuilder.jsonInsert( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonInsert( + Expression jsonDocument, + Expression jsonPath, + Expression value) { + return criteriaBuilder.jsonInsert( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonInsert(Expression jsonDocument, String jsonPath, Object value) { + return criteriaBuilder.jsonInsert( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonInsert(Expression jsonDocument, Expression jsonPath, Object value) { + return criteriaBuilder.jsonInsert( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonReplace(Expression jsonDocument, String jsonPath, Expression value) { + return criteriaBuilder.jsonReplace( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonReplace( + Expression jsonDocument, + Expression jsonPath, + Expression value) { + return criteriaBuilder.jsonReplace( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonReplace(Expression jsonDocument, String jsonPath, Object value) { + return criteriaBuilder.jsonReplace( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Object value) { + return criteriaBuilder.jsonReplace( jsonDocument, jsonPath, value ); + } } 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 a58783d7efb5..9ef21a8f8d09 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 @@ -721,6 +721,30 @@ SqmJsonValueExpression jsonValue( @Override SqmExpression jsonRemove(Expression jsonDocument, Expression jsonPath); + @Override + SqmExpression jsonInsert(Expression jsonDocument, Expression jsonPath, Object value); + + @Override + SqmExpression jsonInsert(Expression jsonDocument, String jsonPath, Object value); + + @Override + SqmExpression jsonInsert(Expression jsonDocument, Expression jsonPath, Expression value); + + @Override + SqmExpression jsonInsert(Expression jsonDocument, String jsonPath, Expression value); + + @Override + SqmExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Object value); + + @Override + SqmExpression jsonReplace(Expression jsonDocument, String jsonPath, Object value); + + @Override + SqmExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Expression value); + + @Override + SqmExpression jsonReplace(Expression jsonDocument, String jsonPath, Expression value); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides 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 783a172b78fa..97761ba83a8e 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 @@ -5596,4 +5596,60 @@ public SqmExpression jsonRemove(Expression jsonDocument, Expression jsonInsert(Expression jsonDocument, Expression jsonPath, Object value) { + return jsonInsert( jsonDocument, jsonPath, value( value ) ); + } + + @Override + public SqmExpression jsonInsert(Expression jsonDocument, String jsonPath, Object value) { + return jsonInsert( jsonDocument, value( jsonPath ), value ); + } + + @Override + public SqmExpression jsonInsert(Expression jsonDocument, String jsonPath, Expression value) { + return jsonInsert( jsonDocument, value( jsonPath ), value ); + } + + @Override + public SqmExpression jsonInsert( + Expression jsonDocument, + Expression jsonPath, + Expression value) { + //noinspection unchecked + return getFunctionDescriptor( "json_insert" ).generateSqmExpression( + (List>) (List) asList( jsonDocument, jsonPath, value ), + null, + queryEngine + ); + } + + @Override + public SqmExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Object value) { + return jsonReplace( jsonDocument, jsonPath, value( value ) ); + } + + @Override + public SqmExpression jsonReplace(Expression jsonDocument, String jsonPath, Object value) { + return jsonReplace( jsonDocument, value( jsonPath ), value ); + } + + @Override + public SqmExpression jsonReplace(Expression jsonDocument, String jsonPath, Expression value) { + return jsonReplace( jsonDocument, value( jsonPath ), value ); + } + + @Override + public SqmExpression jsonReplace( + Expression jsonDocument, + Expression jsonPath, + Expression value) { + //noinspection unchecked + return getFunctionDescriptor( "json_replace" ).generateSqmExpression( + (List>) (List) asList( jsonDocument, jsonPath, value ), + null, + queryEngine + ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.java new file mode 100644 index 000000000000..7324fa1f4618 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.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 . + */ +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.SupportsJsonInsert.class) +public class JsonInsertTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-insert-example[] + em.createQuery( "select json_insert('{\"a\":1}', '$.b', 2)" ).getResultList(); + //end::hql-json-insert-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.java new file mode 100644 index 000000000000..704d94e51699 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.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 . + */ +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.SupportsJsonReplace.class) +public class JsonReplaceTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-replace-example[] + em.createQuery( "select json_replace('{\"a\":1}', '$.a', 2)" ).getResultList(); + //end::hql-json-replace-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 6d07bc3e17a6..1a9aac5fcdff 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -544,6 +544,69 @@ public void testJsonRemoveNonExisting(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonReplace.class) + public void testJsonReplaceNonExisting(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_replace('{}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 0, object.size() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonReplace.class) + public void testJsonReplace(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_replace('{\"a\":456}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 123, object.get( "a" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonInsert.class) + public void testJsonInsert(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_insert('{}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 123, object.get( "a" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonInsert.class) + public void testJsonInsertWithExisting(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_insert('{\"a\":456}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 456, object.get( "a" ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 1a1b9d6048b8..2ad5d9a39c76 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -805,6 +805,18 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonReplace implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_replace" ); + } + } + + public static class SupportsJsonInsert implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_insert" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; From f9ee2de7d183076f49dafb65eeafcb74877393cf Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 17 Sep 2024 15:51:42 +0200 Subject: [PATCH 12/15] HHH-18604 Add json_mergepatch function --- .../chapters/query/hql/QueryLanguage.adoc | 30 +++ .../dialect/CockroachLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 1 + .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 1 + .../hibernate/dialect/PostgreSQLDialect.java | 1 + .../function/CommonFunctionFactory.java | 34 ++- .../json/AbstractJsonMergepatchFunction.java | 39 ++++ .../json/OracleJsonMergepatchFunction.java | 48 ++++ .../PostgreSQLJsonMergepatchFunction.java | 208 ++++++++++++++++++ .../criteria/HibernateCriteriaBuilder.java | 24 ++ .../spi/HibernateCriteriaBuilderDelegate.java | 18 ++ .../org/hibernate/query/sqm/NodeBuilder.java | 9 + .../sqm/internal/SqmCriteriaNodeBuilder.java | 20 ++ .../function/json/JsonMergepatchTest.java | 37 ++++ .../orm/test/query/hql/JsonFunctionTests.java | 41 +++- .../orm/junit/DialectFeatureChecks.java | 6 + 20 files changed, 516 insertions(+), 6 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonMergepatchTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 39f7cd265507..94fac181b789 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1641,6 +1641,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_objectagg()` | Creates a JSON object by aggregating values | `json_set()` | Inserts/Replaces a value by JSON path within a JSON document | `json_remove()` | Removes a value by JSON path within a JSON document +| `json_mergepatch()` | Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge |=== @@ -2083,6 +2084,35 @@ include::{json-example-dir-hql}/JsonInsertTest.java[tags=hql-json-insert-example WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-json-mergepatch-function]] +===== `json_mergepatch()` + +Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge, which is + +* When the first JSON value is not an object, the result is as if the first argument was an empty object +* When the second JSON value is not an object, the result is the second argument +* When both JSON values are objects, members are merged +** Retain first JSON object members when the second JSON object has no members with matching keys +** Retain second JSON object members when the first JSON object has no members with matching keys and the value is not equal to the JSON `null` literal +** Recursively merge values that exist in both JSON objects, except if the second JSON object member is a JSON `null` + +In simple terms this means + +* The second JSON overrides members of the first, with JSON `null` values causing members to be removed +* JSON objects are merged recursively + +NOTE: Arrays and hence objects within arrays are not merged, but replaced. + +[[hql-json-mergepatch-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonMergepatchTest.java[tags=hql-json-mergepatch-example] +---- +==== + +WARNING: SAP HANA, DB2, SQL Server, H2 and HSQLDB do not support this function. On PostgreSQL, this function is emulated. + [[hql-user-defined-functions]] ==== Native and user-defined functions 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 140e935d047f..4967a7075180 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 @@ -512,6 +512,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_cockroachdb(); functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); + functionFactory.jsonMergepatch_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 97e8d3ff263d..bfa1acf85e63 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 @@ -664,6 +664,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_mysql(); functionFactory.jsonReplace_mysql(); functionFactory.jsonInsert_mysql(); + functionFactory.jsonMergepatch_mysql(); } } 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 6ac68632848b..9eec72fa187d 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 @@ -321,6 +321,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_oracle(); functionFactory.jsonReplace_oracle(); functionFactory.jsonInsert_oracle(); + functionFactory.jsonMergepatch_oracle(); } } 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 a5bd61c1e9e2..069a6c2c7245 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 @@ -651,6 +651,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_postgresql(); functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); + functionFactory.jsonMergepatch_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); 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 a8da335cda11..269658529e59 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -479,6 +479,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_cockroachdb(); functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); + functionFactory.jsonMergepatch_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 56024d72fca3..0ce6668b2755 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -649,6 +649,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_mysql(); functionFactory.jsonReplace_mysql(); functionFactory.jsonInsert_mysql(); + functionFactory.jsonMergepatch_mysql(); } @Override 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 95dc5f9f06d0..d4330f7ec01c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -412,6 +412,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_oracle(); functionFactory.jsonReplace_oracle(); functionFactory.jsonInsert_oracle(); + functionFactory.jsonMergepatch_oracle(); } @Override 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 268a5cba249f..4561302daa91 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -612,6 +612,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_postgresql(); functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); + functionFactory.jsonMergepatch_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 84bf02f35a47..507b73880213 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 @@ -97,10 +97,8 @@ 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; @@ -119,6 +117,7 @@ import org.hibernate.dialect.function.json.OracleJsonArrayAggFunction; import org.hibernate.dialect.function.json.OracleJsonArrayFunction; import org.hibernate.dialect.function.json.OracleJsonInsertFunction; +import org.hibernate.dialect.function.json.OracleJsonMergepatchFunction; import org.hibernate.dialect.function.json.OracleJsonObjectAggFunction; import org.hibernate.dialect.function.json.OracleJsonObjectFunction; import org.hibernate.dialect.function.json.OracleJsonRemoveFunction; @@ -128,6 +127,7 @@ import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonInsertFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonMergepatchFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction; @@ -3977,4 +3977,34 @@ public void jsonInsert_oracle() { public void jsonInsert_sqlserver() { functionRegistry.register( "json_insert", new SQLServerJsonInsertFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_mergepatch() function + */ + public void jsonMergepatch_postgresql() { + functionRegistry.register( "json_mergepatch", new PostgreSQLJsonMergepatchFunction( typeConfiguration ) ); + } + + /** + * MySQL json_mergepatch() function + */ + public void jsonMergepatch_mysql() { + functionRegistry.namedDescriptorBuilder( "json_mergepatch", "json_merge_patch" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.min( 2 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.IMPLICIT_JSON + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_mergepatch() function + */ + public void jsonMergepatch_oracle() { + functionRegistry.register( "json_mergepatch", new OracleJsonMergepatchFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.java new file mode 100644 index 000000000000..f7dfad53f2fe --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.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.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_mergepatch function. + */ +public abstract class AbstractJsonMergepatchFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonMergepatchFunction(TypeConfiguration typeConfiguration) { + super( + "json_mergepatch", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.min( 2 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.IMPLICIT_JSON + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java new file mode 100644 index 000000000000..940e8cf10142 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java @@ -0,0 +1,48 @@ +/* + * 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.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_mergepatch function. + */ +public class OracleJsonMergepatchFunction extends AbstractJsonMergepatchFunction { + + public OracleJsonMergepatchFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final String ddlTypeName = translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry() + .getTypeName( SqlTypes.JSON, translator.getSessionFactory().getJdbcServices().getDialect() ); + final int argumentCount = arguments.size(); + for ( int i = 0; i < argumentCount - 1; i++ ) { + sqlAppender.appendSql( "json_mergepatch(" ); + } + arguments.get( 0 ).accept( translator ); + for ( int i = 1; i < argumentCount; i++ ) { + sqlAppender.appendSql( ',' ); + arguments.get( i ).accept( translator ); + sqlAppender.appendSql( " returning " ); + sqlAppender.appendSql( ddlTypeName ); + sqlAppender.appendSql( ')' ); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java new file mode 100644 index 000000000000..5a3d965aeae6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java @@ -0,0 +1,208 @@ +/* + * 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.JdbcMappingContainer; +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.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_mergepatch function. + */ +public class PostgreSQLJsonMergepatchFunction extends AbstractJsonMergepatchFunction { + + public PostgreSQLJsonMergepatchFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + // Introduce a CTE named "args" which will provide easy access to the arguments in the following + sqlAppender.appendSql( "(with recursive args" ); + char separator = '('; + for ( int i = 0; i < arguments.size(); i++ ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( 'd' ); + sqlAppender.appendSql( i ); + separator = ','; + } + + sqlAppender.appendSql( ") as(select" ); + separator = ' '; + for ( int i = 0; i < arguments.size(); i++ ) { + sqlAppender.appendSql( separator ); + renderJsonDocumentExpression( sqlAppender, translator, (Expression) arguments.get( i ) ); + separator = ','; + } + sqlAppender.appendSql( ")," ); + // Render CTEs that explode JSON into key-value pairs for each parent prefix + for ( int i = 0; i < arguments.size(); i++ ) { + renderKeyValueCte( "val" + i, "d" + i, sqlAppender); + } + // Compute the resulting JSON recursively + sqlAppender.appendSql( "res(v,p,l) as(" ); + sqlAppender.appendSql( "select" ); + // Aggregate key-value pairs, preferring the last value + sqlAppender.appendSql( " jsonb_object_agg(coalesce(" ); + renderColumnList(sqlAppender, "k", arguments.size()); + sqlAppender.appendSql( "),coalesce(" ); + renderColumnList(sqlAppender, "v", arguments.size()); + sqlAppender.appendSql( "))" ); + // The parent path + sqlAppender.appendSql( ",coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( ")" ); + // The level within the object tree + sqlAppender.appendSql( ",cardinality(coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( "))" ); + // Full join the two key-value pair tables based on parent prefix and key + sqlAppender.appendSql( " from val0 v0" ); + for ( int i = 1; i < arguments.size(); i++ ) { + sqlAppender.appendSql( " full join val" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( " v" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( " on v0.p=v" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ".p and v0.k=v" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ".k" ); + } + // start at the bottom + sqlAppender.appendSql( " where cardinality(coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( "))=" ); + sqlAppender.appendSql( "(select cardinality(v.p) from val0 v" ); + for ( int i = 1; i < arguments.size(); i++ ) { + sqlAppender.appendSql( " union select cardinality(v.p) from val" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( " v" ); + } + sqlAppender.appendSql( " order by 1 desc limit 1)" ); + // filter rows where the new value is a json null i.e. should be removed + sqlAppender.appendSql( " and jsonb_typeof(coalesce(" ); + renderColumnList(sqlAppender, "v", arguments.size(), 1); + sqlAppender.appendSql( ")) is distinct from 'null'" ); + sqlAppender.appendSql( " group by" ); + sqlAppender.appendSql( " coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( ")" ); + sqlAppender.appendSql( ",cardinality(coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( "))" ); + + sqlAppender.appendSql( " union all " ); + + sqlAppender.appendSql( "select" ); + // Use strict aggregation to ensure a SQL null does not end up as JSON null in the result + sqlAppender.appendSql( " jsonb_object_agg_strict(coalesce(" ); + renderColumnList(sqlAppender, "k", arguments.size()); + sqlAppender.appendSql( "),coalesce(case when coalesce(" ); + renderColumnList(sqlAppender, "k", arguments.size()); + sqlAppender.appendSql( ")=r.p[cardinality(r.p)] then r.v end," ); + renderColumnList(sqlAppender, "v", arguments.size()); + sqlAppender.appendSql( "))" ); + // The parent path + sqlAppender.appendSql( ",coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( ")" ); + // The level within the object tree + sqlAppender.appendSql( ",r.l-1" ); + // Full join the two key-value pair tables based on parent prefix and key + sqlAppender.appendSql( " from val0 v0" ); + for ( int i = 1; i < arguments.size(); i++ ) { + sqlAppender.appendSql( " full join val" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( " v" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( " on v0.p=v" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ".p and v0.k=v" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ".k" ); + } + // Recurse against the previously processed rows with lowest "level" to walk up the tree + sqlAppender.appendSql( " join (select * from res r order by r.l fetch first 1 rows with ties) r" ); + sqlAppender.appendSql( " on cardinality(coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( "))=r.l-1" ); + // filter rows where the new value is a json null i.e. should be removed + sqlAppender.appendSql( " and jsonb_typeof(coalesce(" ); + renderColumnList(sqlAppender, "v", arguments.size(), 1); + sqlAppender.appendSql( ")) is distinct from 'null'" ); + // Stop at the last/root level + sqlAppender.appendSql( " and r.l<>0" ); + sqlAppender.appendSql( " group by" ); + sqlAppender.appendSql( " coalesce(" ); + renderColumnList(sqlAppender, "p", arguments.size()); + sqlAppender.appendSql( ")" ); + sqlAppender.appendSql( ",r.l-1" ); + sqlAppender.appendSql( ") " ); + // Select the last/root level object + sqlAppender.appendSql( "select r.v from res r where r.l=0)" ); + } + + private void renderColumnList(SqlAppender sqlAppender, String column, int size) { + renderColumnList( sqlAppender, column, size, 0 ); + } + + private void renderColumnList(SqlAppender sqlAppender, String column, int size, int end) { + sqlAppender.appendSql( "v" ); + sqlAppender.appendSql( size - 1 ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( column ); + for ( int i = size - 2; i >= end; i-- ) { + sqlAppender.appendSql( ",v" ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( '.' ); + sqlAppender.appendSql( column ); + } + } + + private void renderKeyValueCte(String cteName, String columnName, SqlAppender sqlAppender) { + sqlAppender.appendSql( cteName ); + sqlAppender.appendSql( "(p,k,v) as ("); + sqlAppender.appendSql( "select '{}'::text[],s.k,t." ); + sqlAppender.appendSql( columnName ); + sqlAppender.appendSql( "->s.k from args t join lateral jsonb_object_keys(t." ); + sqlAppender.appendSql( columnName ); + sqlAppender.appendSql( ") s(k) on 1=1 union " ); + sqlAppender.appendSql( "select v.p||v.k,s.k,v.v->s.k from " ); + sqlAppender.appendSql( cteName ); + sqlAppender.appendSql( " v" ); + sqlAppender.appendSql( " join lateral jsonb_object_keys(v.v) s(k)" ); + sqlAppender.appendSql( " on jsonb_typeof(v.v)='object'" ); + sqlAppender.appendSql( ")," ); + } + + private void renderJsonDocumentExpression(SqlAppender sqlAppender, SqlAstTranslator translator, Expression json) { + final boolean needsCast = !isJsonType( json ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + } + + private boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} 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 b1ab1ba010af..73d78e8a779c 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 @@ -4023,6 +4023,30 @@ default JpaPredicate collectionOverlapsNullable(Collection collection1, E @Incubating JpaExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Object value); + /** + * Applies the patch JSON document onto the other JSON document and returns that. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonMergepatch(Expression document, Expression patch); + + /** + * Applies the patch JSON document onto the other JSON document and returns that. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonMergepatch(Expression document, String patch); + + /** + * Applies the patch JSON document onto the other JSON document and returns that. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonMergepatch(String document, Expression patch); + @Override JpaPredicate and(List restrictions); 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 a400ce2e56d9..d025c6bacbb6 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 @@ -3613,4 +3613,22 @@ public JpaExpression jsonReplace(Expression jsonDocument, String json public JpaExpression jsonReplace(Expression jsonDocument, Expression jsonPath, Object value) { return criteriaBuilder.jsonReplace( jsonDocument, jsonPath, value ); } + + @Override + @Incubating + public JpaExpression jsonMergepatch(Expression document, Expression patch) { + return criteriaBuilder.jsonMergepatch( document, patch ); + } + + @Override + @Incubating + public JpaExpression jsonMergepatch(Expression document, String patch) { + return criteriaBuilder.jsonMergepatch( document, patch ); + } + + @Override + @Incubating + public JpaExpression jsonMergepatch(String document, Expression patch) { + return criteriaBuilder.jsonMergepatch( document, patch ); + } } 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 9ef21a8f8d09..4cd1fc896d96 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 @@ -745,6 +745,15 @@ SqmJsonValueExpression jsonValue( @Override SqmExpression jsonReplace(Expression jsonDocument, String jsonPath, Expression value); + @Override + SqmExpression jsonMergepatch(String document, Expression patch); + + @Override + SqmExpression jsonMergepatch(Expression document, String patch); + + @Override + SqmExpression jsonMergepatch(Expression document, Expression patch); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides 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 97761ba83a8e..ee79846ae9b8 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 @@ -5652,4 +5652,24 @@ public SqmExpression jsonReplace( queryEngine ); } + + @Override + public SqmExpression jsonMergepatch(String document, Expression patch) { + return jsonMergepatch( value( document ), patch ); + } + + @Override + public SqmExpression jsonMergepatch(Expression document, String patch) { + return jsonMergepatch( document, value( patch ) ); + } + + @Override + public SqmExpression jsonMergepatch(Expression document, Expression patch) { + //noinspection unchecked + return getFunctionDescriptor( "json_mergepatch" ).generateSqmExpression( + (List>) (List) asList( document, patch ), + null, + queryEngine + ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonMergepatchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonMergepatchTest.java new file mode 100644 index 000000000000..2476b7ad0bbf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonMergepatchTest.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.SupportsJsonMergepatch.class) +public class JsonMergepatchTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-mergepatch-example[] + em.createQuery( "select json_mergepatch('{\"a\":1}', '{\"b\":2}')" ).getResultList(); + //end::hql-json-mergepatch-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 1a9aac5fcdff..805a661fd388 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.query.hql; @@ -607,6 +605,41 @@ public void testJsonInsertWithExisting(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonMergepatch.class) + public void testJsonMergepatch(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_mergepatch('{\"a\":456, \"b\":[1,2], \"c\":{\"a\":1}}', '{\"a\":null, \"b\":[4,5], \"c\":{\"b\":1}}')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 2, object.size() ); + assertEquals( Arrays.asList( parseArray( "[4,5]" ) ), object.get( "b" ) ); + assertEquals( parseObject( "{\"a\":1,\"b\":1}" ), object.get( "c" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonMergepatch.class) + public void testJsonMergepatchVarargs(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_mergepatch('{\"a\":456, \"b\":[1,2], \"c\":{\"a\":1}}', '{\"a\":null, \"b\":[4,5], \"c\":{\"b\":1}}', '{\"d\":1}')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 3, object.size() ); + assertEquals( Arrays.asList( parseArray( "[4,5]" ) ), object.get( "b" ) ); + assertEquals( parseObject( "{\"a\":1,\"b\":1}" ), object.get( "c" ) ); + assertEquals( 1, object.get( "d" ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 2ad5d9a39c76..daf9a94fb09e 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -817,6 +817,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonMergepatch implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_mergepatch" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; From a00a6a66e093a2ada829657420bfc98ae6c174a2 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Tue, 17 Sep 2024 16:20:26 +0200 Subject: [PATCH 13/15] HHH-18604 Spotless license header and EOL config --- .gitattributes | 1 + ...stractPostgreSQLJsonArrayPGObjectType.java | 8 ++---- .../dialect/H2JsonArrayJdbcType.java | 6 ++--- .../MySQLCastingJsonArrayJdbcType.java | 6 ++--- .../dialect/OracleJsonArrayJdbcType.java | 6 ++--- .../PostgreSQLCastingJsonArrayJdbcType.java | 6 ++--- .../PostgreSQLJsonArrayPGObjectJsonType.java | 6 ++--- .../PostgreSQLJsonArrayPGObjectJsonbType.java | 6 ++--- .../json/AbstractJsonInsertFunction.java | 6 ++--- .../json/AbstractJsonMergepatchFunction.java | 6 ++--- .../json/AbstractJsonRemoveFunction.java | 6 ++--- .../json/AbstractJsonReplaceFunction.java | 6 ++--- .../json/AbstractJsonSetFunction.java | 6 ++--- .../json/CastTargetReturnTypeResolver.java | 6 ++--- .../json/CockroachDBJsonExistsFunction.java | 6 ++--- .../json/CockroachDBJsonQueryFunction.java | 6 ++--- .../json/CockroachDBJsonRemoveFunction.java | 6 ++--- .../json/CockroachDBJsonValueFunction.java | 6 ++--- .../json/DB2JsonArrayAggFunction.java | 6 ++--- .../function/json/DB2JsonArrayFunction.java | 6 ++--- .../json/DB2JsonObjectAggFunction.java | 7 ++---- .../function/json/DB2JsonObjectFunction.java | 6 ++--- .../function/json/ExpressionTypeHelper.java | 6 ++--- .../function/json/H2JsonArrayAggFunction.java | 6 ++--- .../function/json/H2JsonExistsFunction.java | 6 ++--- .../json/H2JsonObjectAggFunction.java | 6 ++--- .../function/json/H2JsonQueryFunction.java | 6 ++--- .../function/json/H2JsonValueFunction.java | 9 ++----- .../json/HANAJsonArrayAggFunction.java | 6 ++--- .../function/json/HANAJsonArrayFunction.java | 6 ++--- .../function/json/HANAJsonExistsFunction.java | 6 ++--- .../json/HANAJsonObjectAggFunction.java | 6 ++--- .../function/json/HANAJsonObjectFunction.java | 6 ++--- .../json/HSQLJsonArrayAggFunction.java | 6 ++--- .../function/json/HSQLJsonArrayFunction.java | 6 ++--- .../function/json/HSQLJsonObjectFunction.java | 6 ++--- .../function/json/JsonArrayAggFunction.java | 6 ++--- .../function/json/JsonArrayFunction.java | 6 ++--- .../function/json/JsonExistsFunction.java | 8 ++---- .../function/json/JsonObjectAggFunction.java | 6 ++--- .../json/JsonObjectArgumentsValidator.java | 6 ++--- .../function/json/JsonObjectFunction.java | 6 ++--- .../dialect/function/json/JsonPathHelper.java | 6 ++--- .../function/json/JsonQueryFunction.java | 7 ++---- .../function/json/JsonValueFunction.java | 6 ++--- .../json/MariaDBJsonArrayAggFunction.java | 6 ++--- .../json/MariaDBJsonArrayFunction.java | 6 ++--- .../json/MariaDBJsonObjectAggFunction.java | 6 ++--- .../json/MariaDBJsonQueryFunction.java | 6 ++--- .../json/MariaDBJsonValueFunction.java | 6 ++--- .../json/MySQLJsonArrayAggFunction.java | 6 ++--- .../function/json/MySQLJsonArrayFunction.java | 8 ++---- .../json/MySQLJsonExistsFunction.java | 6 ++--- .../json/MySQLJsonObjectAggFunction.java | 9 ++----- .../json/MySQLJsonObjectFunction.java | 6 ++--- .../function/json/MySQLJsonQueryFunction.java | 6 ++--- .../function/json/MySQLJsonValueFunction.java | 6 ++--- .../json/OracleJsonArrayAggFunction.java | 6 ++--- .../json/OracleJsonArrayFunction.java | 6 ++--- .../json/OracleJsonInsertFunction.java | 6 ++--- .../json/OracleJsonMergepatchFunction.java | 6 ++--- .../json/OracleJsonObjectAggFunction.java | 6 ++--- .../json/OracleJsonObjectFunction.java | 6 ++--- .../json/OracleJsonRemoveFunction.java | 6 ++--- .../json/OracleJsonReplaceFunction.java | 6 ++--- .../function/json/OracleJsonSetFunction.java | 6 ++--- .../json/PostgreSQLJsonArrayAggFunction.java | 7 ++---- .../json/PostgreSQLJsonArrayFunction.java | 6 ++--- .../json/PostgreSQLJsonExistsFunction.java | 6 ++--- .../json/PostgreSQLJsonInsertFunction.java | 6 ++--- .../PostgreSQLJsonMergepatchFunction.java | 6 ++--- .../json/PostgreSQLJsonObjectAggFunction.java | 7 ++---- .../json/PostgreSQLJsonObjectFunction.java | 6 ++--- .../json/PostgreSQLJsonQueryFunction.java | 6 ++--- .../json/PostgreSQLJsonRemoveFunction.java | 7 ++---- .../json/PostgreSQLJsonReplaceFunction.java | 6 ++--- .../json/PostgreSQLJsonSetFunction.java | 6 ++--- .../json/PostgreSQLJsonValueFunction.java | 6 ++--- .../json/SQLServerJsonArrayAggFunction.java | 6 ++--- .../json/SQLServerJsonArrayFunction.java | 6 ++--- .../json/SQLServerJsonExistsFunction.java | 7 ++---- .../json/SQLServerJsonInsertFunction.java | 6 ++--- .../json/SQLServerJsonObjectAggFunction.java | 6 ++--- .../json/SQLServerJsonObjectFunction.java | 6 ++--- .../json/SQLServerJsonQueryFunction.java | 6 ++--- .../json/SQLServerJsonRemoveFunction.java | 6 ++--- .../json/SQLServerJsonReplaceFunction.java | 6 ++--- .../json/SQLServerJsonSetFunction.java | 6 ++--- .../json/SQLServerJsonValueFunction.java | 6 ++--- .../hibernate/internal/util/NumberHelper.java | 6 ++--- .../internal/util/QuotingHelper.java | 1 - .../criteria/JpaJsonExistsExpression.java | 6 ++--- .../criteria/JpaJsonQueryExpression.java | 6 ++--- .../criteria/JpaJsonValueExpression.java | 6 ++--- .../sqm/sql/BaseSqmToSqlAstConverter.java | 2 -- .../AbstractSqmJsonPathExpression.java | 6 ++--- .../expression/SqmJsonExistsExpression.java | 6 ++--- .../tree/expression/SqmJsonNullBehavior.java | 6 ++--- .../SqmJsonObjectAggUniqueKeysBehavior.java | 6 ++--- .../expression/SqmJsonQueryExpression.java | 6 ++--- .../expression/SqmJsonValueExpression.java | 6 ++--- .../expression/JsonExistsErrorBehavior.java | 6 ++--- .../ast/tree/expression/JsonNullBehavior.java | 6 ++--- .../JsonObjectAggUniqueKeysBehavior.java | 6 ++--- .../expression/JsonPathPassingClause.java | 6 ++--- .../expression/JsonQueryEmptyBehavior.java | 6 ++--- .../expression/JsonQueryErrorBehavior.java | 6 ++--- .../tree/expression/JsonQueryWrapMode.java | 6 ++--- .../expression/JsonValueEmptyBehavior.java | 6 ++--- .../expression/JsonValueErrorBehavior.java | 6 ++--- .../jdbc/JsonArrayAsStringJdbcType.java | 6 ++--- .../descriptor/jdbc/JsonArrayJdbcType.java | 6 ++--- .../jdbc/OracleJsonArrayBlobJdbcType.java | 6 ++--- .../jdbc/OracleJsonBlobJdbcType.java | 1 - .../dialect/function/JsonPathHelperTest.java | 25 ++----------------- .../test/function/json/EntityWithJson.java | 6 ++--- .../function/json/JsonArrayAggregateTest.java | 6 ++--- .../orm/test/function/json/JsonArrayTest.java | 6 ++--- .../test/function/json/JsonExistsTest.java | 6 ++--- .../test/function/json/JsonInsertTest.java | 6 ++--- .../json/JsonObjectAggregateTest.java | 6 ++--- .../test/function/json/JsonObjectTest.java | 6 ++--- .../orm/test/function/json/JsonQueryTest.java | 6 ++--- .../test/function/json/JsonRemoveTest.java | 6 ++--- .../test/function/json/JsonReplaceTest.java | 6 ++--- .../orm/test/function/json/JsonSetTest.java | 13 ++-------- .../orm/test/function/json/JsonValueTest.java | 6 ++--- 127 files changed, 247 insertions(+), 540 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..07764a78d984 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java index 2b6c337e0728..30a5daf6f403 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/AbstractPostgreSQLJsonArrayPGObjectType.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect; @@ -11,7 +9,6 @@ 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; @@ -20,7 +17,6 @@ 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; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java index fa2bfcb07c00..8406dfbc6051 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2JsonArrayJdbcType.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java index c95e2e063532..9a774854074a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLCastingJsonArrayJdbcType.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java index c4652262aa5f..5def46e4ed51 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleJsonArrayJdbcType.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java index 9726402cae34..5d06d7140f9a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingJsonArrayJdbcType.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java index 0ed27bc854f8..43daecfe841b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonType.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java index ba1fded5552e..c3245958ff14 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLJsonArrayPGObjectJsonbType.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.java index d41263c9a376..11cc6cbe8917 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonInsertFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.java index f7dfad53f2fe..466526a67b36 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonMergepatchFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java index c42ef85ac98a..fa17d5a67fd1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.java index 4f8808d04536..cf221fad3b91 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonReplaceFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java index b03274d18845..af2aced9815e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java index 8d33450f4cb7..102c6d5ea137 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CastTargetReturnTypeResolver.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java index 858593e0a53e..79778d362f3a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonExistsFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java index e011aebd3d81..c51992bf88bd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonQueryFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.java index c50d4683d828..561212ddc3ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonRemoveFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java index ada19353b3bd..4851752a370b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/CockroachDBJsonValueFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index ac999b0d108d..a1267fdddfc5 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index cfe60664ebd0..c3a07379e232 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.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 index 4b347f47d8c2..52f69b2f7ac5 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -16,7 +14,6 @@ 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; /** 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 index 9e6f485e4290..d9d57a77599d 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.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 index 8f18b2eefad2..5a4211ea592c 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index c8aa67cd76ce..1a076aac334a 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 12fdc9d55b7b..d37fead1ccc9 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 6ba7cee5d44c..f0ac757f1778 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java index 18088d0e5d8f..6550802e0be1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonQueryFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java index 1777a2375607..f704fccb212a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/H2JsonValueFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -10,11 +8,8 @@ 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; 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 index cc1828866143..95a2bf14f2bd 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index cfd929fb5168..379ac10e503f 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java index 6ce661e428f8..78c837234163 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HANAJsonExistsFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 9b04ee7d0bf4..5069a13e92d8 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 1c971e830bbd..3a60ffea5de2 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayAggFunction.java index e5f8393c5406..af939d46948d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/HSQLJsonArrayAggFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index f763534bf048..6dcdceac7a62 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 50c27676ce8f..224c23698ac2 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java index 824bb11cd71d..66dbc8c971a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayAggFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java index 0fa325707bcc..230af1419bdd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonArrayFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 0d5c31c70edc..ca1bc08da44d 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -16,7 +14,6 @@ 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; @@ -31,7 +28,6 @@ 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; 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 index 82d13a9caa98..fb2e0025fc53 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index b1c9af7aa4c5..abc348207792 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index b165734f35fa..5101655f2ea0 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java index 743b9af997ec..87f5b83e64a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 29f7543c2bac..0665aa7df87b 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -24,7 +22,6 @@ 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; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java index f057613286bf..eca1c9a58066 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonValueFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 6c822bd8a332..7e5a8b64e074 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 4fad7aa58735..fb8c03c1f8b0 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index b3469ebb0d4a..a5b48b9d8058 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 08d1b8f4805a..83849d7ab19d 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java index c144dee92a96..857450dfacd5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonValueFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 4e568127df79..c00a154aa66f 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.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 index 80b2ae965b26..daa669c18c10 100644 --- 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 @@ -1,20 +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 http://www.gnu.org/licenses/lgpl-2.1.html + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ 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; /** 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 index f7e71abd3c3e..cb76a1a6906a 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 152925c59ccc..e4c74788a0eb 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -11,13 +9,10 @@ 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; /** 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 index 47bd7fe67529..8c9f95551d07 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index e66a75970474..5a4240dbd31e 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java index 138c536ce1e4..686b04dfb71c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MySQLJsonValueFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 66b7f8c0e60c..9cb55cc79591 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.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 index 2d182789d1fc..e3ea2df0817d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java index 851049a57370..50d677a31080 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonInsertFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java index 940e8cf10142..2aae5e7923d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonMergepatchFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java index fa58ed52ea27..4e505a560222 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java index eaad48b202a9..7ee2ef0f620f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java index 5e4e5d1fdc60..b6dabfea3203 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java index 63ad699053f4..872a28ee4f89 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonReplaceFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java index 60e906bcbedd..f60debdae681 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 1e11f35dbe66..88758f0667ac 100644 --- 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 @@ -1,14 +1,11 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ 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; 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 index 9a37181f2441..57df67b8adf1 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java index aa193c605500..6d43e3eede51 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonExistsFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java index 068c484473e5..d09d4934ad7e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java index 5a3d965aeae6..d066a97124bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonMergepatchFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index df73b06ff828..4f47ee875d7d 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -11,7 +9,6 @@ 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; 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 index 3bd07316f386..48ede18e2f6c 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java index 4782d9912a90..40a497777862 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonQueryFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java index 59ec9b10e07d..b8838771e36d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -16,7 +14,6 @@ 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.Literal; import org.hibernate.type.spi.TypeConfiguration; /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java index e8c97c40d345..6aba8ff7b82e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonReplaceFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java index 33e952a72fcd..e5f02d9aa3f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java index f6b7f8b63ad1..9f7081625a2e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonValueFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 8302e56b823f..d31087f083ae 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index a5066f7569a9..0b7788ad86bb 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index ca851ca4e6c2..93bc7833f6aa 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; @@ -12,7 +10,6 @@ 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; /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java index 1bce8a451bae..1aeaf1015ceb 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index ac06321cf266..02c51abb9e2d 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index d32fd2e116ab..16d8a811abad 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 64e1fd36888a..74d3494ef0fc 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java index 0936a24bfd9c..b1f745bf61a6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java index 35ed15262607..a0177ae1a6cd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java index b9660ca2cee7..ac2aef859a6f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 7a0663b82b9e..e243469122e6 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.dialect.function.json; 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 index 8db8505cb9b6..07e57262bd96 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/NumberHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/NumberHelper.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.internal.util; 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 9153315a7910..cd64310ba889 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 @@ -4,7 +4,6 @@ */ package org.hibernate.internal.util; -import org.hibernate.sql.ast.spi.SqlAppender; public final class QuotingHelper { diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java index cd91ffc10dab..11bf095bcb18 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonExistsExpression.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.criteria; diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java index 399fcdbbd4a5..7b06de2d6672 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonQueryExpression.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.criteria; diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java index 87e3991621ab..74e836c95f15 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/JpaJsonValueExpression.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.criteria; 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 7a9854b87e79..2d06423e2310 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 @@ -289,7 +289,6 @@ 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; @@ -298,7 +297,6 @@ 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; 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 index 72af4fa2a33f..14c564ff30b1 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.sqm.tree.expression; 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 index b56e3a0c9136..d39f8704a1c4 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.sqm.tree.expression; 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 index ee3399435cb7..ce9cbc7efc54 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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. + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.sqm.tree.expression; 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 index 7cfd8e3b3233..ef7bf81f24a5 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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. + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.sqm.tree.expression; 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 index f1c83bbd201d..0acaaef3ff61 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.sqm.tree.expression; 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 index ab07af0dedc9..1122ae5d3a3c 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.query.sqm.tree.expression; 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 index ba2f8a799015..d8a36040b1c8 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index 8ef7b60bc2c5..bd6a83b976e1 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index 6d2c32eaaf5c..1ebfd40f7d01 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index c77ef12a4802..9e4a688a488e 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index db4f304ec05c..00ff4d636b06 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index 0aec8a3438c8..d8bd36b068c5 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index b951d4f970aa..b36ee424936f 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index 121968ab5d99..6ebb71efd10b 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index d70a16fa0e37..7c4d5a5eb30e 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.sql.ast.tree.expression; 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 index b67e056acb8f..17bb5794d385 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayAsStringJdbcType.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.type.descriptor.jdbc; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java index 4835cd84bd90..9397bb2320d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JsonArrayJdbcType.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.type.descriptor.jdbc; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java index 12fe9aaff652..ffcf78be351c 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/OracleJsonArrayBlobJdbcType.java @@ -1,8 +1,6 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.type.descriptor.jdbc; 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 f4be54c890ca..b481d20314cc 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 @@ -11,7 +11,6 @@ import java.sql.SQLException; import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.JsonHelper; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.type.SqlTypes; 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 index 0d00f0cf780a..a98aa266bf6d 100644 --- 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 @@ -1,34 +1,13 @@ /* - * 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 + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ 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; 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 index 558ca50e848d..81ee56f9853e 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.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 index 3cb15e705627..0c6378773169 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; 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 index 7edd9ee8483f..6411ba38bc96 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; 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 index 84a373cdf209..c107ec4020a2 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.java index 7324fa1f4618..bbd679643553 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonInsertTest.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; 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 index 7b1a9f276bd2..966cc0ca8872 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; 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 index e5dfa2f4997d..689b21a24eff 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; 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 index c9f7b301df03..e65cc8226546 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java index 590651c5897b..3577c86e4496 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.java index 704d94e51699..41b984f8fa8f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonReplaceTest.java @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java index dd5dc00d9a78..1e1ebf09ccfc 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java @@ -1,18 +1,10 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ 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; @@ -22,7 +14,6 @@ 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; /** 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 index 231d983f1d0b..e3d391005a3f 100644 --- 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 @@ -1,8 +1,6 @@ /* - * 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 . + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.json; From 30af85038b325f7001e4169d1c293b925b6e6b00 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 18 Sep 2024 10:05:12 +0200 Subject: [PATCH 14/15] HHH-18604 Add json_array_append function --- .../chapters/query/hql/QueryLanguage.adoc | 21 +++++ .../dialect/CockroachLegacyDialect.java | 1 + .../dialect/MariaDBLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 1 + .../dialect/SQLServerLegacyDialect.java | 1 + .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/MariaDBDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 1 + .../hibernate/dialect/PostgreSQLDialect.java | 1 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 49 +++++++++++ .../json/AbstractJsonArrayAppendFunction.java | 38 ++++++++ .../json/MariaDBJsonArrayAppendFunction.java | 47 ++++++++++ .../json/OracleJsonArrayAppendFunction.java | 59 +++++++++++++ .../PostgreSQLJsonArrayAppendFunction.java | 88 +++++++++++++++++++ .../SQLServerJsonArrayAppendFunction.java | 56 ++++++++++++ .../function/json/JsonArrayAppendTest.java | 37 ++++++++ .../orm/test/query/hql/JsonFunctionTests.java | 64 ++++++++++++++ .../orm/junit/DialectFeatureChecks.java | 6 ++ 22 files changed, 477 insertions(+) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayAppendFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAppendFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAppendFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAppendFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAppendTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 94fac181b789..5615f1e611cc 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1642,6 +1642,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_set()` | Inserts/Replaces a value by JSON path within a JSON document | `json_remove()` | Removes a value by JSON path within a JSON document | `json_mergepatch()` | Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge +| `json_array_append()` | Appends to a JSON array of a JSON document by JSON path |=== @@ -2113,6 +2114,26 @@ include::{json-example-dir-hql}/JsonMergepatchTest.java[tags=hql-json-mergepatch WARNING: SAP HANA, DB2, SQL Server, H2 and HSQLDB do not support this function. On PostgreSQL, this function is emulated. +[[hql-json-array-append-function]] +===== `json_array_append()` + +Appends a value by JSON path to a JSON array within a JSON document. +The function takes 3 arguments, the json document, the json path and the value to append. + +If the value within the JSON document as identified by the JSON path is not a JSON array, +it is auto-wrapped into an array. +When no value exists for a JSON path, the document is not changed. + +[[hql-json-array-append-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayAppendTest.java[tags=hql-json-array-append-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions 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 4967a7075180..25c0a0a52f5a 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 @@ -513,6 +513,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); + functionFactory.jsonArrayAppend_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java index e7666c0bf8e2..6abdf43300b8 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 @@ -95,6 +95,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio commonFunctionFactory.jsonQuery_mariadb(); commonFunctionFactory.jsonArrayAgg_mariadb(); commonFunctionFactory.jsonObjectAgg_mariadb(); + commonFunctionFactory.jsonArrayAppend_mariadb(); if ( getVersion().isSameOrAfter( 10, 3, 3 ) ) { commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) 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 bfa1acf85e63..bce5eecb7fa7 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 @@ -665,6 +665,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_mysql(); functionFactory.jsonInsert_mysql(); functionFactory.jsonMergepatch_mysql(); + functionFactory.jsonArrayAppend_mysql(); } } 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 9eec72fa187d..a5e8ddf9df60 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 @@ -322,6 +322,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_oracle(); functionFactory.jsonInsert_oracle(); functionFactory.jsonMergepatch_oracle(); + functionFactory.jsonArrayAppend_oracle(); } } 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 069a6c2c7245..37264b1f4ae1 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 @@ -652,6 +652,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); + functionFactory.jsonArrayAppend_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); 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 c93cc0086a25..8bab38898691 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 @@ -410,6 +410,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_sqlserver(); functionFactory.jsonReplace_sqlserver(); functionFactory.jsonInsert_sqlserver(); + functionFactory.jsonArrayAppend_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 269658529e59..6315eae6704a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -480,6 +480,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); + functionFactory.jsonArrayAppend_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 53d624a16e0d..6a8c504d7ae6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -98,6 +98,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio commonFunctionFactory.jsonQuery_mariadb(); commonFunctionFactory.jsonArrayAgg_mariadb(); commonFunctionFactory.jsonObjectAgg_mariadb(); + commonFunctionFactory.jsonArrayAppend_mariadb(); commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); functionContributions.getFunctionRegistry().patternDescriptorBuilder( "median", "median(?1) over ()" ) .setInvariantType( functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.DOUBLE ) ) 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 0ce6668b2755..bd1e75917fe1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -650,6 +650,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_mysql(); functionFactory.jsonInsert_mysql(); functionFactory.jsonMergepatch_mysql(); + functionFactory.jsonArrayAppend_mysql(); } @Override 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 d4330f7ec01c..b278e9316a65 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -413,6 +413,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_oracle(); functionFactory.jsonInsert_oracle(); functionFactory.jsonMergepatch_oracle(); + functionFactory.jsonArrayAppend_oracle(); } @Override 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 4561302daa91..14943ddfd37e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -613,6 +613,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_postgresql(); functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); + functionFactory.jsonArrayAppend_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 55dd455e5d78..f4a353c7631f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -428,6 +428,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonRemove_sqlserver(); functionFactory.jsonReplace_sqlserver(); functionFactory.jsonInsert_sqlserver(); + functionFactory.jsonArrayAppend_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 507b73880213..c2859ddce260 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 @@ -103,6 +103,7 @@ import org.hibernate.dialect.function.json.JsonQueryFunction; import org.hibernate.dialect.function.json.JsonValueFunction; import org.hibernate.dialect.function.json.MariaDBJsonArrayAggFunction; +import org.hibernate.dialect.function.json.MariaDBJsonArrayAppendFunction; import org.hibernate.dialect.function.json.MariaDBJsonArrayFunction; import org.hibernate.dialect.function.json.MariaDBJsonObjectAggFunction; import org.hibernate.dialect.function.json.MariaDBJsonQueryFunction; @@ -115,6 +116,7 @@ import org.hibernate.dialect.function.json.MySQLJsonQueryFunction; import org.hibernate.dialect.function.json.MySQLJsonValueFunction; import org.hibernate.dialect.function.json.OracleJsonArrayAggFunction; +import org.hibernate.dialect.function.json.OracleJsonArrayAppendFunction; import org.hibernate.dialect.function.json.OracleJsonArrayFunction; import org.hibernate.dialect.function.json.OracleJsonInsertFunction; import org.hibernate.dialect.function.json.OracleJsonMergepatchFunction; @@ -124,6 +126,7 @@ import org.hibernate.dialect.function.json.OracleJsonReplaceFunction; import org.hibernate.dialect.function.json.OracleJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAppendFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonInsertFunction; @@ -136,6 +139,7 @@ import org.hibernate.dialect.function.json.PostgreSQLJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; +import org.hibernate.dialect.function.json.SQLServerJsonArrayAppendFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; import org.hibernate.dialect.function.json.SQLServerJsonInsertFunction; @@ -4007,4 +4011,49 @@ public void jsonMergepatch_mysql() { public void jsonMergepatch_oracle() { functionRegistry.register( "json_mergepatch", new OracleJsonMergepatchFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_array_append() function + */ + public void jsonArrayAppend_postgresql() { + functionRegistry.register( "json_array_append", new PostgreSQLJsonArrayAppendFunction( typeConfiguration ) ); + } + + /** + * MySQL json_array_append() function + */ + public void jsonArrayAppend_mysql() { + functionRegistry.namedDescriptorBuilder( "json_array_append" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * MariaDB json_array_append() function + */ + public void jsonArrayAppend_mariadb() { + functionRegistry.register( "json_array_append", new MariaDBJsonArrayAppendFunction( typeConfiguration ) ); + } + + /** + * Oracle json_array_append() function + */ + public void jsonArrayAppend_oracle() { + functionRegistry.register( "json_array_append", new OracleJsonArrayAppendFunction( typeConfiguration ) ); + } + + /** + * SQL server json_array_append() function + */ + public void jsonArrayAppend_sqlserver() { + functionRegistry.register( "json_array_append", new SQLServerJsonArrayAppendFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayAppendFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayAppendFunction.java new file mode 100644 index 000000000000..a74186aa3dc7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayAppendFunction.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_array_append function. + */ +public abstract class AbstractJsonArrayAppendFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonArrayAppendFunction(TypeConfiguration typeConfiguration) { + super( + "json_array_append", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAppendFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAppendFunction.java new file mode 100644 index 000000000000..31ea28be9912 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/MariaDBJsonArrayAppendFunction.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * MariaDB json_array_append function. + */ +public class MariaDBJsonArrayAppendFunction extends AbstractJsonArrayAppendFunction { + + public MariaDBJsonArrayAppendFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_replace(" ); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ",json_merge(json_extract(" ); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( "),json_array(" ); + value.accept( translator ); + sqlAppender.appendSql( ")))" ); + + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAppendFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAppendFunction.java new file mode 100644 index 000000000000..bfd38c1080ac --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAppendFunction.java @@ -0,0 +1,59 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_array_append function. + */ +public class OracleJsonArrayAppendFunction extends AbstractJsonArrayAppendFunction { + + public OracleJsonArrayAppendFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final String jsonPath = translator.getLiteralValue( (Expression) arguments.get( 1 ) ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "(select case coalesce(json_value(t.d,'" ); + for ( int i = 0; i < jsonPath.length(); i++ ) { + final char c = jsonPath.charAt( i ); + if ( c == '\'') { + sqlAppender.appendSql( "'" ); + } + sqlAppender.appendSql( c ); + } + sqlAppender.appendSql( ".type()'),'x') when 'x' then t.d when 'array' then json_transform(t.d,append " ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( "=t.v) when 'object' then json_transform(t.d,set " ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( "=json_array(coalesce(json_query(t.d," ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( "),'null') format json,t.v)) else json_transform(t.d,set " ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( "=json_array(coalesce(json_value(t.d," ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( "),'null') format json,t.v)) end from (select " ); + json.accept( translator ); + sqlAppender.appendSql( " d," ); + value.accept( translator ); + sqlAppender.appendSql( " v from dual) t)" ); + + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAppendFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAppendFunction.java new file mode 100644 index 000000000000..2c158745c05c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayAppendFunction.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_array_append function. + */ +public class PostgreSQLJsonArrayAppendFunction extends AbstractJsonArrayAppendFunction { + + public PostgreSQLJsonArrayAppendFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "(select jsonb_set_lax(t.d,t.p,(t.d)#>t.p||" ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( translator ); + 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( ",false,'return_target') from (values(" ); + final boolean needsCast = !isJsonType( json ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() + 1 ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[]" ); + sqlAppender.appendSql( ")) t(d,p))" ); + } + + private static boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java new file mode 100644 index 000000000000..4403224b0e16 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_array_append function. + */ +public class SQLServerJsonArrayAppendFunction extends AbstractJsonArrayAppendFunction { + + public SQLServerJsonArrayAppendFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "(select coalesce(" ); + sqlAppender.appendSql("case when json_modify(json_query(t.d,t.p),'append $',t.v) is not null then json_modify(t.d,t.p,json_modify(json_query(t.d,t.p),'append $',t.v)) end,"); + sqlAppender.appendSql("json_modify(t.d,t.p,json_query('['+coalesce(json_value(t.d,t.p),case when json_path_exists(t.d,t.p)=1 then 'null' end)+stuff(json_array(t.v),1,1,','))),"); + sqlAppender.appendSql( "t.d) from (values (" ); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ',' ); + renderValue( sqlAppender, value, translator ); + sqlAppender.appendSql( ")) t(d,p,v))" ); + } + + protected void renderValue(SqlAppender sqlAppender, SqlAstNode value, SqlAstTranslator translator) { + if ( ExpressionTypeHelper.isBoolean( value ) ) { + sqlAppender.appendSql( "cast(" ); + value.accept( translator ); + sqlAppender.appendSql( " as bit)" ); + } + else { + value.accept( translator ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAppendTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAppendTest.java new file mode 100644 index 000000000000..2afcfc53fcf2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayAppendTest.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.SupportsJsonArrayAppend.class) +public class JsonArrayAppendTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-array-append-example[] + em.createQuery( "select json_array_append('{\"a\":[1]}', '$.a', 2)" ).getResultList(); + //end::hql-json-array-append-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 805a661fd388..032e9526a9b7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -640,6 +640,70 @@ public void testJsonMergepatchVarargs(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayAppend.class) + public void testJsonArrayAppend(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_append('{\"b\":[2]}', '$.b', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( Arrays.asList( 2, 1 ), object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayAppend.class) + public void testJsonArrayAppendNonExisting(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_append('{\"b\":[2]}', '$.c', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( List.of( 2 ), object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayAppend.class) + public void testJsonArrayAppendNonArray(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_append('{\"b\":2}', '$.b', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( Arrays.asList( 2, 1 ), object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayAppend.class) + public void testJsonArrayAppendToNull(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_append('{\"b\":null}', '$.b', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( Arrays.asList( null, 1 ), object.get( "b" ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index daf9a94fb09e..0052370de61d 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -823,6 +823,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonArrayAppend implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_array_append" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; From a6343cb18407926e15aeddad5c1ce7ab7ea10367 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Wed, 18 Sep 2024 12:40:10 +0200 Subject: [PATCH 15/15] HHH-18604 Add json_array_insert --- .../chapters/query/hql/QueryLanguage.adoc | 24 +++++ .../dialect/CockroachLegacyDialect.java | 1 + .../community/dialect/MySQLLegacyDialect.java | 1 + .../dialect/OracleLegacyDialect.java | 1 + .../dialect/PostgreSQLLegacyDialect.java | 1 + .../dialect/SQLServerLegacyDialect.java | 1 + .../hibernate/dialect/CockroachDialect.java | 1 + .../org/hibernate/dialect/MySQLDialect.java | 1 + .../org/hibernate/dialect/OracleDialect.java | 1 + .../hibernate/dialect/PostgreSQLDialect.java | 1 + .../hibernate/dialect/SQLServerDialect.java | 1 + .../function/CommonFunctionFactory.java | 41 +++++++++ .../json/AbstractJsonArrayInsertFunction.java | 38 ++++++++ .../json/OracleJsonArrayInsertFunction.java | 43 +++++++++ .../PostgreSQLJsonArrayInsertFunction.java | 87 ++++++++++++++++++ .../json/PostgreSQLJsonInsertFunction.java | 2 +- .../SQLServerJsonArrayInsertFunction.java | 88 +++++++++++++++++++ .../function/json/JsonArrayInsertTest.java | 37 ++++++++ .../orm/test/query/hql/JsonFunctionTests.java | 64 ++++++++++++++ .../orm/junit/DialectFeatureChecks.java | 6 ++ 20 files changed, 439 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayInsertFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayInsertFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayInsertFunction.java create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayInsertTest.java diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index 5615f1e611cc..455fb039bfd6 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1643,6 +1643,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_remove()` | Removes a value by JSON path within a JSON document | `json_mergepatch()` | Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge | `json_array_append()` | Appends to a JSON array of a JSON document by JSON path +| `json_array_insert()` | Inserts a value by JSON path to a JSON array within a JSON document |=== @@ -2134,6 +2135,29 @@ include::{json-example-dir-hql}/JsonArrayAppendTest.java[tags=hql-json-array-app WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-json-array-insert-function]] +===== `json_array_insert()` + +Inserts a value by JSON path to a JSON array within a JSON document. +The function takes 3 arguments, the json document, the json path and the value to append. + +Although the exact behavior is database dependent, usually an error will be triggered if +the JSON path does not end with an array index access i.e. `$.a[0]`. +The zero based array index represents the position at which an element should be inserted in an array. + +If the JSON path without the index does not resolve to a JSON array within the JSON document, +the document is not changed. + +[[hql-json-array-insert-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayInsertTest.java[tags=hql-json-array-insert-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions 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 25c0a0a52f5a..27919427a031 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 @@ -514,6 +514,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index bce5eecb7fa7..ef12c686e38b 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 @@ -666,6 +666,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_mysql(); functionFactory.jsonMergepatch_mysql(); functionFactory.jsonArrayAppend_mysql(); + functionFactory.jsonArrayInsert_mysql(); } } 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 a5e8ddf9df60..215f03e90879 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 @@ -323,6 +323,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_oracle(); functionFactory.jsonMergepatch_oracle(); functionFactory.jsonArrayAppend_oracle(); + functionFactory.jsonArrayInsert_oracle(); } } 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 37264b1f4ae1..5aad34da38b6 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 @@ -653,6 +653,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); 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 8bab38898691..7a7512d24e13 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 @@ -411,6 +411,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_sqlserver(); functionFactory.jsonInsert_sqlserver(); functionFactory.jsonArrayAppend_sqlserver(); + functionFactory.jsonArrayInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 6315eae6704a..93bc7b5eb050 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -481,6 +481,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) 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 bd1e75917fe1..9e3f1d7d5631 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -651,6 +651,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_mysql(); functionFactory.jsonMergepatch_mysql(); functionFactory.jsonArrayAppend_mysql(); + functionFactory.jsonArrayInsert_mysql(); } @Override 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 b278e9316a65..33924ea91742 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -414,6 +414,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_oracle(); functionFactory.jsonMergepatch_oracle(); functionFactory.jsonArrayAppend_oracle(); + functionFactory.jsonArrayInsert_oracle(); } @Override 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 14943ddfd37e..23307a861747 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -614,6 +614,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions 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 f4a353c7631f..a3810537e092 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -429,6 +429,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.jsonReplace_sqlserver(); functionFactory.jsonInsert_sqlserver(); functionFactory.jsonArrayAppend_sqlserver(); + functionFactory.jsonArrayInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); 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 c2859ddce260..a3d2e63062a0 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 @@ -118,6 +118,7 @@ import org.hibernate.dialect.function.json.OracleJsonArrayAggFunction; import org.hibernate.dialect.function.json.OracleJsonArrayAppendFunction; import org.hibernate.dialect.function.json.OracleJsonArrayFunction; +import org.hibernate.dialect.function.json.OracleJsonArrayInsertFunction; import org.hibernate.dialect.function.json.OracleJsonInsertFunction; import org.hibernate.dialect.function.json.OracleJsonMergepatchFunction; import org.hibernate.dialect.function.json.OracleJsonObjectAggFunction; @@ -128,6 +129,7 @@ import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAppendFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonArrayInsertFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonInsertFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonMergepatchFunction; @@ -141,6 +143,7 @@ import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAppendFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; +import org.hibernate.dialect.function.json.SQLServerJsonArrayInsertFunction; import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; import org.hibernate.dialect.function.json.SQLServerJsonInsertFunction; import org.hibernate.dialect.function.json.SQLServerJsonObjectAggFunction; @@ -4056,4 +4059,42 @@ public void jsonArrayAppend_oracle() { public void jsonArrayAppend_sqlserver() { functionRegistry.register( "json_array_append", new SQLServerJsonArrayAppendFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_array_insert() function + */ + public void jsonArrayInsert_postgresql() { + functionRegistry.register( "json_array_insert", new PostgreSQLJsonArrayInsertFunction( typeConfiguration ) ); + } + + /** + * MySQL json_array_insert() function + */ + public void jsonArrayInsert_mysql() { + functionRegistry.namedDescriptorBuilder( "json_array_insert" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_array_insert() function + */ + public void jsonArrayInsert_oracle() { + functionRegistry.register( "json_array_insert", new OracleJsonArrayInsertFunction( typeConfiguration ) ); + } + + /** + * SQL server json_array_insert() function + */ + public void jsonArrayInsert_sqlserver() { + functionRegistry.register( "json_array_insert", new SQLServerJsonArrayInsertFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayInsertFunction.java new file mode 100644 index 000000000000..8583f161a5aa --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayInsertFunction.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_array_insert function. + */ +public abstract class AbstractJsonArrayInsertFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( + "json_array_insert", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayInsertFunction.java new file mode 100644 index 000000000000..dbb1045aefa1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayInsertFunction.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_array_insert function. + */ +public class OracleJsonArrayInsertFunction extends AbstractJsonArrayInsertFunction { + + public OracleJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final String jsonPath = translator.getLiteralValue( (Expression) arguments.get( 1 ) ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",insert " ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( '=' ); + value.accept( translator ); + sqlAppender.appendSql( " ignore on existing)" ); + + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayInsertFunction.java new file mode 100644 index 000000000000..1a5c987eb6a3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayInsertFunction.java @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.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.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_array_insert function. + */ +public class PostgreSQLJsonArrayInsertFunction extends AbstractJsonArrayInsertFunction { + + public PostgreSQLJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "jsonb_insert(" ); + final boolean needsCast = !isJsonType( json ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[]," ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( translator ); + 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( ')' ); + } + + private static boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java index d09d4934ad7e..3935f8a92bf4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java @@ -34,7 +34,7 @@ public void render( final Expression json = (Expression) arguments.get( 0 ); final Expression jsonPath = (Expression) arguments.get( 1 ); final SqlAstNode value = arguments.get( 2 ); - sqlAppender.appendSql( "(select case when t.d#>>t.p is not null then t.d else jsonb_insert(t.d,t.p," ); + sqlAppender.appendSql( "(select case when (t.d)#>>t.p is not null then t.d else jsonb_insert(t.d,t.p," ); if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { sqlAppender.appendSql( "null::jsonb" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java new file mode 100644 index 000000000000..24c9d45c5d1b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_array_insert function. + */ +public class SQLServerJsonArrayInsertFunction extends AbstractJsonArrayInsertFunction { + + public SQLServerJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final String jsonPath = translator.getLiteralValue( (Expression) arguments.get( 1 ) ).trim(); + final int bracketEndIndex = jsonPath.lastIndexOf( ']' ); + final int bracketStartIndex = jsonPath.lastIndexOf( '[' ); + if ( jsonPath.isEmpty() + || bracketEndIndex != jsonPath.length() - 1 + || bracketStartIndex == -1 ) { + throw new QueryException( "JSON path does not end with an array index: " + jsonPath ); + } + final int index; + try { + index = Integer.parseInt( jsonPath.substring( bracketStartIndex + 1, bracketEndIndex ) ); + } + catch ( NumberFormatException e ) { + throw new QueryException( "JSON path does not point to a valid array index: " + jsonPath ); + } + final Expression json = (Expression) arguments.get( 0 ); + final SqlAstNode value = arguments.get( 2 ); + // Only replace data if this is an array + sqlAppender.appendSql( "(select case when left(json_query(x.d,x.p),1)='[' then " ); + // Replace the array + sqlAppender.appendSql( "json_modify(x.d,x.p,json_query((" ); + // Aggregate a new JSON array based on element rows + sqlAppender.appendSql( "select '['+string_agg(t.v,',') within group (order by t.k)+']' from (" ); + + sqlAppender.appendSql( "select x.i k,x.v v union all " ); + sqlAppender.appendSql( "select case when cast(t.[key] as int)>=x.i then cast(t.[key] as int)+1 " ); + sqlAppender.appendSql( "else cast(t.[key] as int) end," ); + // 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 from openjson(x.d,x.p) t) t))) " ); + sqlAppender.appendSql( " else x.d end " ); + // Push args into a values clause since we are going to refer to them multiple times + sqlAppender.appendSql( "from (values(" ); + json.accept( translator ); + sqlAppender.append( ',' ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath.substring( 0, bracketStartIndex ) ); + sqlAppender.append( ',' ); + sqlAppender.appendSql( index ); + sqlAppender.append( ',' ); + value.accept( translator ); + sqlAppender.append( ")) x(d,p,i,v))" ); + } + + protected void renderArgument( + SqlAppender sqlAppender, + SqlAstNode arg, + 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/test/java/org/hibernate/orm/test/function/json/JsonArrayInsertTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayInsertTest.java new file mode 100644 index 000000000000..b1b97c901176 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayInsertTest.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +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.SupportsJsonInsert.class) +public class JsonArrayInsertTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-array-insert-example[] + em.createQuery( "select json_array_insert('{\"a\":[1]}', '$.a[0]', 2)" ).getResultList(); + //end::hql-json-array-insert-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 032e9526a9b7..cb5e2299daca 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -704,6 +704,70 @@ public void testJsonArrayAppendToNull(SessionFactoryScope scope) { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsert(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":[2]}', '$.b[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( Arrays.asList( 1, 2 ), object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsertNonExisting(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":[2]}', '$.c[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( List.of( 2 ), object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsertNonArray(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":2}', '$.b[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 2, object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsertToNull(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":null}', '$.b[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertNull( object.get( "b" ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 0052370de61d..96e6c11a734e 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -829,6 +829,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsJsonArrayInsert implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_array_insert" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;