Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[release/7.0] Fix to #29355 - FromSqlRaw throws Exception when querying all objects that contain a certain string property in a json array column #29366

Merged
merged 1 commit into from
Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 54 additions & 46 deletions src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -440,52 +440,7 @@ void GenerateNonHierarchyNonSplittingEntityType(ITableBase table, TableExpressio
}

var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions);

foreach (var ownedJsonNavigation in GetAllNavigationsInHierarchy(entityType)
.Where(
n => n.ForeignKey.IsOwnership
&& n.TargetEntityType.IsMappedToJson()
&& n.ForeignKey.PrincipalToDependent == n))
{
var targetEntityType = ownedJsonNavigation.TargetEntityType;
var jsonColumnName = targetEntityType.GetContainerColumnName()!;
var jsonColumnTypeMapping = targetEntityType.GetContainerColumnTypeMapping()!;

var jsonColumn = new ConcreteColumnExpression(
jsonColumnName,
tableReferenceExpression,
jsonColumnTypeMapping.ClrType,
jsonColumnTypeMapping,
nullable: !ownedJsonNavigation.ForeignKey.IsRequiredDependent || ownedJsonNavigation.IsCollection);

// for json collections we need to skip ordinal key (which is always the last one)
// simple copy from parent is safe here, because we only do it at top level
// so there is no danger of multiple keys being synthesized (like we have in multi-level nav chains)
var keyPropertiesMap = new Dictionary<IProperty, ColumnExpression>();
var keyProperties = targetEntityType.FindPrimaryKey()!.Properties;
var keyPropertiesCount = ownedJsonNavigation.IsCollection
? keyProperties.Count - 1
: keyProperties.Count;

for (var i = 0; i < keyPropertiesCount; i++)
{
var correspondingParentKeyProperty = ownedJsonNavigation.ForeignKey.PrincipalKey.Properties[i];
keyPropertiesMap[keyProperties[i]] = propertyExpressions[correspondingParentKeyProperty];
}

var entityShaperExpression = new RelationalEntityShaperExpression(
targetEntityType,
new JsonQueryExpression(
targetEntityType,
jsonColumn,
keyPropertiesMap,
ownedJsonNavigation.ClrType,
ownedJsonNavigation.IsCollection),
!ownedJsonNavigation.ForeignKey.IsRequiredDependent);

entityProjection.AddNavigationBinding(ownedJsonNavigation, entityShaperExpression);
}

AddJsonNavigationBindings(entityType, entityProjection, propertyExpressions, tableReferenceExpression);
_projectionMapping[new ProjectionMember()] = entityProjection;

var primaryKey = entityType.FindPrimaryKey();
Expand Down Expand Up @@ -522,6 +477,7 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
}

var entityProjection = new EntityProjectionExpression(entityType, propertyExpressions);
AddJsonNavigationBindings(entityType, entityProjection, propertyExpressions, tableReferenceExpression);
_projectionMapping[new ProjectionMember()] = entityProjection;

var primaryKey = entityType.FindPrimaryKey();
Expand All @@ -534,6 +490,58 @@ internal SelectExpression(IEntityType entityType, TableExpressionBase tableExpre
}
}

private void AddJsonNavigationBindings(
IEntityType entityType,
EntityProjectionExpression entityProjection,
Dictionary<IProperty, ColumnExpression> propertyExpressions,
TableReferenceExpression tableReferenceExpression)
{
foreach (var ownedJsonNavigation in GetAllNavigationsInHierarchy(entityType)
.Where(
n => n.ForeignKey.IsOwnership
&& n.TargetEntityType.IsMappedToJson()
&& n.ForeignKey.PrincipalToDependent == n))
{
var targetEntityType = ownedJsonNavigation.TargetEntityType;
var jsonColumnName = targetEntityType.GetContainerColumnName()!;
var jsonColumnTypeMapping = targetEntityType.GetContainerColumnTypeMapping()!;

var jsonColumn = new ConcreteColumnExpression(
jsonColumnName,
tableReferenceExpression,
jsonColumnTypeMapping.ClrType,
jsonColumnTypeMapping,
nullable: !ownedJsonNavigation.ForeignKey.IsRequiredDependent || ownedJsonNavigation.IsCollection);

// for json collections we need to skip ordinal key (which is always the last one)
// simple copy from parent is safe here, because we only do it at top level
// so there is no danger of multiple keys being synthesized (like we have in multi-level nav chains)
var keyPropertiesMap = new Dictionary<IProperty, ColumnExpression>();
var keyProperties = targetEntityType.FindPrimaryKey()!.Properties;
var keyPropertiesCount = ownedJsonNavigation.IsCollection
? keyProperties.Count - 1
: keyProperties.Count;

for (var i = 0; i < keyPropertiesCount; i++)
{
var correspondingParentKeyProperty = ownedJsonNavigation.ForeignKey.PrincipalKey.Properties[i];
keyPropertiesMap[keyProperties[i]] = propertyExpressions[correspondingParentKeyProperty];
}

var entityShaperExpression = new RelationalEntityShaperExpression(
targetEntityType,
new JsonQueryExpression(
targetEntityType,
jsonColumn,
keyPropertiesMap,
ownedJsonNavigation.ClrType,
ownedJsonNavigation.IsCollection),
!ownedJsonNavigation.ForeignKey.IsRequiredDependent);

entityProjection.AddNavigationBinding(ownedJsonNavigation, entityShaperExpression);
}
}

/// <summary>
/// The list of tags applied to this <see cref="SelectExpression" />.
/// </summary>
Expand Down
180 changes: 180 additions & 0 deletions test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.TestModels.JsonQuery;

namespace Microsoft.EntityFrameworkCore.Query;

public class JsonQuerySqlServerTest : JsonQueryTestBase<JsonQuerySqlServerFixture>
Expand Down Expand Up @@ -838,6 +841,183 @@ FROM [JsonEntitiesAllTypes] AS [j]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_on_entity_with_json_basic(bool async)
{
await AssertQuery(
async,
ss => ((DbSet<JsonEntityBasic>)ss.Set<JsonEntityBasic>()).FromSqlRaw(
Fixture.TestStore.NormalizeDelimitersInRawString("SELECT * FROM [JsonEntitiesBasic] AS j")),
ss => ss.Set<JsonEntityBasic>(),
entryCount: 40);

AssertSql(
"""
SELECT [m].[Id], [m].[EntityBasicId], [m].[Name], JSON_QUERY([m].[OwnedCollectionRoot],'$'), JSON_QUERY([m].[OwnedReferenceRoot],'$')
FROM (
SELECT * FROM "JsonEntitiesBasic" AS j
) AS [m]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSqlInterpolated_on_entity_with_json_with_predicate(bool async)
{
var parameter = new SqlParameter { ParameterName = "prm", Value = 1 };
await AssertQuery(
async,
ss => ((DbSet<JsonEntityBasic>)ss.Set<JsonEntityBasic>()).FromSql(
Fixture.TestStore.NormalizeDelimitersInInterpolatedString($"SELECT * FROM [JsonEntitiesBasic] AS j WHERE [j].[Id] = {parameter}")),
ss => ss.Set<JsonEntityBasic>(),
entryCount: 40);

AssertSql(
"""
prm='1'

SELECT [m].[Id], [m].[EntityBasicId], [m].[Name], JSON_QUERY([m].[OwnedCollectionRoot],'$'), JSON_QUERY([m].[OwnedReferenceRoot],'$')
FROM (
SELECT * FROM "JsonEntitiesBasic" AS j WHERE "j"."Id" = @prm
) AS [m]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_on_entity_with_json_project_json_reference(bool async)
{
await AssertQuery(
async,
ss => ((DbSet<JsonEntityBasic>)ss.Set<JsonEntityBasic>()).FromSqlRaw(
Fixture.TestStore.NormalizeDelimitersInRawString("SELECT * FROM [JsonEntitiesBasic] AS j"))
.AsNoTracking()
.Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch),
ss => ss.Set<JsonEntityBasic>().Select(x => x.OwnedReferenceRoot.OwnedReferenceBranch));

AssertSql(
"""
SELECT JSON_QUERY([m].[OwnedReferenceRoot],'$.OwnedReferenceBranch'), [m].[Id]
FROM (
SELECT * FROM "JsonEntitiesBasic" AS j
) AS [m]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_on_entity_with_json_project_json_collection(bool async)
{
await AssertQuery(
async,
ss => ((DbSet<JsonEntityBasic>)ss.Set<JsonEntityBasic>()).FromSqlRaw(
Fixture.TestStore.NormalizeDelimitersInRawString("SELECT * FROM [JsonEntitiesBasic] AS j"))
.AsNoTracking()
.Select(x => x.OwnedReferenceRoot.OwnedCollectionBranch),
ss => ss.Set<JsonEntityBasic>().Select(x => x.OwnedReferenceRoot.OwnedCollectionBranch),
elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => (ee.Date, ee.Enum, ee.Fraction)));

AssertSql(
"""
SELECT JSON_QUERY([m].[OwnedReferenceRoot],'$.OwnedCollectionBranch'), [m].[Id]
FROM (
SELECT * FROM "JsonEntitiesBasic" AS j
) AS [m]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_on_entity_with_json_inheritance_on_base(bool async)
{
await AssertQuery(
async,
ss => ((DbSet<JsonEntityInheritanceBase>)ss.Set<JsonEntityInheritanceBase>()).FromSqlRaw(
Fixture.TestStore.NormalizeDelimitersInRawString("SELECT * FROM [JsonEntitiesInheritance] AS j")),
ss => ss.Set<JsonEntityInheritanceBase>(),
entryCount: 38);

AssertSql(
"""
SELECT [m].[Id], [m].[Discriminator], [m].[Name], [m].[Fraction], JSON_QUERY([m].[CollectionOnBase],'$'), JSON_QUERY([m].[ReferenceOnBase],'$'), JSON_QUERY([m].[CollectionOnDerived],'$'), JSON_QUERY([m].[ReferenceOnDerived],'$')
FROM (
SELECT * FROM "JsonEntitiesInheritance" AS j
) AS [m]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_on_entity_with_json_inheritance_on_derived(bool async)
{
await AssertQuery(
async,
ss => ((DbSet<JsonEntityInheritanceDerived>)ss.Set<JsonEntityInheritanceDerived>()).FromSqlRaw(
Fixture.TestStore.NormalizeDelimitersInRawString("SELECT * FROM [JsonEntitiesInheritance] AS j")),
ss => ss.Set<JsonEntityInheritanceDerived>(),
entryCount: 25);

AssertSql(
"""
SELECT [m].[Id], [m].[Discriminator], [m].[Name], [m].[Fraction], JSON_QUERY([m].[CollectionOnBase],'$'), JSON_QUERY([m].[ReferenceOnBase],'$'), JSON_QUERY([m].[CollectionOnDerived],'$'), JSON_QUERY([m].[ReferenceOnDerived],'$')
FROM (
SELECT * FROM "JsonEntitiesInheritance" AS j
) AS [m]
WHERE [m].[Discriminator] = N'JsonEntityInheritanceDerived'
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_on_entity_with_json_inheritance_project_reference_on_base(bool async)
{
await AssertQuery(
async,
ss => ((DbSet<JsonEntityInheritanceBase>)ss.Set<JsonEntityInheritanceBase>()).FromSqlRaw(
Fixture.TestStore.NormalizeDelimitersInRawString("SELECT * FROM [JsonEntitiesInheritance] AS j"))
.AsNoTracking()
.OrderBy(x => x.Id)
.Select(x => x.ReferenceOnBase),
ss => ss.Set<JsonEntityInheritanceBase>().OrderBy(x => x.Id).Select(x => x.ReferenceOnBase),
assertOrder: true);

AssertSql(
"""
SELECT JSON_QUERY([m].[ReferenceOnBase],'$'), [m].[Id]
FROM (
SELECT * FROM "JsonEntitiesInheritance" AS j
) AS [m]
ORDER BY [m].[Id]
""");
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task FromSql_on_entity_with_json_inheritance_project_reference_on_derived(bool async)
{
await AssertQuery(
async,
ss => ((DbSet<JsonEntityInheritanceDerived>)ss.Set<JsonEntityInheritanceDerived>()).FromSqlRaw(
Fixture.TestStore.NormalizeDelimitersInRawString("SELECT * FROM [JsonEntitiesInheritance] AS j"))
.AsNoTracking()
.OrderBy(x => x.Id)
.Select(x => x.CollectionOnDerived),
ss => ss.Set<JsonEntityInheritanceDerived>().OrderBy(x => x.Id).Select(x => x.CollectionOnDerived),
elementAsserter: (e, a) => AssertCollection(e, a, elementSorter: ee => (ee.Date, ee.Enum, ee.Fraction)),
assertOrder: true);

AssertSql(
"""
SELECT JSON_QUERY([m].[CollectionOnDerived],'$'), [m].[Id]
FROM (
SELECT * FROM "JsonEntitiesInheritance" AS j
) AS [m]
WHERE [m].[Discriminator] = N'JsonEntityInheritanceDerived'
ORDER BY [m].[Id]
""");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}