Skip to content

Commit 6d9c8a1

Browse files
committed
Support updating multiple JSON properties in the same column
1 parent ebcfb60 commit 6d9c8a1

File tree

7 files changed

+975
-803
lines changed

7 files changed

+975
-803
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.ExecuteUpdate.cs

Lines changed: 539 additions & 505 deletions
Large diffs are not rendered by default.

src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs

Lines changed: 65 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -156,25 +156,16 @@ protected override Expression VisitUpdate(UpdateExpression updateExpression)
156156
}
157157

158158
// SQL Server 2025 modify method (https://learn.microsoft.com/sql/t-sql/data-types/json-data-type#modify-method)
159-
// We generate the syntax here manually rather than just visiting the SqlFunctionExpression because:
160-
// 1. The JSON column (function instance) needs to be rendered *without* the column, unlike elsewhere.
161-
// 2. The JSON path is packaged as a SqlConstantExpression over IReadOnlyList<PathSegment> which we unpack here
162-
// and from which we generate the jsonpath string.
163-
// In any case this isn't a standard setter of the form SET x = y, but rather just SET [x].modify(...).
159+
// This requires special handling since modify isn't a standard setter of the form SET x = y, but rather just
160+
// SET [x].modify(...).
164161
if (value is SqlFunctionExpression
165162
{
166163
Name: "modify",
167-
Instance: ColumnExpression { TypeMapping.StoreType: "json" } jsonColumn,
168-
Arguments: [SqlConstantExpression { Value: IReadOnlyList<PathSegment> jsonPath }, var item]
164+
IsBuiltIn: true,
165+
Instance: ColumnExpression { TypeMapping.StoreType: "json" } instance
169166
})
170167
{
171-
Sql
172-
.Append(_sqlGenerationHelper.DelimitIdentifier(jsonColumn.Name))
173-
.Append(".modify(");
174-
GenerateJsonPath(jsonPath);
175-
Sql.Append(", ");
176-
Visit(item);
177-
Sql.Append(")");
168+
Visit(value);
178169
continue;
179170
}
180171

@@ -260,40 +251,68 @@ protected override Expression VisitSqlConstant(SqlConstantExpression sqlConstant
260251
/// </summary>
261252
protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunctionExpression)
262253
{
263-
if (sqlFunctionExpression is { IsBuiltIn: true, Arguments: not null }
264-
&& string.Equals(sqlFunctionExpression.Name, "COALESCE", StringComparison.OrdinalIgnoreCase))
254+
switch (sqlFunctionExpression)
265255
{
266-
var type = sqlFunctionExpression.Type;
267-
var typeMapping = sqlFunctionExpression.TypeMapping;
268-
var defaultTypeMapping = _typeMappingSource.FindMapping(type);
269-
270-
// ISNULL always return a value having the same type as its first
271-
// argument. Ideally we would convert the argument to have the
272-
// desired type and type mapping, but currently EFCore has some
273-
// trouble in computing types of non-homogeneous expressions
274-
// (tracked in https://github.com/dotnet/efcore/issues/15586). To
275-
// stay on the safe side we only use ISNULL if:
276-
// - all sub-expressions have the same type as the expression
277-
// - all sub-expressions have the same type mapping as the expression
278-
// - the expression is using the default type mapping (combined
279-
// with the two above, this implies that all of the expressions
280-
// are using the default type mapping of the type)
281-
if (defaultTypeMapping == typeMapping
282-
&& sqlFunctionExpression.Arguments.All(a => a.Type == type && a.TypeMapping == typeMapping))
256+
case { IsBuiltIn: true, Arguments: not null }
257+
when string.Equals(sqlFunctionExpression.Name, "COALESCE", StringComparison.OrdinalIgnoreCase):
283258
{
284-
var head = sqlFunctionExpression.Arguments[0];
285-
sqlFunctionExpression = (SqlFunctionExpression)sqlFunctionExpression
286-
.Arguments
287-
.Skip(1)
288-
.Aggregate(
289-
head, (l, r) => new SqlFunctionExpression(
290-
"ISNULL",
291-
arguments: [l, r],
292-
nullable: true,
293-
argumentsPropagateNullability: [false, false],
294-
sqlFunctionExpression.Type,
295-
sqlFunctionExpression.TypeMapping
296-
));
259+
var type = sqlFunctionExpression.Type;
260+
var typeMapping = sqlFunctionExpression.TypeMapping;
261+
var defaultTypeMapping = _typeMappingSource.FindMapping(type);
262+
263+
// ISNULL always return a value having the same type as its first
264+
// argument. Ideally we would convert the argument to have the
265+
// desired type and type mapping, but currently EFCore has some
266+
// trouble in computing types of non-homogeneous expressions
267+
// (tracked in https://github.com/dotnet/efcore/issues/15586). To
268+
// stay on the safe side we only use ISNULL if:
269+
// - all sub-expressions have the same type as the expression
270+
// - all sub-expressions have the same type mapping as the expression
271+
// - the expression is using the default type mapping (combined
272+
// with the two above, this implies that all of the expressions
273+
// are using the default type mapping of the type)
274+
if (defaultTypeMapping == typeMapping
275+
&& sqlFunctionExpression.Arguments.All(a => a.Type == type && a.TypeMapping == typeMapping))
276+
{
277+
var head = sqlFunctionExpression.Arguments[0];
278+
sqlFunctionExpression = (SqlFunctionExpression)sqlFunctionExpression
279+
.Arguments
280+
.Skip(1)
281+
.Aggregate(
282+
head, (l, r) => new SqlFunctionExpression(
283+
"ISNULL",
284+
arguments: [l, r],
285+
nullable: true,
286+
argumentsPropagateNullability: [false, false],
287+
sqlFunctionExpression.Type,
288+
sqlFunctionExpression.TypeMapping
289+
));
290+
}
291+
292+
return base.VisitSqlFunction(sqlFunctionExpression);
293+
}
294+
295+
// SQL Server 2025 modify method (https://learn.microsoft.com/sql/t-sql/data-types/json-data-type#modify-method)
296+
// We get here only from within UPDATE setters.
297+
// We generate the syntax here manually rather than just using the regular function visitation logic since
298+
// the JSON column (function instance) needs to be rendered *without* the column, unlike elsewhere.
299+
case
300+
{
301+
Name: "modify",
302+
IsBuiltIn: true,
303+
Instance: ColumnExpression { TypeMapping.StoreType: "json" } jsonColumn,
304+
Arguments: [SqlConstantExpression { Value: IReadOnlyList<PathSegment> jsonPath }, var item]
305+
}:
306+
{
307+
Sql
308+
.Append(_sqlGenerationHelper.DelimitIdentifier(jsonColumn.Name))
309+
.Append(".modify(");
310+
GenerateJsonPath(jsonPath);
311+
Sql.Append(", ");
312+
Visit(item);
313+
Sql.Append(")");
314+
315+
return sqlFunctionExpression;
297316
}
298317
}
299318

0 commit comments

Comments
 (0)