From ec502138b16cc2cdd3a5812b21fe835fbb349f92 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 19 Sep 2024 19:30:27 +0200 Subject: [PATCH] HHH-18604 Fix some issues with old SQL Server versions --- .../dialect/SQLServerLegacyDialect.java | 16 +-- .../hibernate/dialect/SQLServerDialect.java | 16 +-- .../function/CommonFunctionFactory.java | 32 +++--- .../function/json/ExpressionTypeHelper.java | 7 ++ .../dialect/function/json/JsonPathHelper.java | 45 ++++++++ .../json/SQLServerJsonArrayAggFunction.java | 45 ++++++-- .../SQLServerJsonArrayAppendFunction.java | 36 ++++++- .../json/SQLServerJsonArrayFunction.java | 69 +++++++++++- .../SQLServerJsonArrayInsertFunction.java | 6 +- .../json/SQLServerJsonExistsFunction.java | 101 ++++++++++++------ .../json/SQLServerJsonInsertFunction.java | 32 +++++- .../json/SQLServerJsonObjectAggFunction.java | 39 +++++-- .../json/SQLServerJsonObjectFunction.java | 70 +++++++++++- .../json/SQLServerJsonReplaceFunction.java | 34 +++++- 14 files changed, 457 insertions(+), 91 deletions(-) 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 7a7512d24e..dfed524539 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 @@ -403,20 +403,20 @@ public class SQLServerLegacyDialect extends AbstractTransactSQLDialect { if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); functionFactory.jsonQuery_sqlserver(); - functionFactory.jsonExists_sqlserver(); - functionFactory.jsonObject_sqlserver(); - functionFactory.jsonArray_sqlserver(); + functionFactory.jsonExists_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonObject_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonArray_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonSet_sqlserver(); functionFactory.jsonRemove_sqlserver(); - functionFactory.jsonReplace_sqlserver(); - functionFactory.jsonInsert_sqlserver(); - functionFactory.jsonArrayAppend_sqlserver(); + functionFactory.jsonReplace_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonInsert_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); - functionFactory.jsonArrayAgg_sqlserver(); - functionFactory.jsonObjectAgg_sqlserver(); + functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonObjectAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); } if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.leastGreatest(); 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 a3810537e0..a18eef1473 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -421,20 +421,20 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { if ( getVersion().isSameOrAfter( 13 ) ) { functionFactory.jsonValue_sqlserver(); functionFactory.jsonQuery_sqlserver(); - functionFactory.jsonExists_sqlserver(); - functionFactory.jsonObject_sqlserver(); - functionFactory.jsonArray_sqlserver(); + functionFactory.jsonExists_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonObject_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonArray_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonSet_sqlserver(); functionFactory.jsonRemove_sqlserver(); - functionFactory.jsonReplace_sqlserver(); - functionFactory.jsonInsert_sqlserver(); - functionFactory.jsonArrayAppend_sqlserver(); + functionFactory.jsonReplace_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonInsert_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonArrayAppend_sqlserver( getVersion().isSameOrAfter( 16 ) ); functionFactory.jsonArrayInsert_sqlserver(); } if ( getVersion().isSameOrAfter( 14 ) ) { functionFactory.listagg_stringAggWithinGroup( "varchar(max)" ); - functionFactory.jsonArrayAgg_sqlserver(); - functionFactory.jsonObjectAgg_sqlserver(); + functionFactory.jsonArrayAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); + functionFactory.jsonObjectAgg_sqlserver( getVersion().isSameOrAfter( 16 ) ); } if ( getVersion().isSameOrAfter( 16 ) ) { functionFactory.leastGreatest(); 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 8de8aeadf9..a2d1114e30 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 @@ -3557,8 +3557,8 @@ public class CommonFunctionFactory { /** * SQL Server json_exists() function */ - public void jsonExists_sqlserver() { - functionRegistry.register( "json_exists", new SQLServerJsonExistsFunction( typeConfiguration ) ); + public void jsonExists_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_exists", new SQLServerJsonExistsFunction( supportsExtendedJson, typeConfiguration ) ); } /** @@ -3613,8 +3613,8 @@ public class CommonFunctionFactory { /** * SQL Server json_object() function */ - public void jsonObject_sqlserver() { - functionRegistry.register( "json_object", new SQLServerJsonObjectFunction( typeConfiguration ) ); + public void jsonObject_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_object", new SQLServerJsonObjectFunction( supportsExtendedJson, typeConfiguration ) ); } /** @@ -3669,8 +3669,8 @@ public class CommonFunctionFactory { /** * SQL Server json_array() function */ - public void jsonArray_sqlserver() { - functionRegistry.register( "json_array", new SQLServerJsonArrayFunction( typeConfiguration ) ); + public void jsonArray_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_array", new SQLServerJsonArrayFunction( supportsExtendedJson, typeConfiguration ) ); } /** @@ -3739,8 +3739,8 @@ public class CommonFunctionFactory { /** * SQL Server json_arrayagg() function */ - public void jsonArrayAgg_sqlserver() { - functionRegistry.register( "json_arrayagg", new SQLServerJsonArrayAggFunction( typeConfiguration ) ); + public void jsonArrayAgg_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_arrayagg", new SQLServerJsonArrayAggFunction( supportsExtendedJson, typeConfiguration ) ); } /** @@ -3809,8 +3809,8 @@ public class CommonFunctionFactory { /** * SQL Server json_objectagg() function */ - public void jsonObjectAgg_sqlserver() { - functionRegistry.register( "json_objectagg", new SQLServerJsonObjectAggFunction( typeConfiguration ) ); + public void jsonObjectAgg_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_objectagg", new SQLServerJsonObjectAggFunction( supportsExtendedJson, typeConfiguration ) ); } /** @@ -3943,8 +3943,8 @@ public class CommonFunctionFactory { /** * SQL server json_replace() function */ - public void jsonReplace_sqlserver() { - functionRegistry.register( "json_replace", new SQLServerJsonReplaceFunction( typeConfiguration ) ); + public void jsonReplace_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_replace", new SQLServerJsonReplaceFunction( supportsExtendedJson, typeConfiguration ) ); } /** @@ -3981,8 +3981,8 @@ public class CommonFunctionFactory { /** * SQL server json_insert() function */ - public void jsonInsert_sqlserver() { - functionRegistry.register( "json_insert", new SQLServerJsonInsertFunction( typeConfiguration ) ); + public void jsonInsert_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_insert", new SQLServerJsonInsertFunction( supportsExtendedJson, typeConfiguration ) ); } /** @@ -4056,8 +4056,8 @@ public class CommonFunctionFactory { /** * SQL server json_array_append() function */ - public void jsonArrayAppend_sqlserver() { - functionRegistry.register( "json_array_append", new SQLServerJsonArrayAppendFunction( typeConfiguration ) ); + public void jsonArrayAppend_sqlserver(boolean supportsExtendedJson) { + functionRegistry.register( "json_array_append", new SQLServerJsonArrayAppendFunction( supportsExtendedJson, typeConfiguration ) ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java index 5a4211ea59..b4445bf1c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/ExpressionTypeHelper.java @@ -9,6 +9,7 @@ import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.sqm.CastType; import org.hibernate.sql.ast.tree.SqlAstNode; import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.descriptor.jdbc.JdbcType; @Internal public class ExpressionTypeHelper { @@ -34,6 +35,12 @@ public class ExpressionTypeHelper { && expressionType.getSingleJdbcMapping().getJdbcType().isJson(); } + public static JdbcType getSingleJdbcType(SqlAstNode node) { + final Expression expression = (Expression) node; + final JdbcMappingContainer expressionType = expression.getExpressionType(); + return expressionType.getSingleJdbcMapping().getJdbcType(); + } + public static boolean isBoolean(CastType castType) { switch ( castType ) { case BOOLEAN: diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java index 87f5b83e64..1fb56877a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/JsonPathHelper.java @@ -204,6 +204,51 @@ public class JsonPathHelper { return i + 1; } + public static void inlinePassingClause( + List jsonPathElements, + JsonPathPassingClause passingClause, + SqlAstTranslator walker) { + for ( int i = 0; i < jsonPathElements.size(); i++ ) { + final JsonPathElement jsonPathElement = jsonPathElements.get( i ); + if ( jsonPathElement instanceof JsonParameterIndexAccess parameterIndexAccess ) { + final Expression expression = passingClause.getPassingExpressions() + .get( parameterIndexAccess.parameterName() ); + if ( expression == null ) { + throw new QueryException( "JSON path [" + toJsonPath( jsonPathElements ) + "] uses parameter [" + parameterIndexAccess.parameterName() + "] that is not passed" ); + } + jsonPathElements.set( i, new JsonIndexAccess( walker.getLiteralValue( expression ) ) ); + } + } + } + + public static String toJsonPath(List pathElements) { + return toJsonPath( pathElements, 0, pathElements.size() ); + } + + public static String toJsonPath(List pathElements, int start, int end) { + final StringBuilder jsonPath = new StringBuilder(); + jsonPath.append( "$" ); + for ( int i = start; i < end; i++ ) { + final JsonPathElement jsonPathElement = pathElements.get( i ); + if ( jsonPathElement instanceof JsonAttribute pathAttribute ) { + jsonPath.append( '.' ); + jsonPath.append( pathAttribute.attribute() ); + } + else if ( jsonPathElement instanceof JsonParameterIndexAccess parameterIndexAccess ) { + jsonPath.append( "[$" ); + jsonPath.append( parameterIndexAccess.parameterName() ); + jsonPath.append( "]" ); + } + else { + assert jsonPathElement instanceof JsonIndexAccess; + jsonPath.append( "[" ); + jsonPath.append( ( (JsonIndexAccess) jsonPathElement ).index() ); + jsonPath.append( "]" ); + } + } + return jsonPath.toString(); + } + public sealed interface JsonPathElement {} public record JsonAttribute(String attribute) implements JsonPathElement {} public record JsonIndexAccess(int index) implements JsonPathElement {} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java index d31087f083..6cd3e0daa4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAggFunction.java @@ -17,6 +17,8 @@ import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.spi.TypeConfiguration; /** @@ -24,8 +26,11 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class SQLServerJsonArrayAggFunction extends JsonArrayAggFunction { - public SQLServerJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsExtendedJson; + + public SQLServerJsonArrayAggFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { super( false, typeConfiguration ); + this.supportsExtendedJson = supportsExtendedJson; } @Override @@ -90,10 +95,38 @@ public class SQLServerJsonArrayAggFunction extends JsonArrayAggFunction { Expression arg, JsonNullBehavior nullBehavior, SqlAstTranslator translator) { - sqlAppender.appendSql( "substring(json_array(" ); - arg.accept( translator ); - sqlAppender.appendSql( " null on null),2,len(json_array(" ); - arg.accept( translator ); - sqlAppender.appendSql( " null on null))-2)" ); + if ( supportsExtendedJson ) { + sqlAppender.appendSql( "substring(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null),2,len(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( "))-2)" ); + } + else { + sqlAppender.appendSql( "substring(json_modify('[]','append $'," ); + final boolean needsConversion = needsConversion( arg ); + if ( needsConversion ) { + sqlAppender.appendSql( "convert(nvarchar(max)," ); + } + arg.accept( translator ); + if ( needsConversion ) { + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( "),2,len(json_modify('[]','append $'," ); + if ( needsConversion ) { + sqlAppender.appendSql( "convert(nvarchar(max)," ); + } + arg.accept( translator ); + if ( needsConversion ) { + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( "))-2)" ); + } + } + + static boolean needsConversion(Expression arg) { + final JdbcType jdbcType = ExpressionTypeHelper.getSingleJdbcType( arg ); + // json_modify() doesn't seem to like UUID values + return jdbcType.getDdlTypeCode() == SqlTypes.UUID; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java index 4403224b0e..5ceb03b547 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayAppendFunction.java @@ -18,8 +18,11 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class SQLServerJsonArrayAppendFunction extends AbstractJsonArrayAppendFunction { - public SQLServerJsonArrayAppendFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsExtendedJson; + + public SQLServerJsonArrayAppendFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { super( typeConfiguration ); + this.supportsExtendedJson = supportsExtendedJson; } @Override @@ -33,7 +36,31 @@ public class SQLServerJsonArrayAppendFunction extends AbstractJsonArrayAppendFun final SqlAstNode value = arguments.get( 2 ); sqlAppender.appendSql( "(select coalesce(" ); sqlAppender.appendSql("case when json_modify(json_query(t.d,t.p),'append $',t.v) is not null then json_modify(t.d,t.p,json_modify(json_query(t.d,t.p),'append $',t.v)) end,"); - sqlAppender.appendSql("json_modify(t.d,t.p,json_query('['+coalesce(json_value(t.d,t.p),case when json_path_exists(t.d,t.p)=1 then 'null' end)+stuff(json_array(t.v),1,1,','))),"); + sqlAppender.appendSql("json_modify(t.d,t.p,json_query('['+coalesce(json_value(t.d,t.p),case when "); + if ( supportsExtendedJson ) { + sqlAppender.appendSql( "json_path_exists(t.d,t.p)=1" ); + } + else { + final List pathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + final JsonPathHelper.JsonPathElement lastPathElement = pathElements.get( pathElements.size() - 1 ); + final String prefix = JsonPathHelper.toJsonPath( pathElements, 0, pathElements.size() - 1 ); + final String terminalKey; + if ( lastPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) { + terminalKey = String.valueOf( indexAccess.index() ); + } + else { + assert lastPathElement instanceof JsonPathHelper.JsonAttribute; + terminalKey = ( (JsonPathHelper.JsonAttribute) lastPathElement ).attribute(); + } + + sqlAppender.appendSql( "(select 1 from openjson(t.d," ); + sqlAppender.appendSingleQuoteEscapedString( prefix ); + sqlAppender.appendSql( ") t where t.[key]=" ); + sqlAppender.appendSingleQuoteEscapedString( terminalKey ); + sqlAppender.appendSql( ")=1" ); + } + sqlAppender.appendSql( " then 'null' end)+stuff(json_modify('[]','append $',t.v),1,1,','))),"); sqlAppender.appendSql( "t.d) from (values (" ); json.accept( translator ); sqlAppender.appendSql( ',' ); @@ -49,6 +76,11 @@ public class SQLServerJsonArrayAppendFunction extends AbstractJsonArrayAppendFun value.accept( translator ); sqlAppender.appendSql( " as bit)" ); } + else if ( ExpressionTypeHelper.isJson( value ) ) { + sqlAppender.appendSql( "json_query(" ); + value.accept( translator ); + sqlAppender.appendSql( ')' ); + } else { value.accept( translator ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java index 0b7788ad86..4c0d1334f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayFunction.java @@ -4,9 +4,13 @@ */ 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.JsonNullBehavior; import org.hibernate.type.spi.TypeConfiguration; /** @@ -14,8 +18,66 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class SQLServerJsonArrayFunction extends JsonArrayFunction { - public SQLServerJsonArrayFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsExtendedJson; + + public SQLServerJsonArrayFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { super( typeConfiguration ); + this.supportsExtendedJson = supportsExtendedJson; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( supportsExtendedJson ) { + super.render( sqlAppender, sqlAstArguments, returnType, walker ); + } + else { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "'[]'" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select '['+string_agg(substring(t.d,2,len(t.d)-2),',')" ); + sqlAppender.appendSql( "within group (order by t.k)+']' from (values" ); + char separator = ' '; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( '(' ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ",json_modify('[]','append $'," ); + renderValue( sqlAppender, sqlAstArguments.get( i ), walker ); + sqlAppender.appendSql( "))" ); + separator = ','; + } + sqlAppender.appendSql( ") t(k,d))" ); + } + else { + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( "json_modify(" ); + } + sqlAppender.appendSql( "'[]'" ); + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( ",'append $'," ); + renderValue( sqlAppender, sqlAstArguments.get( i ), walker ); + sqlAppender.appendSql( ')' ); + } + } + } + } } @Override @@ -25,6 +87,11 @@ public class SQLServerJsonArrayFunction extends JsonArrayFunction { value.accept( walker ); sqlAppender.appendSql( " as bit)" ); } + else if ( !supportsExtendedJson && ExpressionTypeHelper.isJson( value ) ) { + sqlAppender.appendSql( "json_query(" ); + value.accept( walker ); + sqlAppender.appendSql( ')' ); + } else { value.accept( walker ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java index 24c9d45c5d..d29a69a3e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonArrayInsertFunction.java @@ -79,10 +79,10 @@ public class SQLServerJsonArrayInsertFunction extends AbstractJsonArrayInsertFun SqlAppender sqlAppender, SqlAstNode arg, SqlAstTranslator translator) { - sqlAppender.appendSql( "substring(json_array(" ); + sqlAppender.appendSql( "substring(json_modify('[]','append $'," ); arg.accept( translator ); - sqlAppender.appendSql( " null on null),2,len(json_array(" ); + sqlAppender.appendSql( "),2,len(json_modify('[]','append $'," ); arg.accept( translator ); - sqlAppender.appendSql( " null on null))-2)" ); + sqlAppender.appendSql( "))-2)" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java index 93bc7833f6..f54f553f72 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonExistsFunction.java @@ -4,6 +4,8 @@ */ package org.hibernate.dialect.function.json; +import java.util.List; + import org.hibernate.QueryException; import org.hibernate.query.ReturnableType; import org.hibernate.sql.ast.SqlAstTranslator; @@ -17,13 +19,11 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class SQLServerJsonExistsFunction extends JsonExistsFunction { - public SQLServerJsonExistsFunction(TypeConfiguration typeConfiguration) { - super( typeConfiguration, true, false ); - } + private final boolean supportsExtendedJson; - @Override - public boolean isPredicate() { - return false; + public SQLServerJsonExistsFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + this.supportsExtendedJson = supportsExtendedJson; } @Override @@ -35,35 +35,14 @@ public class SQLServerJsonExistsFunction extends JsonExistsFunction { if ( arguments.errorBehavior() == JsonExistsErrorBehavior.TRUE ) { throw new QueryException( "Can't emulate json_exists(... true on error) on SQL Server" ); } - if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) { - sqlAppender.append( '(' ); - } - sqlAppender.appendSql( "json_path_exists(" ); - arguments.jsonDocument().accept( walker ); - sqlAppender.appendSql( ',' ); - final JsonPathPassingClause passingClause = arguments.passingClause(); - if ( passingClause != null ) { - JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( - sqlAppender, - "", - arguments.jsonPath(), - passingClause, - walker - ); - } - else { - walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( - sqlAppender, - walker.getLiteralValue( arguments.jsonPath() ) - ); - } - sqlAppender.appendSql( ')' ); - if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) { - // json_path_exists returns 0 if an invalid JSON is given, - // so we have to run openjson to be sure the json is valid and potentially throw an error - sqlAppender.appendSql( "=1 or (select v from openjson(" ); + if ( supportsExtendedJson ) { + if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) { + sqlAppender.append( '(' ); + } + sqlAppender.appendSql( "json_path_exists(" ); arguments.jsonDocument().accept( walker ); - sqlAppender.appendSql( ") with (v varchar(max) " ); + sqlAppender.appendSql( ',' ); + final JsonPathPassingClause passingClause = arguments.passingClause(); if ( passingClause != null ) { JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( sqlAppender, @@ -79,7 +58,59 @@ public class SQLServerJsonExistsFunction extends JsonExistsFunction { walker.getLiteralValue( arguments.jsonPath() ) ); } - sqlAppender.appendSql( ")) is null)" ); + sqlAppender.appendSql( ")=1" ); + if ( arguments.errorBehavior() == JsonExistsErrorBehavior.ERROR ) { + // json_path_exists returns 0 if an invalid JSON is given, + // so we have to run openjson to be sure the json is valid and potentially throw an error + sqlAppender.appendSql( " or (select v from openjson(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ") with (v varchar(max) " ); + if ( passingClause != null ) { + JsonPathHelper.appendInlinedJsonPathIncludingPassingClause( + sqlAppender, + "", + arguments.jsonPath(), + passingClause, + walker + ); + } + else { + walker.getSessionFactory().getJdbcServices().getDialect().appendLiteral( + sqlAppender, + walker.getLiteralValue( arguments.jsonPath() ) + ); + } + sqlAppender.appendSql( ")) is null)" ); + } + } + else { + if ( arguments.errorBehavior() == JsonExistsErrorBehavior.FALSE ) { + throw new QueryException( "Can't emulate json_exists(... false on error) on SQL Server" ); + } + final String jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + final JsonPathPassingClause passingClause = arguments.passingClause(); + final List pathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + if ( passingClause != null ) { + JsonPathHelper.inlinePassingClause( pathElements, passingClause, walker ); + } + final JsonPathHelper.JsonPathElement lastPathElement = pathElements.get( pathElements.size() - 1 ); + final String prefix = JsonPathHelper.toJsonPath( pathElements, 0, pathElements.size() - 1 ); + final String terminalKey; + if ( lastPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) { + terminalKey = String.valueOf( indexAccess.index() ); + } + else { + assert lastPathElement instanceof JsonPathHelper.JsonAttribute; + terminalKey = ( (JsonPathHelper.JsonAttribute) lastPathElement ).attribute(); + } + + sqlAppender.appendSql( "(select 1 from openjson(" ); + arguments.jsonDocument().accept( walker ); + sqlAppender.appendSql( ',' ); + sqlAppender.appendSingleQuoteEscapedString( prefix ); + sqlAppender.appendSql( ") t where t.[key]=" ); + sqlAppender.appendSingleQuoteEscapedString( terminalKey ); + sqlAppender.appendSql( ")=1" ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java index 1aeaf1015c..eb145a1f23 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonInsertFunction.java @@ -18,8 +18,11 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class SQLServerJsonInsertFunction extends AbstractJsonInsertFunction { - public SQLServerJsonInsertFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsExtendedJson; + + public SQLServerJsonInsertFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { super( typeConfiguration ); + this.supportsExtendedJson = supportsExtendedJson; } @Override @@ -28,10 +31,35 @@ public class SQLServerJsonInsertFunction extends AbstractJsonInsertFunction { List arguments, ReturnableType returnType, SqlAstTranslator translator) { - sqlAppender.appendSql( "(select case when coalesce(json_query(t.d,t.p),json_value(t.d,t.p)) is not null then t.d else json_modify(t.d,t.p," ); final Expression json = (Expression) arguments.get( 0 ); final Expression jsonPath = (Expression) arguments.get( 1 ); final SqlAstNode value = arguments.get( 2 ); + + sqlAppender.appendSql( "(select case when " ); + if ( supportsExtendedJson ) { + sqlAppender.appendSql( "json_path_exists(t.d,t.p)=1" ); + } + else { + final List pathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + final JsonPathHelper.JsonPathElement lastPathElement = pathElements.get( pathElements.size() - 1 ); + final String prefix = JsonPathHelper.toJsonPath( pathElements, 0, pathElements.size() - 1 ); + final String terminalKey; + if ( lastPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) { + terminalKey = String.valueOf( indexAccess.index() ); + } + else { + assert lastPathElement instanceof JsonPathHelper.JsonAttribute; + terminalKey = ( (JsonPathHelper.JsonAttribute) lastPathElement ).attribute(); + } + + sqlAppender.appendSql( "(select 1 from openjson(t.d," ); + sqlAppender.appendSingleQuoteEscapedString( prefix ); + sqlAppender.appendSql( ") t where t.[key]=" ); + sqlAppender.appendSingleQuoteEscapedString( terminalKey ); + sqlAppender.appendSql( ")=1" ); + } + sqlAppender.appendSql( " then t.d else json_modify(t.d,t.p," ); renderValue( sqlAppender, value, translator ); sqlAppender.appendSql( ") end from (values("); json.accept( translator ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java index 02c51abb9e..71b5a5024a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectAggFunction.java @@ -15,13 +15,18 @@ import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.type.spi.TypeConfiguration; +import static org.hibernate.dialect.function.json.SQLServerJsonArrayAggFunction.needsConversion; + /** * SQL Server json_objectagg function. */ public class SQLServerJsonObjectAggFunction extends JsonObjectAggFunction { - public SQLServerJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsExtendedJson; + + public SQLServerJsonObjectAggFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { super( ",", false, typeConfiguration ); + this.supportsExtendedJson = supportsExtendedJson; } @Override @@ -66,11 +71,33 @@ public class SQLServerJsonObjectAggFunction extends JsonObjectAggFunction { if ( nullBehavior != JsonNullBehavior.NULL ) { sqlAppender.appendSql( "nullif(" ); } - sqlAppender.appendSql( "substring(json_array(" ); - arg.accept( translator ); - sqlAppender.appendSql( " null on null),2,len(json_array(" ); - arg.accept( translator ); - sqlAppender.appendSql( " null on null))-2)" ); + if ( supportsExtendedJson ) { + sqlAppender.appendSql( "substring(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null),2,len(json_array(" ); + arg.accept( translator ); + sqlAppender.appendSql( " null on null))-2)" ); + } + else { + sqlAppender.appendSql( "substring(json_modify('[]','append $'," ); + final boolean needsConversion = needsConversion( arg ); + if ( needsConversion ) { + sqlAppender.appendSql( "convert(nvarchar(max)," ); + } + arg.accept( translator ); + if ( needsConversion ) { + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( "),2,len(json_modify('[]','append $'," ); + if ( needsConversion ) { + sqlAppender.appendSql( "convert(nvarchar(max)," ); + } + arg.accept( translator ); + if ( needsConversion ) { + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( "))-2)" ); + } if ( nullBehavior != JsonNullBehavior.NULL ) { sqlAppender.appendSql( ",'null')" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java index 16d8a811ab..40b349f683 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonObjectFunction.java @@ -4,9 +4,13 @@ */ 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.JsonNullBehavior; import org.hibernate.type.spi.TypeConfiguration; /** @@ -14,8 +18,67 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class SQLServerJsonObjectFunction extends JsonObjectFunction { - public SQLServerJsonObjectFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsExtendedJson; + + public SQLServerJsonObjectFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { super( typeConfiguration, true ); + this.supportsExtendedJson = supportsExtendedJson; + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( supportsExtendedJson ) { + super.render( sqlAppender, sqlAstArguments, returnType, walker ); + } + else { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "'{}'" ); + } + else { + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( ( sqlAstArguments.size() & 1 ) == 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( sqlAstArguments.size() - 1 ); + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + for ( int i = 0; i < argumentsCount; i += 2 ) { + sqlAppender.appendSql( "json_modify(" ); + } + sqlAppender.appendSql( "'{}'" ); + for ( int i = 0; i < argumentsCount; i += 2 ) { + sqlAppender.appendSql( ",'$.'+" ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( ',' ); + renderValue( sqlAppender, sqlAstArguments.get( i + 1 ), walker ); + sqlAppender.appendSql( ')' ); + } + } + else { + sqlAppender.appendSql( "(select '{'+string_agg(substring(t.k,2,len(t.k)-2)" ); + sqlAppender.appendSql( "+':'+substring(t.d,2,len(t.d)-2),',')+'}' from (values" ); + char separator = ' '; + for ( int i = 0; i < argumentsCount; i += 2 ) { + sqlAppender.appendSql( separator ); + sqlAppender.appendSql( "(json_modify('[]','append $'," ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( "),json_modify('[]','append $'," ); + renderValue( sqlAppender, sqlAstArguments.get( i + 1 ), walker ); + sqlAppender.appendSql( "))" ); + separator = ','; + } + sqlAppender.appendSql( ") t(k,d))" ); + } + } + } } @Override @@ -25,6 +88,11 @@ public class SQLServerJsonObjectFunction extends JsonObjectFunction { value.accept( walker ); sqlAppender.appendSql( " as bit)" ); } + else if ( !supportsExtendedJson && ExpressionTypeHelper.isJson( value ) ) { + sqlAppender.appendSql( "json_query(" ); + value.accept( walker ); + sqlAppender.appendSql( ')' ); + } else { value.accept( walker ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java index a0177ae1a6..4a6bab1183 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/json/SQLServerJsonReplaceFunction.java @@ -18,8 +18,11 @@ import org.hibernate.type.spi.TypeConfiguration; */ public class SQLServerJsonReplaceFunction extends AbstractJsonReplaceFunction { - public SQLServerJsonReplaceFunction(TypeConfiguration typeConfiguration) { + private final boolean supportsExtendedJson; + + public SQLServerJsonReplaceFunction(boolean supportsExtendedJson, TypeConfiguration typeConfiguration) { super( typeConfiguration ); + this.supportsExtendedJson = supportsExtendedJson; } @Override @@ -28,12 +31,37 @@ public class SQLServerJsonReplaceFunction extends AbstractJsonReplaceFunction { List arguments, ReturnableType returnType, SqlAstTranslator translator) { - sqlAppender.appendSql( "(select case when coalesce(json_query(t.d,t.p),json_value(t.d,t.p)) is null then t.d else json_modify(t.d,t.p," ); final Expression json = (Expression) arguments.get( 0 ); final Expression jsonPath = (Expression) arguments.get( 1 ); final SqlAstNode value = arguments.get( 2 ); + + sqlAppender.appendSql( "(select case when " ); + if ( supportsExtendedJson ) { + sqlAppender.appendSql( "json_path_exists(t.d,t.p)=1" ); + } + else { + final List pathElements = + JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( jsonPath ) ); + final JsonPathHelper.JsonPathElement lastPathElement = pathElements.get( pathElements.size() - 1 ); + final String prefix = JsonPathHelper.toJsonPath( pathElements, 0, pathElements.size() - 1 ); + final String terminalKey; + if ( lastPathElement instanceof JsonPathHelper.JsonIndexAccess indexAccess ) { + terminalKey = String.valueOf( indexAccess.index() ); + } + else { + assert lastPathElement instanceof JsonPathHelper.JsonAttribute; + terminalKey = ( (JsonPathHelper.JsonAttribute) lastPathElement ).attribute(); + } + + sqlAppender.appendSql( "(select 1 from openjson(t.d," ); + sqlAppender.appendSingleQuoteEscapedString( prefix ); + sqlAppender.appendSql( ") t where t.[key]=" ); + sqlAppender.appendSingleQuoteEscapedString( terminalKey ); + sqlAppender.appendSql( ")=1" ); + } + sqlAppender.appendSql( " then json_modify(t.d,t.p," ); renderValue( sqlAppender, value, translator ); - sqlAppender.appendSql( ") end from (values("); + sqlAppender.appendSql( ") else t.d end from (values("); json.accept( translator ); sqlAppender.appendSql( ',' ); jsonPath.accept( translator );