Skip to content

Commit 7deeb21

Browse files
authored
Correct parentheses logic for SQLite JsonScalarExpression (#30908)
Fixes #30886
1 parent d811023 commit 7deeb21

File tree

6 files changed

+72
-14
lines changed

6 files changed

+72
-14
lines changed

src/EFCore.Relational/Query/QuerySqlGenerator.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1533,7 +1533,7 @@ protected virtual bool RequiresParentheses(SqlExpression outerExpression, SqlExp
15331533
return true;
15341534
}
15351535

1536-
case CollateExpression or LikeExpression or AtTimeZoneExpression:
1536+
case CollateExpression or LikeExpression or AtTimeZoneExpression or JsonScalarExpression:
15371537
return !TryGetOperatorInfo(outerExpression, out outerPrecedence, out _)
15381538
|| !TryGetOperatorInfo(innerExpression, out innerPrecedence, out _)
15391539
|| outerPrecedence >= innerPrecedence;

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

+3
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,9 @@ protected override bool TryGetOperatorInfo(SqlExpression expression, out int pre
591591
LikeExpression => (350, false),
592592
AtTimeZoneExpression => (1200, false),
593593

594+
// On SQL Server, JsonScalarExpression renders as a function (JSON_VALUE()), so there's never a need for parentheses.
595+
JsonScalarExpression => (9999, false),
596+
594597
_ => default,
595598
};
596599

test/EFCore.Relational.Specification.Tests/Query/OperatorsQueryTestBase.cs

+35-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ from e4 in context.Set<OperatorEntityLong>()
169169
from e5 in context.Set<OperatorEntityLong>()
170170
orderby e3.Id, e4.Id, e5.Id
171171
select ((~(-(-((e5.Value + e3.Value) + 2))) % (-(e4.Value + e4.Value) - e3.Value)))).ToList();
172-
172+
173173
Assert.Equal(expected.Count, actual.Count);
174174
for (var i = 0; i < expected.Count; i++)
175175
{
@@ -267,4 +267,38 @@ public virtual async Task Negate_on_like_expression(bool async)
267267
Assert.Equal(expected[i], actual[i]);
268268
}
269269
}
270+
271+
#nullable enable
272+
[ConditionalTheory]
273+
[MemberData(nameof(IsAsyncData))]
274+
public virtual async Task Concat_and_json_scalar(bool async)
275+
{
276+
var contextFactory = await InitializeAsync<DbContext>(
277+
onModelCreating: mb => mb
278+
.Entity<Owner>()
279+
.OwnsOne(o => o.Owned)
280+
.ToJson(),
281+
seed: context =>
282+
{
283+
context.Set<Owner>().AddRange(
284+
new Owner { Owned = new() { SomeProperty = "Bar" } },
285+
new Owner { Owned = new() { SomeProperty = "Baz" } });
286+
context.SaveChanges();
287+
});
288+
await using var context = contextFactory.CreateContext();
289+
290+
var result = await context.Set<Owner>().SingleAsync(o => "Foo" + o.Owned.SomeProperty == "FooBar");
291+
Assert.Equal("Bar", result.Owned.SomeProperty);
292+
}
293+
294+
class Owner
295+
{
296+
public int Id { get; set; }
297+
public Owned Owned { get; set; } = null!;
298+
}
299+
300+
class Owned
301+
{
302+
public string SomeProperty { get; set; } = "";
303+
}
270304
}

test/EFCore.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryTestBase.cs

+8-12
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,10 @@ public virtual async Task Column_with_custom_converter()
131131
public virtual async Task Parameter_with_inferred_value_converter()
132132
{
133133
var contextFactory = await InitializeAsync<TestContext>(
134-
onModelCreating: mb => mb.Entity<TestEntity>(
135-
b =>
136-
{
137-
b.Property<IntWrapper>("PropertyWithValueConverter")
138-
.HasConversion(w => w.Value, i => new IntWrapper(i));
139-
}),
134+
onModelCreating: mb => mb
135+
.Entity<TestEntity>()
136+
.Property<IntWrapper>("PropertyWithValueConverter")
137+
.HasConversion(w => w.Value, i => new IntWrapper(i)),
140138
seed: context =>
141139
{
142140
var entry1 = context.Add(new TestEntity { Id = 1 });
@@ -158,12 +156,10 @@ public virtual async Task Parameter_with_inferred_value_converter()
158156
public virtual async Task Constant_with_inferred_value_converter()
159157
{
160158
var contextFactory = await InitializeAsync<TestContext>(
161-
onModelCreating: mb => mb.Entity<TestEntity>(
162-
b =>
163-
{
164-
b.Property<IntWrapper>("PropertyWithValueConverter")
165-
.HasConversion(w => w.Value, i => new IntWrapper(i));
166-
}),
159+
onModelCreating: mb => mb
160+
.Entity<TestEntity>()
161+
.Property<IntWrapper>("PropertyWithValueConverter")
162+
.HasConversion(w => w.Value, i => new IntWrapper(i)),
167163
seed: context =>
168164
{
169165
var entry1 = context.Add(new TestEntity { Id = 1 });

test/EFCore.SqlServer.FunctionalTests/Query/OperatorsQuerySqlServerTest.cs

+12
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ WHERE [o].[Value] IS NOT NULL AND NOT ([o].[Value] LIKE N'A%')
128128
""");
129129
}
130130

131+
public override async Task Concat_and_json_scalar(bool async)
132+
{
133+
await base.Concat_and_json_scalar(async);
134+
135+
AssertSql(
136+
"""
137+
SELECT TOP(2) [o].[Id], [o].[Owned]
138+
FROM [Owner] AS [o]
139+
WHERE N'Foo' + JSON_VALUE([o].[Owned], '$.SomeProperty') = N'FooBar'
140+
""");
141+
}
142+
131143
[ConditionalTheory]
132144
[MemberData(nameof(IsAsyncData))]
133145
public virtual async Task Where_AtTimeZone_datetimeoffset_constant(bool async)

test/EFCore.Sqlite.FunctionalTests/Query/OperatorsQuerySqliteTest.cs

+13
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ public override async Task Negate_on_like_expression(bool async)
117117
SELECT "o"."Id"
118118
FROM "OperatorEntityString" AS "o"
119119
WHERE "o"."Value" IS NOT NULL AND NOT ("o"."Value" LIKE 'A%')
120+
""");
121+
}
122+
123+
public override async Task Concat_and_json_scalar(bool async)
124+
{
125+
await base.Concat_and_json_scalar(async);
126+
127+
AssertSql(
128+
"""
129+
SELECT "o"."Id", "o"."Owned"
130+
FROM "Owner" AS "o"
131+
WHERE 'Foo' || ("o"."Owned" ->> 'SomeProperty') = 'FooBar'
132+
LIMIT 2
120133
""");
121134
}
122135
}

0 commit comments

Comments
 (0)