HHH-18496 Add json_objectagg

This commit is contained in:
Christian Beikov 2024-09-11 20:01:49 +02:00
parent c58485c4ef
commit 59ae75bb52
47 changed files with 1608 additions and 72 deletions

View File

@ -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

View File

@ -0,0 +1,9 @@
"json_objectagg(" expressionOrPredicate ("value"|":") expressionOrPredicate jsonNullClause? uniqueKeysClause? ")" filterClause?
jsonNullClause
: ("absent"|"null") "on null"
;
uniqueKeysClause
: ("with"|"without") "unique keys"
;

View File

@ -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)" )

View File

@ -437,6 +437,7 @@ public class DB2LegacyDialect extends Dialect {
functionFactory.jsonObject_db2();
functionFactory.jsonArray_db2();
functionFactory.jsonArrayAgg_db2();
functionFactory.jsonObjectAgg_db2();
}
}
}

View File

@ -407,6 +407,7 @@ public class H2LegacyDialect extends Dialect {
functionFactory.jsonQuery_h2();
functionFactory.jsonExists_h2();
functionFactory.jsonArrayAgg_h2();
functionFactory.jsonObjectAgg_h2();
}
}
else {

View File

@ -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

View File

@ -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 ()" )

View File

@ -659,6 +659,7 @@ public class MySQLLegacyDialect extends Dialect {
functionFactory.jsonObject_mysql();
functionFactory.jsonArray_mysql();
functionFactory.jsonArrayAgg_mysql();
functionFactory.jsonObjectAgg_mysql();
}
}

View File

@ -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 );
}
}

View File

@ -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();

View File

@ -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];

View File

@ -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

View File

@ -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)" )

View File

@ -423,6 +423,7 @@ public class DB2Dialect extends Dialect {
functionFactory.jsonObject_db2();
functionFactory.jsonArray_db2();
functionFactory.jsonArrayAgg_db2();
functionFactory.jsonObjectAgg_db2();
}
}

View File

@ -350,6 +350,7 @@ public class H2Dialect extends Dialect {
functionFactory.jsonQuery_h2();
functionFactory.jsonExists_h2();
functionFactory.jsonArrayAgg_h2();
functionFactory.jsonObjectAgg_h2();
}
}

View File

@ -499,6 +499,7 @@ public class HANADialect extends Dialect {
functionFactory.jsonObject_hana();
functionFactory.jsonArray_hana();
functionFactory.jsonArrayAgg_hana();
functionFactory.jsonObjectAgg_hana();
}
}
}

View File

@ -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

View File

@ -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 ) )

View File

@ -644,6 +644,7 @@ public class MySQLDialect extends Dialect {
functionFactory.jsonObject_mysql();
functionFactory.jsonArray_mysql();
functionFactory.jsonArrayAgg_mysql();
functionFactory.jsonObjectAgg_mysql();
}
@Override

View File

@ -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 );
}
}

View File

@ -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();

View File

@ -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 ) );
}
}

View File

@ -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<JsonPathHelper.JsonPathElement> 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" );
}
}

View File

@ -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<JsonPathHelper.JsonPathElement> 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( ")" );
}
}
}

View File

@ -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')" );
}
}
}

View File

@ -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
}
}

View File

@ -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')" );
}
}
}
}

View File

@ -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<? extends SqlAstNode> sqlAstArguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> walker) {
render( sqlAppender, sqlAstArguments, null, returnType, walker );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> 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<? extends SqlAstNode> 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
);
}
}
}

View File

@ -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')" );
}
}
}

View File

@ -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')" );
}
}
}

View File

@ -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))" );
}
}
}

View File

@ -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))" );
}
}
}

View File

@ -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,7 +42,10 @@ public class PostgreSQLJsonArrayAggFunction extends JsonArrayAggFunction {
List<SortSpecification> withinGroup,
ReturnableType<?> returnType,
SqlAstTranslator<?> translator) {
final boolean caseWrapper = filter != null && !supportsFilter;
if ( supportsStandard ) {
super.render( sqlAppender, sqlAstArguments, filter, withinGroup, returnType, translator );
}
else {
final String jsonTypeName = translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry()
.getTypeName( SqlTypes.JSON, translator.getSessionFactory().getJdbcServices().getDialect() );
sqlAppender.appendSql( jsonTypeName );
@ -51,9 +57,6 @@ public class PostgreSQLJsonArrayAggFunction extends JsonArrayAggFunction {
else {
nullBehavior = JsonNullBehavior.ABSENT;
}
if ( nullBehavior != JsonNullBehavior.NULL ) {
sqlAppender.appendSql( "_strict" );
}
sqlAppender.appendSql( '(' );
final SqlAstNode firstArg = sqlAstArguments.get( 0 );
final Expression arg;
@ -64,21 +67,7 @@ public class PostgreSQLJsonArrayAggFunction extends JsonArrayAggFunction {
else {
arg = (Expression) firstArg;
}
if ( caseWrapper ) {
if ( nullBehavior != JsonNullBehavior.ABSENT ) {
throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." );
}
translator.getCurrentClauseStack().push( Clause.WHERE );
sqlAppender.appendSql( "case when " );
filter.accept( translator );
translator.getCurrentClauseStack().pop();
sqlAppender.appendSql( " then " );
renderArgument( sqlAppender, arg, nullBehavior, translator );
sqlAppender.appendSql( " else null end)" );
}
else {
renderArgument( sqlAppender, arg, nullBehavior, translator );
}
if ( withinGroup != null && !withinGroup.isEmpty() ) {
translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP );
sqlAppender.appendSql( " order by " );
@ -91,12 +80,23 @@ public class PostgreSQLJsonArrayAggFunction extends JsonArrayAggFunction {
}
sqlAppender.appendSql( ')' );
if ( !caseWrapper && filter != null ) {
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)" );
}
}
}
}

View File

@ -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( ')' );

View File

@ -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 );
}
}

View File

@ -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( ')' );

View File

@ -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')" );
}
}
}

View File

@ -3847,6 +3847,70 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
@Incubating
JpaExpression<String> jsonArrayAggWithNulls(Expression<?> value, Predicate filter, JpaOrder... orderBy);
/**
* Aggregates the given value under the given key into a JSON object.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> 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<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value);
/**
* Aggregates the given value under the given key into a JSON object.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> 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<String> jsonObjectAggWithUniqueKeysAndNulls(Expression<?> key, Expression<?> value);
/**
* Aggregates the given value under the given key into a JSON object.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> 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<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value, Predicate filter);
/**
* Aggregates the given value under the given key into a JSON object.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> 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<String> jsonObjectAggWithUniqueKeysAndNulls(Expression<?> key, Expression<?> value, Predicate filter);
@Override
JpaPredicate and(List<Predicate> restrictions);

View File

@ -3472,4 +3472,55 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
public JpaExpression<String> jsonArrayAggWithNulls(Expression<?> value, Predicate filter, JpaOrder... orderBy) {
return criteriaBuilder.jsonArrayAggWithNulls( value, filter, orderBy );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAgg(Expression<?> key, Expression<?> value) {
return criteriaBuilder.jsonObjectAgg( key, value );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value) {
return criteriaBuilder.jsonObjectAggWithNulls( key, value );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAggWithUniqueKeys(Expression<?> key, Expression<?> value) {
return criteriaBuilder.jsonObjectAggWithUniqueKeys( key, value );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAggWithUniqueKeysAndNulls(Expression<?> key, Expression<?> value) {
return criteriaBuilder.jsonObjectAggWithUniqueKeysAndNulls( key, value );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAgg(Expression<?> key, Expression<?> value, Predicate filter) {
return criteriaBuilder.jsonObjectAgg( key, value, filter );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value, Predicate filter) {
return criteriaBuilder.jsonObjectAggWithNulls( key, value, filter );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAggWithUniqueKeys(Expression<?> key, Expression<?> value, Predicate filter) {
return criteriaBuilder.jsonObjectAggWithUniqueKeys( key, value, filter );
}
@Override
@Incubating
public JpaExpression<String> jsonObjectAggWithUniqueKeysAndNulls(
Expression<?> key,
Expression<?> value,
Predicate filter) {
return criteriaBuilder.jsonObjectAggWithUniqueKeysAndNulls( key, value, filter );
}
}

View File

@ -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<R> extends HqlParserBaseVisitor<Object> implem
);
}
@Override
public Object visitJsonObjectAggFunction(HqlParser.JsonObjectAggFunctionContext ctx) {
final HqlParser.JsonNullClauseContext jsonNullClauseContext = ctx.jsonNullClause();
final HqlParser.JsonUniqueKeysClauseContext jsonUniqueKeysClauseContext = ctx.jsonUniqueKeysClause();
final ArrayList<SqmTypedNode<?>> 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;

View File

@ -679,6 +679,30 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext {
@Override
SqmExpression<String> jsonArrayAgg(Expression<?> value, JpaOrder... orderBy);
@Override
SqmExpression<String> jsonObjectAggWithUniqueKeysAndNulls(Expression<?> key, Expression<?> value);
@Override
SqmExpression<String> jsonObjectAggWithUniqueKeys(Expression<?> key, Expression<?> value);
@Override
SqmExpression<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value);
@Override
SqmExpression<String> jsonObjectAgg(Expression<?> key, Expression<?> value);
@Override
SqmExpression<String> jsonObjectAggWithUniqueKeysAndNulls(Expression<?> key, Expression<?> value, Predicate filter);
@Override
SqmExpression<String> jsonObjectAggWithUniqueKeys(Expression<?> key, Expression<?> value, Predicate filter);
@Override
SqmExpression<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value, Predicate filter);
@Override
SqmExpression<String> jsonObjectAgg(Expression<?> key, Expression<?> value, Predicate filter);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Covariant overrides

View File

@ -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<String> jsonObjectAggWithUniqueKeysAndNulls(Expression<?> key, Expression<?> value) {
return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, SqmJsonObjectAggUniqueKeysBehavior.WITH, null );
}
@Override
public SqmExpression<String> jsonObjectAggWithUniqueKeys(Expression<?> key, Expression<?> value) {
return jsonObjectAgg( key, value, null, SqmJsonObjectAggUniqueKeysBehavior.WITH, null );
}
@Override
public SqmExpression<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value) {
return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, null, null );
}
@Override
public SqmExpression<String> jsonObjectAgg(Expression<?> key, Expression<?> value) {
return jsonObjectAgg( key, value, null, null, null );
}
@Override
public SqmExpression<String> jsonObjectAggWithUniqueKeysAndNulls(
Expression<?> key,
Expression<?> value,
Predicate filter) {
return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, SqmJsonObjectAggUniqueKeysBehavior.WITH, filter );
}
@Override
public SqmExpression<String> jsonObjectAggWithUniqueKeys(Expression<?> key, Expression<?> value, Predicate filter) {
return jsonObjectAgg( key, value, null, SqmJsonObjectAggUniqueKeysBehavior.WITH, filter );
}
@Override
public SqmExpression<String> jsonObjectAggWithNulls(Expression<?> key, Expression<?> value, Predicate filter) {
return jsonObjectAgg( key, value, SqmJsonNullBehavior.NULL, null, filter );
}
@Override
public SqmExpression<String> jsonObjectAgg(Expression<?> key, Expression<?> value, Predicate filter) {
return jsonObjectAgg( key, value, null, null, filter );
}
private SqmExpression<String> jsonObjectAgg(
Expression<?> key,
Expression<?> value,
@Nullable SqmJsonNullBehavior nullBehavior,
@Nullable SqmJsonObjectAggUniqueKeysBehavior uniqueKeysBehavior,
@Nullable Predicate filterPredicate) {
final ArrayList<SqmTypedNode<?>> 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;

View File

@ -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<Object> {
/**
* Aggregate only unique keys. Fail aggregation if a duplicate is encountered.
*/
WITH,
/**
* Aggregate duplicate keys without failing.
*/
WITHOUT;
@Override
public @Nullable SqmExpressible<Object> getNodeType() {
return null;
}
@Override
public NodeBuilder nodeBuilder() {
return null;
}
@Override
public SqmJsonObjectAggUniqueKeysBehavior copy(SqmCopyContext context) {
return this;
}
@Override
public <X> X accept(SemanticQueryWalker<X> 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" );
}
}
}

View File

@ -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");
}
}

View File

@ -0,0 +1,66 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.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[]
} );
}
}

View File

@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> parseObject(String json) {

View File

@ -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;