Skip to content

Commit 78f22f8

Browse files
committed
Property encode JSON scalars in partial updates
And introduce new store type tests
1 parent 6a5ad0d commit 78f22f8

File tree

13 files changed

+705
-41
lines changed

13 files changed

+705
-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: 147 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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,57 @@ [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 bool TrySerializeScalarToJson(
590+
JsonScalarExpression target,
591+
SqlExpression value,
592+
[NotNullWhen(true)] out SqlExpression? jsonValue)
593+
{
594+
var providerClrType = (value.TypeMapping!.Converter?.ProviderClrType ?? value.Type).UnwrapNullableType();
595+
596+
// SQLite has no bool type, so if we simply sent the bool as-is, we'd get 1/0 in the JSON document.
597+
// To get an actual unquoted true/false value, we pass "true"/"false" string through the json() minifier, which does this.
598+
// See https://sqlite.org/forum/info/91d09974c3754ea6.
599+
if (providerClrType == typeof(bool))
600+
{
601+
jsonValue = _sqlExpressionFactory.Function(
602+
"json",
603+
[
604+
value is SqlConstantExpression { Value: bool constant }
605+
? _sqlExpressionFactory.Constant(constant ? "true" : "false")
606+
: _sqlExpressionFactory.Case(
607+
[
608+
new CaseWhenClause(
609+
_sqlExpressionFactory.Equal(value, _sqlExpressionFactory.Constant(true)),
610+
_sqlExpressionFactory.Constant("true")),
611+
new CaseWhenClause(
612+
_sqlExpressionFactory.Equal(value, _sqlExpressionFactory.Constant(false)),
613+
_sqlExpressionFactory.Constant("false"))
614+
],
615+
elseResult: _sqlExpressionFactory.Constant("null"))
616+
],
617+
nullable: true,
618+
argumentsPropagateNullability: [true],
619+
typeof(string),
620+
_typeMappingSource.FindMapping(typeof(string)));
621+
622+
return true;
623+
}
624+
625+
if (providerClrType == typeof(ulong))
626+
{
627+
// See #36689
628+
throw new InvalidOperationException(SqliteStrings.ExecuteUpdateJsonPartialUpdateDoesNotSupportUlong);
629+
}
630+
631+
return base.TrySerializeScalarToJson(target, value, out jsonValue);
632+
}
633+
583634
/// <summary>
584635
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
585636
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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 class StoreTypeCosmosTest : StoreTypeTestBase
7+
{
8+
// #34395
9+
public override Task ULong() => Task.CompletedTask;
10+
11+
protected override ITestStoreFactory TestStoreFactory => CosmosTestStoreFactory.Instance;
12+
13+
protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
14+
=> base.AddOptions(builder)
15+
.ConfigureWarnings(o => o.Ignore(CosmosEventId.NoPartitionKeyDefined));
16+
}

test/EFCore.InMemory.FunctionalTests/InMemoryComplianceTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class InMemoryComplianceTest : ComplianceTestBase
2929
typeof(NorthwindBulkUpdatesTestBase<>),
3030
typeof(JsonQueryTestBase<>),
3131
typeof(AdHocJsonQueryTestBase),
32+
typeof(StoreTypeTestBase),
3233

3334
// Relationships tests - not implemented for InMemory
3435
typeof(AssociationsCollectionTestBase<>),
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
await context.Set<StoreTypeEntity<T>>().ExecuteUpdateAsync(s => s.SetProperty(e => e.Container.Value, e => e.OtherValue));
84+
var result = await context.Set<StoreTypeEntity<T>>().Where(e => e.Id == 1).SingleAsync();
85+
Assert.Equal(otherValue, result.Container.Value, comparer);
86+
});
87+
88+
public override void OnModelCreating<T>(ModelBuilder modelBuilder)
89+
{
90+
base.OnModelCreating<T>(modelBuilder);
91+
92+
modelBuilder.Entity<StoreTypeEntity<T>>(b =>
93+
{
94+
b.ToTable("StoreTypeEntity");
95+
b.ComplexProperty(e => e.Container, cb => cb.ToJson());
96+
});
97+
}
98+
99+
public override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
100+
=> facade.UseTransaction(transaction.GetDbTransaction());
101+
}

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)