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 b9d2eb38a1..939b72d8bc 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 0000000000..3cf2ca6b62 --- /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 23a95630ff..35ceee43dd 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 class CockroachLegacyDialect extends Dialect { 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 986b114669..276adb29bf 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 @@ -437,6 +437,7 @@ public class DB2LegacyDialect extends Dialect { 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 3a1e756ac3..8cc4b20d29 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 @@ -407,6 +407,7 @@ public class H2LegacyDialect extends Dialect { 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 95dc215757..902ea452f4 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 class HSQLLegacyDialect extends Dialect { 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 8eab1428f1..7b74be7d18 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 class MariaDBLegacyDialect extends MySQLLegacyDialect { 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 91adce1680..d60f4075a1 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 class MySQLLegacyDialect extends Dialect { 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 e9bc5ca499..7503b70eb9 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 @@ -638,7 +638,8 @@ public class PostgreSQLLegacyDialect extends Dialect { functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); - functionFactory.jsonArrayAgg(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); } else { functionFactory.jsonValue_postgresql(); @@ -647,12 +648,14 @@ public class PostgreSQLLegacyDialect extends Dialect { 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 dcc37ca6b9..5d2ab5fd3a 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 class SQLServerLegacyDialect extends AbstractTransactSQLDialect { 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 7de06724d5..1737e80444 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 8630a73a56..dfcc5aa1a5 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 972a2fe880..dc1edf76b3 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 class CockroachDialect extends Dialect { 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 81002d4037..023a550fbe 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -423,6 +423,7 @@ public class DB2Dialect extends Dialect { 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 358c2d7080..d8465a0dc2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -350,6 +350,7 @@ public class H2Dialect extends Dialect { 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 eccf7bec9f..b499e3b8d3 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 class HANADialect extends Dialect { 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 9e885ade0b..fe5ae3fa3b 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 class HSQLDialect extends Dialect { 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 3fdfffe148..6aa3b6e5aa 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 class MariaDBDialect extends MySQLDialect { 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 e06b51cb03..590a42bbaf 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 class MySQLDialect extends Dialect { 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 deb50f7a01..b2014b1949 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -599,7 +599,8 @@ public class PostgreSQLDialect extends Dialect { functionFactory.jsonExists(); functionFactory.jsonObject(); functionFactory.jsonArray(); - functionFactory.jsonArrayAgg(); + functionFactory.jsonArrayAgg_postgresql( true ); + functionFactory.jsonObjectAgg_postgresql( true ); } else { functionFactory.jsonValue_postgresql(); @@ -608,12 +609,14 @@ public class PostgreSQLDialect extends Dialect { 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 97a271f89f..4bf64348b5 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 class SQLServerDialect extends AbstractTransactSQLDialect { 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 b70f685b0f..6c019dcc84 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.OracleArrayConstructorFunction; import org.hibernate.dialect.function.array.OracleArrayContainsFunction; import org.hibernate.dialect.function.array.PostgreSQLArrayPositionsFunction; import org.hibernate.dialect.function.array.PostgreSQLArrayTrimEmulation; +import org.hibernate.dialect.function.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.HSQLJsonObjectFunction; import org.hibernate.dialect.function.json.JsonArrayAggFunction; import org.hibernate.dialect.function.json.JsonArrayFunction; import org.hibernate.dialect.function.json.JsonExistsFunction; +import org.hibernate.dialect.function.json.JsonObjectAggFunction; import org.hibernate.dialect.function.json.JsonObjectFunction; import org.hibernate.dialect.function.json.JsonQueryFunction; import org.hibernate.dialect.function.json.JsonValueFunction; import org.hibernate.dialect.function.json.MariaDBJsonArrayAggFunction; import org.hibernate.dialect.function.json.MariaDBJsonArrayFunction; +import org.hibernate.dialect.function.json.MariaDBJsonObjectAggFunction; import org.hibernate.dialect.function.json.MariaDBJsonQueryFunction; import org.hibernate.dialect.function.json.MariaDBJsonValueFunction; import org.hibernate.dialect.function.json.MySQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.MySQLJsonArrayFunction; import org.hibernate.dialect.function.json.MySQLJsonExistsFunction; +import org.hibernate.dialect.function.json.MySQLJsonObjectAggFunction; import org.hibernate.dialect.function.json.MySQLJsonObjectFunction; import org.hibernate.dialect.function.json.MySQLJsonQueryFunction; import org.hibernate.dialect.function.json.MySQLJsonValueFunction; @@ -113,12 +121,14 @@ import org.hibernate.dialect.function.json.OracleJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonObjectAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; +import org.hibernate.dialect.function.json.SQLServerJsonObjectAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonObjectFunction; import org.hibernate.dialect.function.json.SQLServerJsonQueryFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; @@ -3458,6 +3468,13 @@ public class CommonFunctionFactory { 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 class CommonFunctionFactory { 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 class CommonFunctionFactory { 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 class CommonFunctionFactory { /** * 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 class CommonFunctionFactory { 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 0000000000..858593e0a5 --- /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 0000000000..e011aebd3d --- /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 0000000000..4b347f47d8 --- /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 0000000000..6ba7cee5d4 --- /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 0000000000..9b04ee7d0b --- /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 0000000000..82d13a9caa --- /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 a6e8ff3af3..6c822bd8a3 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 @@ public class MariaDBJsonArrayAggFunction extends JsonArrayAggFunction { 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 0000000000..b3469ebb0d --- /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 e162f623e0..4e568127df 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 @@ public class MySQLJsonArrayAggFunction extends JsonArrayAggFunction { 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 0000000000..152925c59c --- /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 a15d14492a..1e11f35dbe 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 @@ import org.hibernate.type.spi.TypeConfiguration; */ 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 class PostgreSQLJsonArrayAggFunction extends JsonArrayAggFunction { 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 ); + if ( supportsStandard ) { + super.render( sqlAppender, sqlAstArguments, filter, withinGroup, returnType, translator ); } 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." ); + 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 ); } - 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 ); + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + sqlAppender.appendSql( '(' ); + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + renderArgument( sqlAppender, arg, nullBehavior, translator ); + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); } - 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 153288b19b..9a37181f24 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 class PostgreSQLJsonArrayFunction extends JsonArrayFunction { 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 0000000000..df73b06ff8 --- /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 90b4c87790..3bd07316f3 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 class PostgreSQLJsonObjectFunction extends JsonObjectFunction { 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 0000000000..ac06321cf2 --- /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 f4d956a2f3..3a21e7380c 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 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @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 f6cf4a0419..6b36b87ebe 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 class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde 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 ef985b4404..e68e776473 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.SqmFunction; import org.hibernate.query.sqm.tree.expression.SqmHqlNumericLiteral; import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonNullBehavior; +import org.hibernate.query.sqm.tree.expression.SqmJsonObjectAggUniqueKeysBehavior; import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmLiteral; @@ -2931,6 +2932,38 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implem ); } + @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 97e121349b..262dd33a58 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 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext { @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 7a462a01f9..a37a3940b8 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 @@ -122,6 +122,7 @@ import org.hibernate.query.sqm.tree.expression.SqmFormat; import org.hibernate.query.sqm.tree.expression.SqmFunction; import org.hibernate.query.sqm.tree.expression.SqmJsonExistsExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonNullBehavior; +import org.hibernate.query.sqm.tree.expression.SqmJsonObjectAggUniqueKeysBehavior; import org.hibernate.query.sqm.tree.expression.SqmJsonQueryExpression; import org.hibernate.query.sqm.tree.expression.SqmJsonValueExpression; import org.hibernate.query.sqm.tree.expression.SqmLiteral; @@ -5433,6 +5434,72 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { 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 0000000000..7cfd8e3b32 --- /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 0000000000..6d2c32eaaf --- /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 0000000000..5f9c493924 --- /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 44fd74bc01..2d110961d9 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.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.UUID; +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.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.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; @DomainModel( annotatedClasses = { JsonFunctionTests.JsonHolder.class, @@ -89,9 +100,12 @@ public class JsonFunctionTests { 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 class JsonFunctionTests { 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 class JsonFunctionTests { ); } + @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 6e39b3df28..39fae56b04 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 @@ abstract public class DialectFeatureChecks { } } + 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;