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 40f67107c5..0e2ffd6228 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1639,6 +1639,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_query()` | Queries non-scalar values by JSON path in a JSON document | `json_arrayagg()` | Creates a JSON array by aggregating values | `json_objectagg()` | Creates a JSON object by aggregating values +| `json_set()` | Inserts/Replaces a value by JSON path within a JSON document |=== @@ -2013,6 +2014,22 @@ include::{json-example-dir-hql}/JsonObjectAggregateTest.java[tags=hql-json-objec WARNING: Some databases like e.g. MySQL, SAP HANA, DB2 and SQL Server do not support raising an error on duplicate keys. +[[hql-json-set-function]] +===== `json_set()` + +Inserts/Replaces a value by JSON path within a JSON document. +The function takes 3 arguments, the json document, the json path and the new value to set/insert. + +[[hql-json-set-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonSetTest.java[tags=hql-json-set-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 35ceee43dd..8f4704251d 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -508,6 +508,7 @@ public class CockroachLegacyDialect extends Dialect { functionFactory.jsonArray_postgresql(); functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); + functionFactory.jsonSet_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index d60f4075a1..2e8b1b3b28 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -660,6 +660,7 @@ public class MySQLLegacyDialect extends Dialect { functionFactory.jsonArray_mysql(); functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); + functionFactory.jsonSet_mysql(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index cc9fda66d4..c1479523f1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -316,6 +316,7 @@ public class OracleLegacyDialect extends Dialect { functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonSet_oracle(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 7503b70eb9..3d192349b4 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 @@ -658,6 +658,7 @@ public class PostgreSQLLegacyDialect extends Dialect { functionFactory.jsonObjectAgg_postgresql( false ); } } + functionFactory.jsonSet_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index 5d2ab5fd3a..5c2d74b55b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -406,6 +406,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); + functionFactory.jsonSet_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index dc1edf76b3..b18965483c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -475,6 +475,7 @@ public class CockroachDialect extends Dialect { functionFactory.jsonArray_postgresql(); functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); + functionFactory.jsonSet_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 590a42bbaf..d8f933408f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -645,6 +645,7 @@ public class MySQLDialect extends Dialect { functionFactory.jsonArray_mysql(); functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); + functionFactory.jsonSet_mysql(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 623d20fc55..6ce60a665b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -407,6 +407,7 @@ public class OracleDialect extends Dialect { functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonSet_oracle(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index b2014b1949..9aef3b5aa2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -619,6 +619,7 @@ public class PostgreSQLDialect extends Dialect { functionFactory.jsonObjectAgg_postgresql( false ); } } + functionFactory.jsonSet_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 4bf64348b5..2fb7e72c73 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -424,6 +424,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { functionFactory.jsonExists_sqlserver(); functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); + functionFactory.jsonSet_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 6c019dcc84..857243b6e9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -118,12 +118,14 @@ import org.hibernate.dialect.function.json.MySQLJsonValueFunction; import org.hibernate.dialect.function.json.OracleJsonArrayAggFunction; import org.hibernate.dialect.function.json.OracleJsonArrayFunction; import org.hibernate.dialect.function.json.OracleJsonObjectFunction; +import org.hibernate.dialect.function.json.OracleJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonExistsFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction; +import org.hibernate.dialect.function.json.PostgreSQLJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayFunction; @@ -131,15 +133,18 @@ import org.hibernate.dialect.function.json.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.SQLServerJsonSetFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.type.BasicType; import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.type.SqlTypes; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -3796,4 +3801,42 @@ public class CommonFunctionFactory { public void jsonObjectAgg_db2() { functionRegistry.register( "json_objectagg", new DB2JsonObjectAggFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_set() function + */ + public void jsonSet_postgresql() { + functionRegistry.register( "json_set", new PostgreSQLJsonSetFunction( typeConfiguration ) ); + } + + /** + * MySQL json_set() function + */ + public void jsonSet_mysql() { + functionRegistry.namedDescriptorBuilder( "json_set" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_set() function + */ + public void jsonSet_oracle() { + functionRegistry.register( "json_set", new OracleJsonSetFunction( typeConfiguration ) ); + } + + /** + * SQL Server json_set() function + */ + public void jsonSet_sqlserver() { + functionRegistry.register( "json_set", new SQLServerJsonSetFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java new file mode 100644 index 0000000000..b03274d188 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonSetFunction.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_set function. + */ +public abstract class AbstractJsonSetFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonSetFunction(TypeConfiguration typeConfiguration) { + super( + "json_set", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 3 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING, + FunctionParameterType.ANY + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java new file mode 100644 index 0000000000..60e906bcbe --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonSetFunction.java @@ -0,0 +1,44 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_set function. + */ +public class OracleJsonSetFunction extends AbstractJsonSetFunction { + + public OracleJsonSetFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",set " ); + jsonPath.accept( translator ); + sqlAppender.appendSql( '=' ); + value.accept( translator ); + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java new file mode 100644 index 0000000000..33e952a72f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonSetFunction.java @@ -0,0 +1,90 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_set function. + */ +public class PostgreSQLJsonSetFunction extends AbstractJsonSetFunction { + + public PostgreSQLJsonSetFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "jsonb_set(" ); + final boolean needsCast = !isJsonType( json ) && json instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[]," ); + if ( value instanceof Literal && ( (Literal) value ).getLiteralValue() == null ) { + sqlAppender.appendSql( "null::jsonb" ); + } + else { + sqlAppender.appendSql( "to_jsonb(" ); + value.accept( translator ); + if ( value instanceof Literal literal && literal.getJdbcMapping().getJdbcType().isString() ) { + // PostgreSQL until version 16 is not smart enough to infer the type of a string literal + sqlAppender.appendSql( "::text" ); + } + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( ",true)" ); + } + + private boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java new file mode 100644 index 0000000000..b9660ca2ce --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonSetFunction.java @@ -0,0 +1,44 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_set function. + */ +public class SQLServerJsonSetFunction extends AbstractJsonSetFunction { + + public SQLServerJsonSetFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_modify(" ); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ',' ); + value.accept( translator ); + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index 3a21e7380c..c3229e7c7c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -3911,6 +3911,38 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Incubating JpaExpression jsonObjectAggWithUniqueKeysAndNulls(Expression key, Expression value, Predicate filter); + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value); + + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value); + + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Object value); + + /** + * Inserts/Replaces a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value); + @Override JpaPredicate and(List restrictions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index 6b36b87ebe..c4a024e4e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -3523,4 +3523,28 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde Predicate filter) { return criteriaBuilder.jsonObjectAggWithUniqueKeysAndNulls( key, value, filter ); } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, String jsonPath, Object value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } + + @Override + @Incubating + public JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value) { + return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 262dd33a58..02392cc95a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -703,6 +703,18 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext { @Override SqmExpression jsonObjectAgg(Expression key, Expression value, Predicate filter); + @Override + SqmExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value); + + @Override + SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Object value); + + @Override + SqmExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value); + + @Override + SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index a37a3940b8..4e1dea58d4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -5555,4 +5555,29 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { } return list; } + + @Override + public SqmExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value) { + return jsonSet( jsonDocument, jsonPath, value( value ) ); + } + + @Override + public SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Object value) { + return jsonSet( jsonDocument, value( jsonPath ), value( value ) ); + } + + @Override + public SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value) { + return jsonSet( jsonDocument, value( jsonPath ), value ); + } + + @Override + public SqmExpression jsonSet(Expression jsonDocument, Expression jsonPath, Expression value) { + //noinspection unchecked + return getFunctionDescriptor( "json_set" ).generateSqmExpression( + (List>) (List) asList( jsonDocument, jsonPath, value ), + null, + queryEngine + ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java new file mode 100644 index 0000000000..dd5dc00d9a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonSetTest.java @@ -0,0 +1,46 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import org.hibernate.cfg.QuerySettings; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.HANADialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonSet.class) +public class JsonSetTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-set-example[] + em.createQuery( "select json_set('{\"a\":1}', '$.a', 2)" ).getResultList(); + //end::hql-json-set-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 3b904a1f25..2dce0a2d0e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -466,6 +466,38 @@ public class JsonFunctionTests { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonSet.class) + public void testJsonSet(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_set('{}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 123, object.get( "a" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonSet.class) + public void testJsonSetReplace(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_set('{\"a\":456}', '$.a', 123)", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 123, object.get( "a" ) ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 39fae56b04..00722a2d62 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -793,6 +793,12 @@ abstract public class DialectFeatureChecks { } } + public static class SupportsJsonSet implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_set" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;