HHH-18604 Add json_mergepatch function

This commit is contained in:
Christian Beikov 2024-09-17 15:51:42 +02:00
parent 051bc78ae6
commit ab37d1509b
20 changed files with 516 additions and 6 deletions

View File

@ -1641,6 +1641,7 @@ it is necessary to enable the `hibernate.query.hql.json_functions_enabled` confi
| `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
| `json_mergepatch()` | Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge
|===
@ -2083,6 +2084,35 @@ include::{json-example-dir-hql}/JsonInsertTest.java[tags=hql-json-insert-example
WARNING: SAP HANA, DB2, H2 and HSQLDB do not support this function.
[[hql-json-mergepatch-function]]
===== `json_mergepatch()`
Merges JSON documents by performing an https://tools.ietf.org/html/rfc7396[RFC 7396] compliant merge, which is
* When the first JSON value is not an object, the result is as if the first argument was an empty object
* When the second JSON value is not an object, the result is the second argument
* When both JSON values are objects, members are merged
** Retain first JSON object members when the second JSON object has no members with matching keys
** Retain second JSON object members when the first JSON object has no members with matching keys and the value is not equal to the JSON `null` literal
** Recursively merge values that exist in both JSON objects, except if the second JSON object member is a JSON `null`
In simple terms this means
* The second JSON overrides members of the first, with JSON `null` values causing members to be removed
* JSON objects are merged recursively
NOTE: Arrays and hence objects within arrays are not merged, but replaced.
[[hql-json-mergepatch-example]]
====
[source, java, indent=0]
----
include::{json-example-dir-hql}/JsonMergepatchTest.java[tags=hql-json-mergepatch-example]
----
====
WARNING: SAP HANA, DB2, SQL Server, H2 and HSQLDB do not support this function. On PostgreSQL, this function is emulated.
[[hql-user-defined-functions]]
==== Native and user-defined functions

View File

@ -512,6 +512,7 @@ public class CockroachLegacyDialect extends Dialect {
functionFactory.jsonRemove_cockroachdb();
functionFactory.jsonReplace_postgresql();
functionFactory.jsonInsert_postgresql();
functionFactory.jsonMergepatch_postgresql();
// Postgres uses # instead of ^ for XOR
functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" )

View File

@ -664,6 +664,7 @@ public class MySQLLegacyDialect extends Dialect {
functionFactory.jsonRemove_mysql();
functionFactory.jsonReplace_mysql();
functionFactory.jsonInsert_mysql();
functionFactory.jsonMergepatch_mysql();
}
}

View File

@ -321,6 +321,7 @@ public class OracleLegacyDialect extends Dialect {
functionFactory.jsonRemove_oracle();
functionFactory.jsonReplace_oracle();
functionFactory.jsonInsert_oracle();
functionFactory.jsonMergepatch_oracle();
}
}

View File

@ -662,6 +662,7 @@ public class PostgreSQLLegacyDialect extends Dialect {
functionFactory.jsonRemove_postgresql();
functionFactory.jsonReplace_postgresql();
functionFactory.jsonInsert_postgresql();
functionFactory.jsonMergepatch_postgresql();
if ( getVersion().isSameOrAfter( 9, 4 ) ) {
functionFactory.makeDateTimeTimestamp();

View File

@ -479,6 +479,7 @@ public class CockroachDialect extends Dialect {
functionFactory.jsonRemove_cockroachdb();
functionFactory.jsonReplace_postgresql();
functionFactory.jsonInsert_postgresql();
functionFactory.jsonMergepatch_postgresql();
// Postgres uses # instead of ^ for XOR
functionContributions.getFunctionRegistry().patternDescriptorBuilder( "bitxor", "(?1#?2)" )

View File

@ -649,6 +649,7 @@ public class MySQLDialect extends Dialect {
functionFactory.jsonRemove_mysql();
functionFactory.jsonReplace_mysql();
functionFactory.jsonInsert_mysql();
functionFactory.jsonMergepatch_mysql();
}
@Override

View File

@ -412,6 +412,7 @@ public class OracleDialect extends Dialect {
functionFactory.jsonRemove_oracle();
functionFactory.jsonReplace_oracle();
functionFactory.jsonInsert_oracle();
functionFactory.jsonMergepatch_oracle();
}
@Override

View File

@ -623,6 +623,7 @@ public class PostgreSQLDialect extends Dialect {
functionFactory.jsonRemove_postgresql();
functionFactory.jsonReplace_postgresql();
functionFactory.jsonInsert_postgresql();
functionFactory.jsonMergepatch_postgresql();
functionFactory.makeDateTimeTimestamp();
// Note that PostgreSQL doesn't support the OVER clause for ordered set-aggregate functions

View File

@ -97,10 +97,8 @@ import org.hibernate.dialect.function.json.HANAJsonObjectFunction;
import org.hibernate.dialect.function.json.HSQLJsonArrayAggFunction;
import org.hibernate.dialect.function.json.HSQLJsonArrayFunction;
import org.hibernate.dialect.function.json.HSQLJsonObjectFunction;
import org.hibernate.dialect.function.json.JsonArrayAggFunction;
import org.hibernate.dialect.function.json.JsonArrayFunction;
import org.hibernate.dialect.function.json.JsonExistsFunction;
import org.hibernate.dialect.function.json.JsonObjectAggFunction;
import org.hibernate.dialect.function.json.JsonObjectFunction;
import org.hibernate.dialect.function.json.JsonQueryFunction;
import org.hibernate.dialect.function.json.JsonValueFunction;
@ -119,6 +117,7 @@ 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.OracleJsonInsertFunction;
import org.hibernate.dialect.function.json.OracleJsonMergepatchFunction;
import org.hibernate.dialect.function.json.OracleJsonObjectAggFunction;
import org.hibernate.dialect.function.json.OracleJsonObjectFunction;
import org.hibernate.dialect.function.json.OracleJsonRemoveFunction;
@ -128,6 +127,7 @@ 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.PostgreSQLJsonInsertFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonMergepatchFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonObjectAggFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonObjectFunction;
import org.hibernate.dialect.function.json.PostgreSQLJsonQueryFunction;
@ -3977,4 +3977,34 @@ public class CommonFunctionFactory {
public void jsonInsert_sqlserver() {
functionRegistry.register( "json_insert", new SQLServerJsonInsertFunction( typeConfiguration ) );
}
/**
* PostgreSQL json_mergepatch() function
*/
public void jsonMergepatch_postgresql() {
functionRegistry.register( "json_mergepatch", new PostgreSQLJsonMergepatchFunction( typeConfiguration ) );
}
/**
* MySQL json_mergepatch() function
*/
public void jsonMergepatch_mysql() {
functionRegistry.namedDescriptorBuilder( "json_mergepatch", "json_merge_patch" )
.setArgumentsValidator( new ArgumentTypesValidator(
StandardArgumentsValidators.min( 2 ),
FunctionParameterType.IMPLICIT_JSON,
FunctionParameterType.IMPLICIT_JSON
) )
.setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON )
) )
.register();
}
/**
* Oracle json_mergepatch() function
*/
public void jsonMergepatch_oracle() {
functionRegistry.register( "json_mergepatch", new OracleJsonMergepatchFunction( typeConfiguration ) );
}
}

View File

@ -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_mergepatch function.
*/
public abstract class AbstractJsonMergepatchFunction extends AbstractSqmSelfRenderingFunctionDescriptor {
public AbstractJsonMergepatchFunction(TypeConfiguration typeConfiguration) {
super(
"json_mergepatch",
FunctionKind.NORMAL,
new ArgumentTypesValidator(
StandardArgumentsValidators.min( 2 ),
FunctionParameterType.IMPLICIT_JSON,
FunctionParameterType.IMPLICIT_JSON
),
StandardFunctionReturnTypeResolvers.invariant(
typeConfiguration.getBasicTypeRegistry().resolve( String.class, SqlTypes.JSON )
),
null
);
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.type.SqlTypes;
import org.hibernate.type.spi.TypeConfiguration;
/**
* Oracle json_mergepatch function.
*/
public class OracleJsonMergepatchFunction extends AbstractJsonMergepatchFunction {
public OracleJsonMergepatchFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> translator) {
final String ddlTypeName = translator.getSessionFactory().getTypeConfiguration().getDdlTypeRegistry()
.getTypeName( SqlTypes.JSON, translator.getSessionFactory().getJdbcServices().getDialect() );
final int argumentCount = arguments.size();
for ( int i = 0; i < argumentCount - 1; i++ ) {
sqlAppender.appendSql( "json_mergepatch(" );
}
arguments.get( 0 ).accept( translator );
for ( int i = 1; i < argumentCount; i++ ) {
sqlAppender.appendSql( ',' );
arguments.get( i ).accept( translator );
sqlAppender.appendSql( " returning " );
sqlAppender.appendSql( ddlTypeName );
sqlAppender.appendSql( ')' );
}
}
}

View File

@ -0,0 +1,208 @@
/*
* 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.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.type.spi.TypeConfiguration;
/**
* PostgreSQL json_mergepatch function.
*/
public class PostgreSQLJsonMergepatchFunction extends AbstractJsonMergepatchFunction {
public PostgreSQLJsonMergepatchFunction(TypeConfiguration typeConfiguration) {
super( typeConfiguration );
}
@Override
public void render(
SqlAppender sqlAppender,
List<? extends SqlAstNode> arguments,
ReturnableType<?> returnType,
SqlAstTranslator<?> translator) {
// Introduce a CTE named "args" which will provide easy access to the arguments in the following
sqlAppender.appendSql( "(with recursive args" );
char separator = '(';
for ( int i = 0; i < arguments.size(); i++ ) {
sqlAppender.appendSql( separator );
sqlAppender.appendSql( 'd' );
sqlAppender.appendSql( i );
separator = ',';
}
sqlAppender.appendSql( ") as(select" );
separator = ' ';
for ( int i = 0; i < arguments.size(); i++ ) {
sqlAppender.appendSql( separator );
renderJsonDocumentExpression( sqlAppender, translator, (Expression) arguments.get( i ) );
separator = ',';
}
sqlAppender.appendSql( ")," );
// Render CTEs that explode JSON into key-value pairs for each parent prefix
for ( int i = 0; i < arguments.size(); i++ ) {
renderKeyValueCte( "val" + i, "d" + i, sqlAppender);
}
// Compute the resulting JSON recursively
sqlAppender.appendSql( "res(v,p,l) as(" );
sqlAppender.appendSql( "select" );
// Aggregate key-value pairs, preferring the last value
sqlAppender.appendSql( " jsonb_object_agg(coalesce(" );
renderColumnList(sqlAppender, "k", arguments.size());
sqlAppender.appendSql( "),coalesce(" );
renderColumnList(sqlAppender, "v", arguments.size());
sqlAppender.appendSql( "))" );
// The parent path
sqlAppender.appendSql( ",coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( ")" );
// The level within the object tree
sqlAppender.appendSql( ",cardinality(coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( "))" );
// Full join the two key-value pair tables based on parent prefix and key
sqlAppender.appendSql( " from val0 v0" );
for ( int i = 1; i < arguments.size(); i++ ) {
sqlAppender.appendSql( " full join val" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( " v" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( " on v0.p=v" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( ".p and v0.k=v" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( ".k" );
}
// start at the bottom
sqlAppender.appendSql( " where cardinality(coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( "))=" );
sqlAppender.appendSql( "(select cardinality(v.p) from val0 v" );
for ( int i = 1; i < arguments.size(); i++ ) {
sqlAppender.appendSql( " union select cardinality(v.p) from val" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( " v" );
}
sqlAppender.appendSql( " order by 1 desc limit 1)" );
// filter rows where the new value is a json null i.e. should be removed
sqlAppender.appendSql( " and jsonb_typeof(coalesce(" );
renderColumnList(sqlAppender, "v", arguments.size(), 1);
sqlAppender.appendSql( ")) is distinct from 'null'" );
sqlAppender.appendSql( " group by" );
sqlAppender.appendSql( " coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( ")" );
sqlAppender.appendSql( ",cardinality(coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( "))" );
sqlAppender.appendSql( " union all " );
sqlAppender.appendSql( "select" );
// Use strict aggregation to ensure a SQL null does not end up as JSON null in the result
sqlAppender.appendSql( " jsonb_object_agg_strict(coalesce(" );
renderColumnList(sqlAppender, "k", arguments.size());
sqlAppender.appendSql( "),coalesce(case when coalesce(" );
renderColumnList(sqlAppender, "k", arguments.size());
sqlAppender.appendSql( ")=r.p[cardinality(r.p)] then r.v end," );
renderColumnList(sqlAppender, "v", arguments.size());
sqlAppender.appendSql( "))" );
// The parent path
sqlAppender.appendSql( ",coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( ")" );
// The level within the object tree
sqlAppender.appendSql( ",r.l-1" );
// Full join the two key-value pair tables based on parent prefix and key
sqlAppender.appendSql( " from val0 v0" );
for ( int i = 1; i < arguments.size(); i++ ) {
sqlAppender.appendSql( " full join val" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( " v" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( " on v0.p=v" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( ".p and v0.k=v" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( ".k" );
}
// Recurse against the previously processed rows with lowest "level" to walk up the tree
sqlAppender.appendSql( " join (select * from res r order by r.l fetch first 1 rows with ties) r" );
sqlAppender.appendSql( " on cardinality(coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( "))=r.l-1" );
// filter rows where the new value is a json null i.e. should be removed
sqlAppender.appendSql( " and jsonb_typeof(coalesce(" );
renderColumnList(sqlAppender, "v", arguments.size(), 1);
sqlAppender.appendSql( ")) is distinct from 'null'" );
// Stop at the last/root level
sqlAppender.appendSql( " and r.l<>0" );
sqlAppender.appendSql( " group by" );
sqlAppender.appendSql( " coalesce(" );
renderColumnList(sqlAppender, "p", arguments.size());
sqlAppender.appendSql( ")" );
sqlAppender.appendSql( ",r.l-1" );
sqlAppender.appendSql( ") " );
// Select the last/root level object
sqlAppender.appendSql( "select r.v from res r where r.l=0)" );
}
private void renderColumnList(SqlAppender sqlAppender, String column, int size) {
renderColumnList( sqlAppender, column, size, 0 );
}
private void renderColumnList(SqlAppender sqlAppender, String column, int size, int end) {
sqlAppender.appendSql( "v" );
sqlAppender.appendSql( size - 1 );
sqlAppender.appendSql( '.' );
sqlAppender.appendSql( column );
for ( int i = size - 2; i >= end; i-- ) {
sqlAppender.appendSql( ",v" );
sqlAppender.appendSql( i );
sqlAppender.appendSql( '.' );
sqlAppender.appendSql( column );
}
}
private void renderKeyValueCte(String cteName, String columnName, SqlAppender sqlAppender) {
sqlAppender.appendSql( cteName );
sqlAppender.appendSql( "(p,k,v) as (");
sqlAppender.appendSql( "select '{}'::text[],s.k,t." );
sqlAppender.appendSql( columnName );
sqlAppender.appendSql( "->s.k from args t join lateral jsonb_object_keys(t." );
sqlAppender.appendSql( columnName );
sqlAppender.appendSql( ") s(k) on 1=1 union " );
sqlAppender.appendSql( "select v.p||v.k,s.k,v.v->s.k from " );
sqlAppender.appendSql( cteName );
sqlAppender.appendSql( " v" );
sqlAppender.appendSql( " join lateral jsonb_object_keys(v.v) s(k)" );
sqlAppender.appendSql( " on jsonb_typeof(v.v)='object'" );
sqlAppender.appendSql( ")," );
}
private void renderJsonDocumentExpression(SqlAppender sqlAppender, SqlAstTranslator<?> translator, Expression json) {
final boolean needsCast = !isJsonType( json );
if ( needsCast ) {
sqlAppender.appendSql( "cast(" );
}
json.accept( translator );
if ( needsCast ) {
sqlAppender.appendSql( " as jsonb)" );
}
}
private boolean isJsonType(Expression expression) {
final JdbcMappingContainer expressionType = expression.getExpressionType();
return expressionType != null && expressionType.getSingleJdbcMapping().getJdbcType().isJson();
}
}

View File

@ -4023,6 +4023,30 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
@Incubating
JpaExpression<String> jsonReplace(Expression<?> jsonDocument, Expression<String> jsonPath, Object value);
/**
* Applies the patch JSON document onto the other JSON document and returns that.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> jsonMergepatch(Expression<?> document, Expression<?> patch);
/**
* Applies the patch JSON document onto the other JSON document and returns that.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> jsonMergepatch(Expression<?> document, String patch);
/**
* Applies the patch JSON document onto the other JSON document and returns that.
*
* @since 7.0
*/
@Incubating
JpaExpression<String> jsonMergepatch(String document, Expression<?> patch);
@Override
JpaPredicate and(List<Predicate> restrictions);

View File

@ -3613,4 +3613,22 @@ public class HibernateCriteriaBuilderDelegate implements HibernateCriteriaBuilde
public JpaExpression<String> jsonReplace(Expression<?> jsonDocument, Expression<String> jsonPath, Object value) {
return criteriaBuilder.jsonReplace( jsonDocument, jsonPath, value );
}
@Override
@Incubating
public JpaExpression<String> jsonMergepatch(Expression<?> document, Expression<?> patch) {
return criteriaBuilder.jsonMergepatch( document, patch );
}
@Override
@Incubating
public JpaExpression<String> jsonMergepatch(Expression<?> document, String patch) {
return criteriaBuilder.jsonMergepatch( document, patch );
}
@Override
@Incubating
public JpaExpression<String> jsonMergepatch(String document, Expression<?> patch) {
return criteriaBuilder.jsonMergepatch( document, patch );
}
}

View File

@ -745,6 +745,15 @@ public interface NodeBuilder extends HibernateCriteriaBuilder, BindingContext {
@Override
SqmExpression<String> jsonReplace(Expression<?> jsonDocument, String jsonPath, Expression<?> value);
@Override
SqmExpression<String> jsonMergepatch(String document, Expression<?> patch);
@Override
SqmExpression<String> jsonMergepatch(Expression<?> document, String patch);
@Override
SqmExpression<String> jsonMergepatch(Expression<?> document, Expression<?> patch);
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Covariant overrides

View File

@ -5651,4 +5651,24 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, Serializable {
queryEngine
);
}
@Override
public SqmExpression<String> jsonMergepatch(String document, Expression<?> patch) {
return jsonMergepatch( value( document ), patch );
}
@Override
public SqmExpression<String> jsonMergepatch(Expression<?> document, String patch) {
return jsonMergepatch( document, value( patch ) );
}
@Override
public SqmExpression<String> jsonMergepatch(Expression<?> document, Expression<?> patch) {
//noinspection unchecked
return getFunctionDescriptor( "json_mergepatch" ).generateSqmExpression(
(List<? extends SqmTypedNode<?>>) (List<?>) asList( document, patch ),
null,
queryEngine
);
}
}

View File

@ -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.SupportsJsonMergepatch.class)
public class JsonMergepatchTest {
@Test
public void testSimple(SessionFactoryScope scope) {
scope.inSession( em -> {
//tag::hql-json-mergepatch-example[]
em.createQuery( "select json_mergepatch('{\"a\":1}', '{\"b\":2}')" ).getResultList();
//end::hql-json-mergepatch-example[]
} );
}
}

View File

@ -1,8 +1,6 @@
/*
* 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
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.query.hql;
@ -607,6 +605,41 @@ public class JsonFunctionTests {
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonMergepatch.class)
public void testJsonMergepatch(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
String json = session.createQuery(
"select json_mergepatch('{\"a\":456, \"b\":[1,2], \"c\":{\"a\":1}}', '{\"a\":null, \"b\":[4,5], \"c\":{\"b\":1}}')",
String.class
).getSingleResult();
Map<String, Object> object = parseObject( json );
assertEquals( 2, object.size() );
assertEquals( Arrays.asList( parseArray( "[4,5]" ) ), object.get( "b" ) );
assertEquals( parseObject( "{\"a\":1,\"b\":1}" ), object.get( "c" ) );
}
);
}
@Test
@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonMergepatch.class)
public void testJsonMergepatchVarargs(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
String json = session.createQuery(
"select json_mergepatch('{\"a\":456, \"b\":[1,2], \"c\":{\"a\":1}}', '{\"a\":null, \"b\":[4,5], \"c\":{\"b\":1}}', '{\"d\":1}')",
String.class
).getSingleResult();
Map<String, Object> object = parseObject( json );
assertEquals( 3, object.size() );
assertEquals( Arrays.asList( parseArray( "[4,5]" ) ), object.get( "b" ) );
assertEquals( parseObject( "{\"a\":1,\"b\":1}" ), object.get( "c" ) );
assertEquals( 1, object.get( "d" ) );
}
);
}
private static final ObjectMapper MAPPER = new ObjectMapper();
private static Map<String, Object> parseObject(String json) {

View File

@ -817,6 +817,12 @@ abstract public class DialectFeatureChecks {
}
}
public static class SupportsJsonMergepatch implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return definesFunction( dialect, "json_mergepatch" );
}
}
public static class IsJtds implements DialectFeatureCheck {
public boolean apply(Dialect dialect) {
return dialect instanceof SybaseDialect && ( (SybaseDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS;