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 5615f1e611..455fb039bf 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1643,6 +1643,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_remove()` | Removes a value by JSON path within a JSON document | `json_mergepatch()` | Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge | `json_array_append()` | Appends to a JSON array of a JSON document by JSON path +| `json_array_insert()` | Inserts a value by JSON path to a JSON array within a JSON document |=== @@ -2134,6 +2135,29 @@ include::{json-example-dir-hql}/JsonArrayAppendTest.java[tags=hql-json-array-app WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-json-array-insert-function]] +===== `json_array_insert()` + +Inserts a value by JSON path to a JSON array within a JSON document. +The function takes 3 arguments, the json document, the json path and the value to append. + +Although the exact behavior is database dependent, usually an error will be triggered if +the JSON path does not end with an array index access i.e. `$.a[0]`. +The zero based array index represents the position at which an element should be inserted in an array. + +If the JSON path without the index does not resolve to a JSON array within the JSON document, +the document is not changed. + +[[hql-json-array-insert-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonArrayInsertTest.java[tags=hql-json-array-insert-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 25c0a0a52f..27919427a0 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -514,6 +514,7 @@ public class CockroachLegacyDialect extends Dialect { functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 73397f4427..0d679e7e25 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -666,6 +666,7 @@ public class MySQLLegacyDialect extends Dialect { functionFactory.jsonInsert_mysql(); functionFactory.jsonMergepatch_mysql(); functionFactory.jsonArrayAppend_mysql(); + functionFactory.jsonArrayInsert_mysql(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index b7cc8cc196..2e66da8197 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -323,6 +323,7 @@ public class OracleLegacyDialect extends Dialect { functionFactory.jsonInsert_oracle(); functionFactory.jsonMergepatch_oracle(); functionFactory.jsonArrayAppend_oracle(); + functionFactory.jsonArrayInsert_oracle(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 0daaa07ef7..291300bab7 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 @@ -664,6 +664,7 @@ public class PostgreSQLLegacyDialect extends Dialect { functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index 8bab388986..7a7512d24e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -411,6 +411,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { functionFactory.jsonReplace_sqlserver(); functionFactory.jsonInsert_sqlserver(); functionFactory.jsonArrayAppend_sqlserver(); + functionFactory.jsonArrayInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 6315eae670..93bc7b5eb0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -481,6 +481,7 @@ public class CockroachDialect extends Dialect { functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 363b081b30..06a3190bd4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -651,6 +651,7 @@ public class MySQLDialect extends Dialect { functionFactory.jsonInsert_mysql(); functionFactory.jsonMergepatch_mysql(); functionFactory.jsonArrayAppend_mysql(); + functionFactory.jsonArrayInsert_mysql(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 66fad9f5c3..5574e03d45 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -414,6 +414,7 @@ public class OracleDialect extends Dialect { functionFactory.jsonInsert_oracle(); functionFactory.jsonMergepatch_oracle(); functionFactory.jsonArrayAppend_oracle(); + functionFactory.jsonArrayInsert_oracle(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 2134c20ace..17b7d0b562 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -625,6 +625,7 @@ public class PostgreSQLDialect extends Dialect { functionFactory.jsonInsert_postgresql(); functionFactory.jsonMergepatch_postgresql(); functionFactory.jsonArrayAppend_postgresql(); + functionFactory.jsonArrayInsert_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index f4a353c763..a3810537e0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -429,6 +429,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { functionFactory.jsonReplace_sqlserver(); functionFactory.jsonInsert_sqlserver(); functionFactory.jsonArrayAppend_sqlserver(); + functionFactory.jsonArrayInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index c2859ddce2..a3d2e63062 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -118,6 +118,7 @@ import org.hibernate.dialect.function.json.MySQLJsonValueFunction; import org.hibernate.dialect.function.json.OracleJsonArrayAggFunction; import org.hibernate.dialect.function.json.OracleJsonArrayAppendFunction; import org.hibernate.dialect.function.json.OracleJsonArrayFunction; +import org.hibernate.dialect.function.json.OracleJsonArrayInsertFunction; import org.hibernate.dialect.function.json.OracleJsonInsertFunction; import org.hibernate.dialect.function.json.OracleJsonMergepatchFunction; import org.hibernate.dialect.function.json.OracleJsonObjectAggFunction; @@ -128,6 +129,7 @@ import org.hibernate.dialect.function.json.OracleJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAppendFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonArrayInsertFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonInsertFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonMergepatchFunction; @@ -141,6 +143,7 @@ import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAppendFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; +import org.hibernate.dialect.function.json.SQLServerJsonArrayInsertFunction; import org.hibernate.dialect.function.json.SQLServerJsonExistsFunction; import org.hibernate.dialect.function.json.SQLServerJsonInsertFunction; import org.hibernate.dialect.function.json.SQLServerJsonObjectAggFunction; @@ -4056,4 +4059,42 @@ public class CommonFunctionFactory { public void jsonArrayAppend_sqlserver() { functionRegistry.register( "json_array_append", new SQLServerJsonArrayAppendFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_array_insert() function + */ + public void jsonArrayInsert_postgresql() { + functionRegistry.register( "json_array_insert", new PostgreSQLJsonArrayInsertFunction( typeConfiguration ) ); + } + + /** + * MySQL json_array_insert() function + */ + public void jsonArrayInsert_mysql() { + functionRegistry.namedDescriptorBuilder( "json_array_insert" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_array_insert() function + */ + public void jsonArrayInsert_oracle() { + functionRegistry.register( "json_array_insert", new OracleJsonArrayInsertFunction( typeConfiguration ) ); + } + + /** + * SQL server json_array_insert() function + */ + public void jsonArrayInsert_sqlserver() { + functionRegistry.register( "json_array_insert", new SQLServerJsonArrayInsertFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayInsertFunction.java new file mode 100644 index 0000000000..8583f161a5 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonArrayInsertFunction.java @@ -0,0 +1,38 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_array_insert function. + */ +public abstract class AbstractJsonArrayInsertFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( + "json_array_insert", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayInsertFunction.java new file mode 100644 index 0000000000..dbb1045aef --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayInsertFunction.java @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_array_insert function. + */ +public class OracleJsonArrayInsertFunction extends AbstractJsonArrayInsertFunction { + + public OracleJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final String jsonPath = translator.getLiteralValue( (Expression) arguments.get( 1 ) ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",insert " ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath ); + sqlAppender.appendSql( '=' ); + value.accept( translator ); + sqlAppender.appendSql( " ignore on existing)" ); + + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayInsertFunction.java new file mode 100644 index 0000000000..1a5c987eb6 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonArrayInsertFunction.java @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_array_insert function. + */ +public class PostgreSQLJsonArrayInsertFunction extends AbstractJsonArrayInsertFunction { + + public PostgreSQLJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "jsonb_insert(" ); + final boolean needsCast = !isJsonType( json ); + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[]," ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( translator ); + if ( value instanceof Literal literal && literal.getJdbcMapping().getJdbcType().isString() ) { + // PostgreSQL until version 16 is not smart enough to infer the type of a string literal + sqlAppender.appendSql( "::text" ); + } + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( ')' ); + } + + private static boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java index d09d4934ad..3935f8a92b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonInsertFunction.java @@ -34,7 +34,7 @@ public class PostgreSQLJsonInsertFunction extends AbstractJsonInsertFunction { final Expression json = (Expression) arguments.get( 0 ); final Expression jsonPath = (Expression) arguments.get( 1 ); final SqlAstNode value = arguments.get( 2 ); - sqlAppender.appendSql( "(select case when t.d#>>t.p is not null then t.d else jsonb_insert(t.d,t.p," ); + sqlAppender.appendSql( "(select case when (t.d)#>>t.p is not null then t.d else jsonb_insert(t.d,t.p," ); if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { sqlAppender.appendSql( "null::jsonb" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java new file mode 100644 index 0000000000..24c9d45c5d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_array_insert function. + */ +public class SQLServerJsonArrayInsertFunction extends AbstractJsonArrayInsertFunction { + + public SQLServerJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final String jsonPath = translator.getLiteralValue( (Expression) arguments.get( 1 ) ).trim(); + final int bracketEndIndex = jsonPath.lastIndexOf( ']' ); + final int bracketStartIndex = jsonPath.lastIndexOf( '[' ); + if ( jsonPath.isEmpty() + || bracketEndIndex != jsonPath.length() - 1 + || bracketStartIndex == -1 ) { + throw new QueryException( "JSON path does not end with an array index: " + jsonPath ); + } + final int index; + try { + index = Integer.parseInt( jsonPath.substring( bracketStartIndex + 1, bracketEndIndex ) ); + } + catch ( NumberFormatException e ) { + throw new QueryException( "JSON path does not point to a valid array index: " + jsonPath ); + } + final Expression json = (Expression) arguments.get( 0 ); + final SqlAstNode value = arguments.get( 2 ); + // Only replace data if this is an array + sqlAppender.appendSql( "(select case when left(json_query(x.d,x.p),1)='[' then " ); + // Replace the array + sqlAppender.appendSql( "json_modify(x.d,x.p,json_query((" ); + // Aggregate a new JSON array based on element rows + sqlAppender.appendSql( "select '['+string_agg(t.v,',') within group (order by t.k)+']' from (" ); + + sqlAppender.appendSql( "select x.i k,x.v v union all " ); + sqlAppender.appendSql( "select case when cast(t.[key] as int)>=x.i then cast(t.[key] as int)+1 " ); + sqlAppender.appendSql( "else cast(t.[key] as int) end," ); + // type 0 is a null literal + sqlAppender.appendSql( "case t.type when 0 then 'null' when 1 then "); + // type 1 is a string literal. to quote it, we use for json path and trim the string down to just the value + sqlAppender.appendSql( + "(select substring(a.v,6,len(a.v)-6) from (select t.value a for json path,without_array_wrapper) a(v))" ); + sqlAppender.appendSql( " else t.value end from openjson(x.d,x.p) t) t))) " ); + sqlAppender.appendSql( " else x.d end " ); + // Push args into a values clause since we are going to refer to them multiple times + sqlAppender.appendSql( "from (values(" ); + json.accept( translator ); + sqlAppender.append( ',' ); + sqlAppender.appendSingleQuoteEscapedString( jsonPath.substring( 0, bracketStartIndex ) ); + sqlAppender.append( ',' ); + sqlAppender.appendSql( index ); + sqlAppender.append( ',' ); + value.accept( translator ); + sqlAppender.append( ")) x(d,p,i,v))" ); + } + + protected void renderArgument( + SqlAppender sqlAppender, + SqlAstNode arg, + SqlAstTranslator translator) { + sqlAppender.appendSql( "substring(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null),2,len(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null))-2)" ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayInsertTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayInsertTest.java new file mode 100644 index 0000000000..b1b97c9011 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonArrayInsertTest.java @@ -0,0 +1,37 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.function.json; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonInsert.class) +public class JsonArrayInsertTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-array-insert-example[] + em.createQuery( "select json_array_insert('{\"a\":[1]}', '$.a[0]', 2)" ).getResultList(); + //end::hql-json-array-insert-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 032e9526a9..cb5e2299da 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -704,6 +704,70 @@ public class JsonFunctionTests { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsert(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":[2]}', '$.b[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( Arrays.asList( 1, 2 ), object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsertNonExisting(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":[2]}', '$.c[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( List.of( 2 ), object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsertNonArray(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":2}', '$.b[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 2, object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonArrayInsert.class) + public void testJsonArrayInsertToNull(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_array_insert('{\"b\":null}', '$.b[0]', 1)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertNull( object.get( "b" ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 0052370de6..96e6c11a73 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -829,6 +829,12 @@ abstract public class DialectFeatureChecks { } } + public static class SupportsJsonArrayInsert implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_array_insert" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;