HHH-18604 Add json_mergepatch function
This commit is contained in:
parent
051bc78ae6
commit
ab37d1509b
|
@ -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
|
||||
|
||||
|
|
|
@ -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)" )
|
||||
|
|
|
@ -664,6 +664,7 @@ public class MySQLLegacyDialect extends Dialect {
|
|||
functionFactory.jsonRemove_mysql();
|
||||
functionFactory.jsonReplace_mysql();
|
||||
functionFactory.jsonInsert_mysql();
|
||||
functionFactory.jsonMergepatch_mysql();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -321,6 +321,7 @@ public class OracleLegacyDialect extends Dialect {
|
|||
functionFactory.jsonRemove_oracle();
|
||||
functionFactory.jsonReplace_oracle();
|
||||
functionFactory.jsonInsert_oracle();
|
||||
functionFactory.jsonMergepatch_oracle();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)" )
|
||||
|
|
|
@ -649,6 +649,7 @@ public class MySQLDialect extends Dialect {
|
|||
functionFactory.jsonRemove_mysql();
|
||||
functionFactory.jsonReplace_mysql();
|
||||
functionFactory.jsonInsert_mysql();
|
||||
functionFactory.jsonMergepatch_mysql();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -412,6 +412,7 @@ public class OracleDialect extends Dialect {
|
|||
functionFactory.jsonRemove_oracle();
|
||||
functionFactory.jsonReplace_oracle();
|
||||
functionFactory.jsonInsert_oracle();
|
||||
functionFactory.jsonMergepatch_oracle();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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( ')' );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[]
|
||||
} );
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue