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 0e2ffd6228..740de80621 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -1640,6 +1640,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi | `json_arrayagg()` | Creates a JSON array by aggregating values | `json_objectagg()` | Creates a JSON object by aggregating values | `json_set()` | Inserts/Replaces a value by JSON path within a JSON document +| `json_remove()` | Removes a value by JSON path within a JSON document |=== @@ -2030,6 +2031,22 @@ include::{json-example-dir-hql}/JsonSetTest.java[tags=hql-json-set-example] WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. +[[hql-json-remove-function]] +===== `json_remove()` + +Removes a value by JSON path within a JSON document. +The function takes 2 arguments, the json document and the json path representing what to remove. + +[[hql-json-remove-example]] +==== +[source, java, indent=0] +---- +include::{json-example-dir-hql}/JsonRemoveTest.java[tags=hql-json-remove-example] +---- +==== + +WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function. + [[hql-user-defined-functions]] ==== Native and user-defined functions diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index 8f4704251d..eadb73c094 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java @@ -509,6 +509,7 @@ public class CockroachLegacyDialect extends Dialect { functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 2e8b1b3b28..f44ae5ea0a 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java @@ -661,6 +661,7 @@ public class MySQLLegacyDialect extends Dialect { functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); functionFactory.jsonSet_mysql(); + functionFactory.jsonRemove_mysql(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index c1479523f1..01da356bbf 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java @@ -316,7 +316,9 @@ public class OracleLegacyDialect extends Dialect { functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonObjectAgg_oracle(); functionFactory.jsonSet_oracle(); + functionFactory.jsonRemove_oracle(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 3d192349b4..7fe2ef6f0e 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 @@ -659,6 +659,7 @@ public class PostgreSQLLegacyDialect extends Dialect { } } functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); if ( getVersion().isSameOrAfter( 9, 4 ) ) { functionFactory.makeDateTimeTimestamp(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java index 5c2d74b55b..5a55001a99 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SQLServerLegacyDialect.java @@ -407,6 +407,7 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); functionFactory.jsonSet_sqlserver(); + functionFactory.jsonRemove_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index b18965483c..28a338587f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -476,6 +476,7 @@ public class CockroachDialect extends Dialect { functionFactory.jsonArrayAgg_postgresql( false ); functionFactory.jsonObjectAgg_postgresql( false ); functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); // Postgres uses # instead of ^ for XOR functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" ) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index d8f933408f..8c087eb13c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -646,6 +646,7 @@ public class MySQLDialect extends Dialect { functionFactory.jsonArrayAgg_mysql(); functionFactory.jsonObjectAgg_mysql(); functionFactory.jsonSet_mysql(); + functionFactory.jsonRemove_mysql(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 6ce60a665b..cf3a270471 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -407,7 +407,9 @@ public class OracleDialect extends Dialect { functionFactory.jsonObject_oracle(); functionFactory.jsonArray_oracle(); functionFactory.jsonArrayAgg_oracle(); + functionFactory.jsonObjectAgg_oracle(); functionFactory.jsonSet_oracle(); + functionFactory.jsonRemove_oracle(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 9aef3b5aa2..ade07c1e6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -620,6 +620,7 @@ public class PostgreSQLDialect extends Dialect { } } functionFactory.jsonSet_postgresql(); + functionFactory.jsonRemove_postgresql(); functionFactory.makeDateTimeTimestamp(); // Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 2fb7e72c73..d2177d0aec 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -425,6 +425,7 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { functionFactory.jsonObject_sqlserver(); functionFactory.jsonArray_sqlserver(); functionFactory.jsonSet_sqlserver(); + functionFactory.jsonRemove_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 857243b6e9..6d87238987 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -117,7 +117,9 @@ import org.hibernate.dialect.function.json.MySQLJsonQueryFunction; import org.hibernate.dialect.function.json.MySQLJsonValueFunction; import org.hibernate.dialect.function.json.OracleJsonArrayAggFunction; import org.hibernate.dialect.function.json.OracleJsonArrayFunction; +import org.hibernate.dialect.function.json.OracleJsonObjectAggFunction; import org.hibernate.dialect.function.json.OracleJsonObjectFunction; +import org.hibernate.dialect.function.json.OracleJsonRemoveFunction; import org.hibernate.dialect.function.json.OracleJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayAggFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonArrayFunction; @@ -125,6 +127,7 @@ import org.hibernate.dialect.function.json.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.PostgreSQLJsonRemoveFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonSetFunction; import org.hibernate.dialect.function.json.PostgreSQLJsonValueFunction; import org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction; @@ -133,6 +136,7 @@ import org.hibernate.dialect.function.json.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.SQLServerJsonRemoveFunction; import org.hibernate.dialect.function.json.SQLServerJsonSetFunction; import org.hibernate.dialect.function.json.SQLServerJsonValueFunction; import org.hibernate.query.sqm.function.SqmFunctionRegistry; @@ -3753,6 +3757,13 @@ public class CommonFunctionFactory { functionRegistry.register( "json_arrayagg", new HANAJsonArrayAggFunction( typeConfiguration ) ); } + /** + * Oracle json_objectagg() function + */ + public void jsonObjectAgg_oracle() { + functionRegistry.register( "json_objectagg", new OracleJsonObjectAggFunction( typeConfiguration ) ); + } + /** * json_objectagg() function for H2 and HSQLDB */ @@ -3839,4 +3850,41 @@ public class CommonFunctionFactory { public void jsonSet_sqlserver() { functionRegistry.register( "json_set", new SQLServerJsonSetFunction( typeConfiguration ) ); } + + /** + * PostgreSQL json_remove() function + */ + public void jsonRemove_postgresql() { + functionRegistry.register( "json_remove", new PostgreSQLJsonRemoveFunction( typeConfiguration ) ); + } + + /** + * MySQL json_remove() function + */ + public void jsonRemove_mysql() { + functionRegistry.namedDescriptorBuilder( "json_remove" ) + .setArgumentsValidator( new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 2 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING + ) ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ) ) + .register(); + } + + /** + * Oracle json_remove() function + */ + public void jsonRemove_oracle() { + functionRegistry.register( "json_remove", new OracleJsonRemoveFunction( typeConfiguration ) ); + } + + /** + * SQL server json_remove() function + */ + public void jsonRemove_sqlserver() { + functionRegistry.register( "json_remove", new SQLServerJsonRemoveFunction( typeConfiguration ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java new file mode 100644 index 0000000000..c42ef85ac9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/AbstractJsonRemoveFunction.java @@ -0,0 +1,39 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.ArgumentTypesValidator; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Standard json_remove function. + */ +public abstract class AbstractJsonRemoveFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public AbstractJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( + "json_remove", + FunctionKind.NORMAL, + new ArgumentTypesValidator( + StandardArgumentsValidators.exactly( 2 ), + FunctionParameterType.IMPLICIT_JSON, + FunctionParameterType.STRING + ), + StandardFunctionReturnTypeResolvers.invariant( + typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON ) + ), + null + ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java index 8926ff0ad7..66b7f8c0e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayAggFunction.java @@ -6,9 +6,14 @@ */ package org.hibernate.dialect.function.json; +import java.util.List; + +import org.hibernate.dialect.function.CastFunction; import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.CastTarget; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; import org.hibernate.type.SqlTypes; @@ -19,8 +24,12 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class OracleJsonArrayAggFunction extends JsonArrayAggFunction { + private final CastTarget stringCastTarget; + private CastFunction castFunction; + public OracleJsonArrayAggFunction(TypeConfiguration typeConfiguration) { super( false, typeConfiguration ); + this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); } @Override @@ -29,11 +38,29 @@ public class OracleJsonArrayAggFunction extends JsonArrayAggFunction { Expression arg, JsonNullBehavior nullBehavior, SqlAstTranslator translator) { - arg.accept( translator ); - final JdbcMappingContainer expressionType = arg.getExpressionType(); - if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() - && !SqlTypes.isJsonType( expressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() ) ) { + if ( ExpressionTypeHelper.isNonNativeBoolean( arg ) ) { + CastFunction castFunction = this.castFunction; + if ( castFunction == null ) { + castFunction = this.castFunction = (CastFunction) translator.getSessionFactory() + .getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( "cast" ); + } + castFunction.render( + sqlAppender, + List.of( arg, stringCastTarget ), + (ReturnableType) stringCastTarget.getJdbcMapping(), + translator + ); sqlAppender.appendSql( " format json" ); } + else { + arg.accept( translator ); + final JdbcMappingContainer expressionType = arg.getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() + && !SqlTypes.isJsonType( expressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() ) ) { + sqlAppender.appendSql( " format json" ); + } + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java index 3a56bc7187..2d182789d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonArrayFunction.java @@ -9,11 +9,14 @@ package org.hibernate.dialect.function.json; import java.util.List; import org.hibernate.dialect.function.CastFunction; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -49,6 +52,11 @@ public class OracleJsonArrayFunction extends JsonArrayFunction { } else { value.accept( walker ); + final JdbcMappingContainer expressionType = ( (Expression) value ).getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() + && !SqlTypes.isJsonType( expressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() ) ) { + sqlAppender.appendSql( " format json" ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java new file mode 100644 index 0000000000..fa58ed52ea --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectAggFunction.java @@ -0,0 +1,66 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.dialect.function.CastFunction; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_objectagg function. + */ +public class OracleJsonObjectAggFunction extends JsonObjectAggFunction { + + private final CastTarget stringCastTarget; + private CastFunction castFunction; + + public OracleJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( " value ", false, typeConfiguration ); + this.stringCastTarget = new CastTarget( typeConfiguration.getBasicTypeForJavaType( String.class ) ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, + Expression arg, + JsonNullBehavior nullBehavior, + SqlAstTranslator translator) { + if ( ExpressionTypeHelper.isNonNativeBoolean( arg ) ) { + CastFunction castFunction = this.castFunction; + if ( castFunction == null ) { + castFunction = this.castFunction = (CastFunction) translator.getSessionFactory() + .getQueryEngine() + .getSqmFunctionRegistry() + .findFunctionDescriptor( "cast" ); + } + castFunction.render( + sqlAppender, + List.of( arg, stringCastTarget ), + (ReturnableType) stringCastTarget.getJdbcMapping(), + translator + ); + sqlAppender.appendSql( " format json" ); + } + else { + arg.accept( translator ); + final JdbcMappingContainer expressionType = arg.getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() + && !SqlTypes.isJsonType( expressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() ) ) { + sqlAppender.appendSql( " format json" ); + } + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java index 049df279c4..eaad48b202 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonObjectFunction.java @@ -9,11 +9,14 @@ package org.hibernate.dialect.function.json; import java.util.List; import org.hibernate.dialect.function.CastFunction; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.spi.SqlAppender; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -49,6 +52,11 @@ public class OracleJsonObjectFunction extends JsonObjectFunction { } else { value.accept( walker ); + final JdbcMappingContainer expressionType = ( (Expression) value ).getExpressionType(); + if ( expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson() + && !SqlTypes.isJsonType( expressionType.getSingleJdbcMapping().getJdbcType().getDdlTypeCode() ) ) { + sqlAppender.appendSql( " format json" ); + } } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java new file mode 100644 index 0000000000..5e4e5d1fdc --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/OracleJsonRemoveFunction.java @@ -0,0 +1,41 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * Oracle json_remove function. + */ +public class OracleJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public OracleJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + sqlAppender.appendSql( "json_transform(" ); + json.accept( translator ); + sqlAppender.appendSql( ",remove " ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java new file mode 100644 index 0000000000..73229b6b05 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/PostgreSQLJsonRemoveFunction.java @@ -0,0 +1,76 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * PostgreSQL json_set function. + */ +public class PostgreSQLJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public PostgreSQLJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + sqlAppender.appendSql( "jsonb_set_lax(" ); + final boolean needsCast = !isJsonType( json ) && json instanceof JdbcParameter; + if ( needsCast ) { + sqlAppender.appendSql( "cast(" ); + } + json.accept( translator ); + if ( needsCast ) { + sqlAppender.appendSql( " as jsonb)" ); + } + sqlAppender.appendSql( ',' ); + List jsonPathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + sqlAppender.appendSql( "array" ); + char separator = '['; + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( separator ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + separator = ','; + } + sqlAppender.appendSql( "]::text[],null,true,'delete_key')" ); + } + + private boolean isJsonType(Expression expression) { + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java new file mode 100644 index 0000000000..0936a24bfd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonRemoveFunction.java @@ -0,0 +1,41 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.dialect.function.json; + +import java.util.List; + +import org.hibernate.query.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SQL Server json_remove function. + */ +public class SQLServerJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public SQLServerJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + sqlAppender.appendSql( "json_modify(" ); + json.accept( translator ); + sqlAppender.appendSql( ',' ); + jsonPath.accept( translator ); + sqlAppender.appendSql( ",null)" ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index c3229e7c7c..30136487f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -3943,6 +3943,22 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Incubating JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value); + /** + * Removes a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonRemove(Expression jsonDocument, String jsonPath); + + /** + * Removes a value by JSON path within a JSON document. + * + * @since 7.0 + */ + @Incubating + JpaExpression jsonRemove(Expression jsonDocument, Expression jsonPath); + @Override JpaPredicate and(List restrictions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java index c4a024e4e1..d8088f26db 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/spi/HibernateCriteriaBuilderDelegate.java @@ -3547,4 +3547,16 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde public JpaExpression jsonSet(Expression jsonDocument, Expression jsonPath, Object value) { return criteriaBuilder.jsonSet( jsonDocument, jsonPath, value ); } + + @Override + @Incubating + public JpaExpression jsonRemove(Expression jsonDocument, String jsonPath) { + return criteriaBuilder.jsonRemove( jsonDocument, jsonPath ); + } + + @Override + @Incubating + public JpaExpression jsonRemove(Expression jsonDocument, Expression jsonPath) { + return criteriaBuilder.jsonRemove( jsonDocument, jsonPath ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index 02392cc95a..a58783d7ef 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -715,6 +715,12 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext { @Override SqmExpression jsonSet(Expression jsonDocument, String jsonPath, Expression value); + @Override + SqmExpression jsonRemove(Expression jsonDocument, String jsonPath); + + @Override + SqmExpression jsonRemove(Expression jsonDocument, Expression jsonPath); + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Covariant overrides diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 4e1dea58d4..3a2136f9c6 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 @@ -5580,4 +5580,19 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable { queryEngine ); } + + @Override + public SqmExpression jsonRemove(Expression jsonDocument, String jsonPath) { + return jsonRemove( jsonDocument, value( jsonPath ) ); + } + + @Override + public SqmExpression jsonRemove(Expression jsonDocument, Expression jsonPath) { + //noinspection unchecked + return getFunctionDescriptor( "json_remove" ).generateSqmExpression( + (List>) (List) asList( jsonDocument, jsonPath ), + null, + queryEngine + ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java new file mode 100644 index 0000000000..590651c589 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/json/JsonRemoveTest.java @@ -0,0 +1,39 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.function.json; + +import org.hibernate.cfg.QuerySettings; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +/** + * @author Christian Beikov + */ +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.JSON_FUNCTIONS_ENABLED, value = "true")) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsJsonRemove.class) +public class JsonRemoveTest { + + @Test + public void testSimple(SessionFactoryScope scope) { + scope.inSession( em -> { + //tag::hql-json-remove-example[] + em.createQuery( "select json_remove('{\"a\":1,\"b\":2}', '$.a')" ).getResultList(); + //end::hql-json-remove-example[] + } ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java index 2dce0a2d0e..6d07bc3e17 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/JsonFunctionTests.java @@ -498,6 +498,52 @@ public class JsonFunctionTests { ); } + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonRemove.class) + public void testJsonRemove(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_remove('{\"a\":123,\"b\":456}', '$.a')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 1, object.size() ); + assertEquals( 456, object.get( "b" ) ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonRemove.class) + public void testJsonRemoveToEmpty(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_remove('{\"a\":123}', '$.a')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 0, object.size() ); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonRemove.class) + public void testJsonRemoveNonExisting(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String json = session.createQuery( + "select json_remove('{}', '$.a')", + String.class + ).getSingleResult(); + Map object = parseObject( json ); + assertEquals( 0, object.size() ); + } + ); + } + private static final ObjectMapper MAPPER = new ObjectMapper(); private static Map parseObject(String json) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index 00722a2d62..1a1b9d6048 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -799,6 +799,12 @@ abstract public class DialectFeatureChecks { } } + public static class SupportsJsonRemove implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return definesFunction( dialect, "json_remove" ); + } + } + public static class IsJtds implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;