Skip to content

Commit 44a2855

Browse files
committed
Property encode JSON scalars in partial updates
And introduce new store type tests
1 parent 6711c2a commit 44a2855

File tree

11 files changed

+650
-41
lines changed

11 files changed

+650
-41
lines changed

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,12 @@
433433
<data name="ExecuteOperationWithUnsupportedOperatorInSqlGeneration" xml:space="preserve">
434434
<value>The operation '{operation}' contains a select expression feature that isn't supported in the query SQL generator, but has been declared as supported by provider during translation phase. This is a bug in your EF Core provider, file an issue at https://aka.ms/efcorefeedback.</value>
435435
</data>
436+
<data name="ExecuteUpdateCannotSetJsonPropertyToNonJsonColumn" xml:space="preserve">
437+
<value>'ExecuteUpdate' cannot currently set a property in a JSON column to a regular, non-JSON column; see https://github.com/dotnet/efcore/issues/36688.</value>
438+
</data>
439+
<data name="ExecuteUpdateCannotSetJsonPropertyToArbitraryExpression" xml:space="preserve">
440+
<value>'ExecuteUpdate' cannot currently set a property in a JSON column to arbitrary expressions; only constants, parameters and other JSON properties are supported; see https://github.com/dotnet/efcore/issues/36688.</value>
441+
</data>
436442
<data name="ExecuteUpdateDeleteOnEntityNotMappedToTable" xml:space="preserve">
437443
<value>'ExecuteUpdate' or 'ExecuteDelete' was called on entity type '{entityType}', but that entity type is not mapped to a table.</value>
438444
</data>

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

Lines changed: 132 additions & 40 deletions
Large diffs are not rendered by default.

src/EFCore.Sqlite.Core/Properties/SqliteStrings.Designer.cs

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Sqlite.Core/Properties/SqliteStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@
123123
<data name="ApplyNotSupported" xml:space="preserve">
124124
<value>Translating this query requires the SQL APPLY operation, which is not supported on SQLite.</value>
125125
</data>
126+
<data name="ExecuteUpdateJsonPartialUpdateDoesNotSupportUlong" xml:space="preserve">
127+
<value>ExecuteUpdate partial updates of ulong properties within JSON columns is not supported.</value>
128+
</data>
126129
<data name="DefaultNotSupported" xml:space="preserve">
127130
<value>Translating this operation requires the 'DEFAULT' keyword, which is not supported on SQLite.</value>
128131
</data>

src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,52 @@ [new PathSegment(translatedIndex)],
580580
}
581581
}
582582

583+
/// <summary>
584+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
585+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
586+
/// any release. You should only use it directly in your code with extreme caution and knowing that
587+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
588+
/// </summary>
589+
protected override SqlExpression SerializeScalarToJson(SqlExpression value)
590+
{
591+
var providerClrType = (value.TypeMapping!.Converter?.ProviderClrType ?? value.Type).UnwrapNullableType();
592+
593+
// SQLite has no bool type, so if we simply sent the bool as-is, we'd get 1/0 in the JSON document.
594+
// To get an actual unquoted true/false value, we pass "true"/"false" string through the json() minifier, which does this.
595+
// See https://sqlite.org/forum/info/91d09974c3754ea6.
596+
if (providerClrType == typeof(bool))
597+
{
598+
return _sqlExpressionFactory.Function(
599+
"json",
600+
[
601+
value is SqlConstantExpression { Value: bool constant }
602+
? _sqlExpressionFactory.Constant(constant ? "true" : "false")
603+
: _sqlExpressionFactory.Case(
604+
[
605+
new CaseWhenClause(
606+
_sqlExpressionFactory.Equal(value, _sqlExpressionFactory.Constant(true)),
607+
_sqlExpressionFactory.Constant("true")),
608+
new CaseWhenClause(
609+
_sqlExpressionFactory.Equal(value, _sqlExpressionFactory.Constant(false)),
610+
_sqlExpressionFactory.Constant("false"))
611+
],
612+
elseResult: _sqlExpressionFactory.Constant("null"))
613+
],
614+
nullable: true,
615+
argumentsPropagateNullability: [true],
616+
typeof(string),
617+
_typeMappingSource.FindMapping(typeof(string)));
618+
}
619+
620+
if (providerClrType == typeof(ulong))
621+
{
622+
// See #36689
623+
throw new InvalidOperationException(SqliteStrings.ExecuteUpdateJsonPartialUpdateDoesNotSupportUlong);
624+
}
625+
626+
return base.SerializeScalarToJson(value);
627+
}
628+
583629
/// <summary>
584630
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
585631
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore;
5+
6+
public abstract class StoreTypeRelationalTestBase : StoreTypeTestBase
7+
{
8+
protected override async Task TestType<T>(
9+
T value,
10+
T otherValue,
11+
ContextFactory<DbContext> contextFactory,
12+
Func<T, T, bool> comparer)
13+
{
14+
await base.TestType<T>(value, otherValue, contextFactory, comparer);
15+
16+
// Extra test scenarios for relational
17+
await TestExecuteUpdateWithinJsonToParameter(contextFactory, value, otherValue, comparer);
18+
await TestExecuteUpdateWithinJsonToConstant(contextFactory, value, otherValue, comparer);
19+
await TestExecuteUpdateWithinJsonToJsonProperty(contextFactory, value, otherValue, comparer);
20+
await TestExecuteUpdateWithinJsonToNonJsonColumn(contextFactory, value, otherValue, comparer);
21+
}
22+
23+
protected virtual async Task TestExecuteUpdateWithinJsonToParameter<T>(
24+
ContextFactory<DbContext> contextFactory,
25+
T value,
26+
T otherValue,
27+
Func<T, T, bool> comparer)
28+
=> await TestHelpers.ExecuteWithStrategyInTransactionAsync(
29+
contextFactory.CreateContext,
30+
UseTransaction,
31+
async context =>
32+
{
33+
await context.Set<StoreTypeEntity<T>>().ExecuteUpdateAsync(s => s.SetProperty(e => e.Container.Value, e => otherValue));
34+
var result = await context.Set<StoreTypeEntity<T>>().Where(e => e.Id == 1).SingleAsync();
35+
Assert.Equal(otherValue, result.Container.Value, comparer);
36+
});
37+
38+
protected virtual async Task TestExecuteUpdateWithinJsonToConstant<T>(
39+
ContextFactory<DbContext> contextFactory,
40+
T value,
41+
T otherValue,
42+
Func<T, T, bool> comparer)
43+
=> await TestHelpers.ExecuteWithStrategyInTransactionAsync(
44+
contextFactory.CreateContext,
45+
UseTransaction,
46+
async context =>
47+
{
48+
var parameter = Expression.Parameter(typeof(StoreTypeEntity<T>));
49+
var valueExpression = Expression.Lambda<Func<StoreTypeEntity<T>, T>>(
50+
Expression.Constant(otherValue, typeof(T)),
51+
parameter);
52+
53+
await context.Set<StoreTypeEntity<T>>().ExecuteUpdateAsync(s => s.SetProperty(e => e.Container.Value, valueExpression));
54+
var result = await context.Set<StoreTypeEntity<T>>().Where(e => e.Id == 1).SingleAsync();
55+
Assert.Equal(otherValue, result.Container.Value, comparer);
56+
});
57+
58+
protected virtual async Task TestExecuteUpdateWithinJsonToJsonProperty<T>(
59+
ContextFactory<DbContext> contextFactory,
60+
T value,
61+
T otherValue,
62+
Func<T, T, bool> comparer)
63+
=> await TestHelpers.ExecuteWithStrategyInTransactionAsync(
64+
contextFactory.CreateContext,
65+
UseTransaction,
66+
async context =>
67+
{
68+
await context.Set<StoreTypeEntity<T>>().ExecuteUpdateAsync(s => s.SetProperty(e => e.Container.Value, e => e.Container.OtherValue));
69+
var result = await context.Set<StoreTypeEntity<T>>().Where(e => e.Id == 1).SingleAsync();
70+
Assert.Equal(otherValue, result.Container.Value, comparer);
71+
});
72+
73+
protected virtual async Task TestExecuteUpdateWithinJsonToNonJsonColumn<T>(
74+
ContextFactory<DbContext> contextFactory,
75+
T value,
76+
T otherValue,
77+
Func<T, T, bool> comparer)
78+
=> await TestHelpers.ExecuteWithStrategyInTransactionAsync(
79+
contextFactory.CreateContext,
80+
UseTransaction,
81+
async context =>
82+
{
83+
Func<Task> testAction = async () =>
84+
{
85+
await context.Set<StoreTypeEntity<T>>().ExecuteUpdateAsync(s => s.SetProperty(e => e.Container.Value, e => e.OtherValue));
86+
var result = await context.Set<StoreTypeEntity<T>>().Where(e => e.Id == 1).SingleAsync();
87+
Assert.Equal(otherValue, result.Container.Value, comparer);
88+
};
89+
90+
if (typeof(T) == typeof(string)
91+
|| typeof(T) == typeof(bool)
92+
|| typeof(T).IsNumeric())
93+
{
94+
await testAction();
95+
}
96+
else
97+
{
98+
// See #36688 for supporting this for relational types other than string/numeric/bool
99+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(testAction);
100+
Assert.Equal(RelationalStrings.ExecuteUpdateCannotSetJsonPropertyToNonJsonColumn, exception.Message);
101+
}
102+
});
103+
104+
public override void OnModelCreating<T>(ModelBuilder modelBuilder)
105+
{
106+
base.OnModelCreating<T>(modelBuilder);
107+
108+
modelBuilder.Entity<StoreTypeEntity<T>>(b =>
109+
{
110+
b.ToTable("StoreTypeEntity");
111+
b.ComplexProperty(e => e.Container, cb => cb.ToJson());
112+
});
113+
}
114+
115+
public override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
116+
=> facade.UseTransaction(transaction.GetDbTransaction());
117+
}

test/EFCore.Specification.Tests/NonSharedModelTestBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ protected NonSharedModelTestBase()
3737
{
3838
}
3939

40-
protected NonSharedModelTestBase(NonSharedFixture fixture)
40+
protected NonSharedModelTestBase(NonSharedFixture? fixture)
4141
=> Fixture = fixture;
4242

4343
public virtual Task InitializeAsync()

0 commit comments

Comments
 (0)