Skip to content

Commit d12d53f

Browse files
committed
Fix to #32911 - ComplexProperty with AsSplitQuery
Problem was that when we lift structural type projection during pushdown, if that projection contains complex types with the same column names (e.g. cross join of same entities - it's perfectly fine to do in a vacuum, we just alias the columns whose names are repeated) we would lift the projection incorrectly. What we do is go through all the properties, apply the corresponding columns to the selectExpression if needed and generate StructuralTypeProjection object if the projection needs to be applied one level up. For complex types we would generate a shaper expression and then run it through the same process, BUT the nested complex properties would be added to a flat structure along with the primitive properties, rather than in separate cache dedicated for complex property shapers. This was wrong and not what we expected to see, when processing this structure one level up (i.e. when applying projection to the outer select) SELECT [applying_this_projection_was_wrong] FROM ( SELECT c.Id, c.Name, c.ComplexProp, o.ComplexProp as ComplexProp0 FROM Customers as c JOIN Orders as o ON ... ) as s i.e. applying projection once worked fine, but doing it second time did not. The reason why is that we expected to see information about the complex type shape in the complex property shaper cache, rather than flat structure for primitives, but it wasn't there. So we assumed this is the first time we the projection is being applied, so we conjure up the complex type shaper based on table alias and IColumn metadata. This results in a situation, where complex property that was aliased is never picked. So we end up with: SELECT s.Id, s.Name, s.ComplexProp -- we would also try to add s.ComplexProp again, instead of s.ComplexProp0 but of course we don't add same thing twice FROM ( SELECT c.Id, c.Name, c.ComplexProp, o.ComplexProp as ComplexProp0 FROM Customers as c JOIN Orders as o ON ... ) as s This leads to bad data - two different objects with distinct data in them are mapped to the same column in the database. Fix is to property build a complex type shaper structure when applying projection instead, so the structure we generate matches expectations. Fixes #32911
1 parent 229d1f3 commit d12d53f

File tree

8 files changed

+901
-75
lines changed

8 files changed

+901
-75
lines changed

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs

Lines changed: 92 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,20 +2077,21 @@ void HandleStructuralTypeProjection(
20772077
projection1.StructuralType.DisplayName(), projection2.StructuralType.DisplayName()));
20782078
}
20792079

2080-
var propertyExpressions = new Dictionary<IProperty, ColumnExpression>();
2081-
2082-
ProcessStructuralType(projection1, projection2);
2080+
var resultProjection = ProcessStructuralType(projection1, projection2);
2081+
_projectionMapping[projectionMember] = resultProjection;
20832082

2084-
void ProcessStructuralType(
2085-
StructuralTypeProjectionExpression nestedProjection1,
2086-
StructuralTypeProjectionExpression nestedProjection2)
2083+
StructuralTypeProjectionExpression ProcessStructuralType(
2084+
StructuralTypeProjectionExpression structuralProjection1,
2085+
StructuralTypeProjectionExpression structuralProjection2)
20872086
{
2088-
var type = nestedProjection1.StructuralType;
2087+
var propertyExpressions = new Dictionary<IProperty, ColumnExpression>();
2088+
var complexPropertyCache = new Dictionary<IComplexProperty, StructuralTypeShaperExpression>();
2089+
var type = structuralProjection1.StructuralType;
20892090

20902091
foreach (var property in type.GetAllPropertiesInHierarchy())
20912092
{
2092-
var column1 = nestedProjection1.BindProperty(property);
2093-
var column2 = nestedProjection2.BindProperty(property);
2093+
var column1 = structuralProjection1.BindProperty(property);
2094+
var column2 = structuralProjection2.BindProperty(property);
20942095
var alias = GenerateUniqueColumnAlias(column1.Name);
20952096
var innerProjection = new ProjectionExpression(column1, alias);
20962097
select1._projection.Add(innerProjection);
@@ -2127,7 +2128,7 @@ void ProcessStructuralType(
21272128
// If the top-level projection - not the current nested one - is a complex type and not an entity type, then add
21282129
// all its columns to the "otherExpressions" list (i.e. columns not part of a an entity primary key). This is
21292130
// the same as with a non-structural type projection.
2130-
else if (projection1.StructuralType is IComplexType)
2131+
else if (structuralProjection1.StructuralType is IComplexType)
21312132
{
21322133
var outerTypeMapping = column1.TypeMapping ?? column1.TypeMapping;
21332134
if (outerTypeMapping == null)
@@ -2141,52 +2142,68 @@ void ProcessStructuralType(
21412142
}
21422143
}
21432144

2144-
foreach (var complexProperty in GetAllComplexPropertiesInHierarchy(nestedProjection1.StructuralType))
2145+
foreach (var complexProperty in GetAllComplexPropertiesInHierarchy(structuralProjection1.StructuralType))
21452146
{
2146-
ProcessStructuralType(
2147-
(StructuralTypeProjectionExpression)nestedProjection1.BindComplexProperty(complexProperty).ValueBufferExpression,
2148-
(StructuralTypeProjectionExpression)nestedProjection2.BindComplexProperty(complexProperty).ValueBufferExpression);
2149-
}
2150-
}
2147+
var complexPropertyShaper1 = structuralProjection1.BindComplexProperty(complexProperty);
2148+
var complexPropertyShaper2 = structuralProjection2.BindComplexProperty(complexProperty);
21512149

2152-
Check.DebugAssert(
2153-
projection1.TableMap.Count == projection2.TableMap.Count,
2154-
"Set operation over entity projections with different table map counts");
2155-
Check.DebugAssert(
2156-
projection1.TableMap.Keys.All(t => projection2.TableMap.ContainsKey(t)),
2157-
"Set operation over entity projections with table map discrepancy");
2150+
var resultComplexProjection = ProcessStructuralType(
2151+
(StructuralTypeProjectionExpression)complexPropertyShaper1.ValueBufferExpression,
2152+
(StructuralTypeProjectionExpression)complexPropertyShaper2.ValueBufferExpression);
21582153

2159-
var tableMap = projection1.TableMap.ToDictionary(kvp => kvp.Key, _ => setOperationAlias);
2154+
var resultComplexShaper = new RelationalStructuralTypeShaperExpression(
2155+
complexProperty.ComplexType,
2156+
resultComplexProjection,
2157+
resultComplexProjection.IsNullable);
21602158

2161-
var discriminatorExpression = projection1.DiscriminatorExpression;
2162-
if (projection1.DiscriminatorExpression != null
2163-
&& projection2.DiscriminatorExpression != null)
2164-
{
2165-
var alias = GenerateUniqueColumnAlias(DiscriminatorColumnAlias);
2166-
var innerProjection = new ProjectionExpression(projection1.DiscriminatorExpression, alias);
2167-
select1._projection.Add(innerProjection);
2168-
select2._projection.Add(new ProjectionExpression(projection2.DiscriminatorExpression, alias));
2169-
discriminatorExpression = CreateColumnExpression(innerProjection, setOperationAlias);
2170-
}
2159+
complexPropertyCache[complexProperty] = resultComplexShaper;
2160+
}
21712161

2172-
var outerProjection = new StructuralTypeProjectionExpression(
2173-
projection1.StructuralType, propertyExpressions, tableMap, nullable: false, discriminatorExpression);
2162+
Check.DebugAssert(
2163+
structuralProjection1.TableMap.Count == structuralProjection2.TableMap.Count,
2164+
"Set operation over entity projections with different table map counts");
2165+
Check.DebugAssert(
2166+
structuralProjection1.TableMap.Keys.All(t => structuralProjection2.TableMap.ContainsKey(t)),
2167+
"Set operation over entity projections with table map discrepancy");
2168+
Check.DebugAssert(
2169+
structuralProjection1.StructuralType == structuralProjection2.StructuralType,
2170+
"Set operation over entity projections with different structural types");
2171+
Check.DebugAssert(
2172+
structuralProjection1.IsNullable == structuralProjection2.IsNullable,
2173+
"Set operation over entity projections with different nullabilities");
2174+
2175+
var tableMap = projection1.TableMap.ToDictionary(kvp => kvp.Key, _ => setOperationAlias);
2176+
2177+
var discriminatorExpression = structuralProjection1.DiscriminatorExpression;
2178+
if (structuralProjection1.DiscriminatorExpression != null
2179+
&& structuralProjection2.DiscriminatorExpression != null)
2180+
{
2181+
var alias = GenerateUniqueColumnAlias(DiscriminatorColumnAlias);
2182+
var innerProjection = new ProjectionExpression(structuralProjection1.DiscriminatorExpression, alias);
2183+
select1._projection.Add(innerProjection);
2184+
select2._projection.Add(new ProjectionExpression(structuralProjection2.DiscriminatorExpression, alias));
2185+
discriminatorExpression = CreateColumnExpression(innerProjection, setOperationAlias);
2186+
}
21742187

2175-
if (outerIdentifiers.Length > 0 && outerProjection is { StructuralType: IEntityType entityType })
2176-
{
2177-
var primaryKey = entityType.FindPrimaryKey();
2188+
var outerProjection = new StructuralTypeProjectionExpression(
2189+
structuralProjection1.StructuralType, propertyExpressions, complexPropertyCache, tableMap, nullable: false, discriminatorExpression);
21782190

2179-
// We know that there are existing identifiers (see condition above); we know we must have a key since a keyless
2180-
// entity type would have wiped the identifiers when generating the join.
2181-
Check.DebugAssert(primaryKey != null, "primary key is null.");
2182-
foreach (var property in primaryKey.Properties)
2191+
if (outerIdentifiers.Length > 0 && outerProjection is { StructuralType: IEntityType entityType })
21832192
{
2184-
entityProjectionIdentifiers.Add(outerProjection.BindProperty(property));
2185-
entityProjectionValueComparers.Add(property.GetKeyValueComparer());
2193+
var primaryKey = entityType.FindPrimaryKey();
2194+
2195+
// We know that there are existing identifiers (see condition above); we know we must have a key since a keyless
2196+
// entity type would have wiped the identifiers when generating the join.
2197+
Check.DebugAssert(primaryKey != null, "primary key is null.");
2198+
foreach (var property in primaryKey.Properties)
2199+
{
2200+
entityProjectionIdentifiers.Add(outerProjection.BindProperty(property));
2201+
entityProjectionValueComparers.Add(property.GetKeyValueComparer());
2202+
}
21862203
}
2187-
}
21882204

2189-
_projectionMapping[projectionMember] = outerProjection;
2205+
return outerProjection;
2206+
}
21902207
}
21912208

21922209
string GenerateUniqueColumnAlias(string baseAlias)
@@ -2560,10 +2577,10 @@ public static StructuralTypeShaperExpression GenerateComplexPropertyShaperExpres
25602577
// We do not support complex type splitting, so we will only ever have a single table/view mapping to it.
25612578
// See Issue #32853 and Issue #31248
25622579
var complexTypeTable = complexProperty.ComplexType.GetViewOrTableMappings().Single().Table;
2563-
if (!containerProjection.TableMap.TryGetValue(complexTypeTable, out var tableReferenceExpression))
2580+
if (!containerProjection.TableMap.TryGetValue(complexTypeTable, out var tableAlias))
25642581
{
25652582
complexTypeTable = complexProperty.ComplexType.GetDefaultMappings().Single().Table;
2566-
tableReferenceExpression = containerProjection.TableMap[complexTypeTable];
2583+
tableAlias = containerProjection.TableMap[complexTypeTable];
25672584
}
25682585
var isComplexTypeNullable = containerProjection.IsNullable || complexProperty.IsNullable;
25692586

@@ -2581,14 +2598,14 @@ public static StructuralTypeShaperExpression GenerateComplexPropertyShaperExpres
25812598
// TODO: Reimplement EntityProjectionExpression via TableMap, and then use that here
25822599
var column = complexTypeTable.FindColumn(property)!;
25832600
propertyExpressionMap[property] = CreateColumnExpression(
2584-
property, column, tableReferenceExpression, isComplexTypeNullable || column.IsNullable);
2601+
property, column, tableAlias, isComplexTypeNullable || column.IsNullable);
25852602
}
25862603

25872604
// The table map of the target complex type should only ever contains a single table (no table splitting).
25882605
// If the source is itself a complex type (nested complex type), its table map is already suitable and we can just pass it on.
25892606
var newTableMap = containerProjection.TableMap.Count == 1
25902607
? containerProjection.TableMap
2591-
: new Dictionary<ITableBase, string> { [complexTypeTable] = tableReferenceExpression };
2608+
: new Dictionary<ITableBase, string> { [complexTypeTable] = tableAlias };
25922609

25932610
Check.DebugAssert(newTableMap.Single().Key == complexTypeTable, "Bad new table map");
25942611

@@ -3530,33 +3547,35 @@ StructuralTypeProjectionExpression LiftEntityProjectionFromSubquery(
35303547
string subqueryAlias)
35313548
{
35323549
var propertyExpressions = new Dictionary<IProperty, ColumnExpression>();
3550+
var complexPropertyCache = new Dictionary<IComplexProperty, StructuralTypeShaperExpression>();
35333551

3534-
HandleTypeProjection(projection);
3535-
3536-
void HandleTypeProjection(StructuralTypeProjectionExpression typeProjection)
3552+
foreach (var property in projection.StructuralType.GetAllPropertiesInHierarchy())
35373553
{
3538-
foreach (var property in typeProjection.StructuralType.GetAllPropertiesInHierarchy())
3554+
// json entity projection (i.e. JSON entity that was transformed into query root) may have synthesized keys
3555+
// but they don't correspond to any columns - we need to skip those
3556+
if (projection is { StructuralType: IEntityType entityType }
3557+
&& entityType.IsMappedToJson()
3558+
&& property.IsOrdinalKeyProperty())
35393559
{
3540-
// json entity projection (i.e. JSON entity that was transformed into query root) may have synthesized keys
3541-
// but they don't correspond to any columns - we need to skip those
3542-
if (typeProjection is { StructuralType: IEntityType entityType }
3543-
&& entityType.IsMappedToJson()
3544-
&& property.IsOrdinalKeyProperty())
3545-
{
3546-
continue;
3547-
}
3548-
3549-
var innerColumn = typeProjection.BindProperty(property);
3550-
var outerColumn = subquery.GenerateOuterColumn(subqueryAlias, innerColumn);
3551-
projectionMap[innerColumn] = outerColumn;
3552-
propertyExpressions[property] = outerColumn;
3560+
continue;
35533561
}
35543562

3555-
foreach (var complexProperty in GetAllComplexPropertiesInHierarchy(typeProjection.StructuralType))
3556-
{
3557-
HandleTypeProjection(
3558-
(StructuralTypeProjectionExpression)typeProjection.BindComplexProperty(complexProperty).ValueBufferExpression);
3559-
}
3563+
var innerColumn = projection.BindProperty(property);
3564+
var outerColumn = subquery.GenerateOuterColumn(subqueryAlias, innerColumn);
3565+
3566+
projectionMap[innerColumn] = outerColumn;
3567+
propertyExpressions[property] = outerColumn;
3568+
}
3569+
3570+
foreach (var complexProperty in GetAllComplexPropertiesInHierarchy(projection.StructuralType))
3571+
{
3572+
var complexPropertyShaper = projection.BindComplexProperty(complexProperty);
3573+
3574+
var complexTypeProjectionExpression = LiftEntityProjectionFromSubquery(
3575+
(StructuralTypeProjectionExpression)complexPropertyShaper.ValueBufferExpression,
3576+
subqueryAlias);
3577+
3578+
complexPropertyCache[complexProperty] = complexPropertyShaper.Update(complexTypeProjectionExpression);
35603579
}
35613580

35623581
ColumnExpression? discriminatorExpression = null;
@@ -3570,7 +3589,7 @@ void HandleTypeProjection(StructuralTypeProjectionExpression typeProjection)
35703589
var tableMap = projection.TableMap.ToDictionary(kvp => kvp.Key, _ => subqueryAlias);
35713590

35723591
var newEntityProjection = new StructuralTypeProjectionExpression(
3573-
projection.StructuralType, propertyExpressions, tableMap, nullable: false, discriminatorExpression);
3592+
projection.StructuralType, propertyExpressions, complexPropertyCache, tableMap, nullable: false, discriminatorExpression);
35743593

35753594
if (projection.StructuralType is IEntityType entityType2)
35763595
{

src/EFCore.Relational/Query/StructuralTypeProjectionExpression.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,49 @@ public StructuralTypeProjectionExpression(
4444
{
4545
}
4646

47+
/// <summary>
48+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
49+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
50+
/// any release. You should only use it directly in your code with extreme caution and knowing that
51+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
52+
/// </summary>
53+
[EntityFrameworkInternal]
54+
public StructuralTypeProjectionExpression(
55+
ITypeBase type,
56+
IReadOnlyDictionary<IProperty, ColumnExpression> propertyExpressionMap,
57+
Dictionary<IComplexProperty, StructuralTypeShaperExpression> complexPropertyCache,
58+
IReadOnlyDictionary<ITableBase, string> tableMap,
59+
bool nullable = false,
60+
SqlExpression? discriminatorExpression = null)
61+
: this(
62+
type,
63+
propertyExpressionMap,
64+
new Dictionary<INavigation, StructuralTypeShaperExpression>(),
65+
complexPropertyCache,
66+
tableMap,
67+
nullable,
68+
discriminatorExpression)
69+
{
70+
}
71+
72+
private StructuralTypeProjectionExpression(
73+
ITypeBase type,
74+
IReadOnlyDictionary<IProperty, ColumnExpression> propertyExpressionMap,
75+
Dictionary<INavigation, StructuralTypeShaperExpression> ownedNavigationMap,
76+
Dictionary<IComplexProperty, StructuralTypeShaperExpression> complexPropertyCache,
77+
IReadOnlyDictionary<ITableBase, string> tableMap,
78+
bool nullable,
79+
SqlExpression? discriminatorExpression = null)
80+
{
81+
StructuralType = type;
82+
_propertyExpressionMap = propertyExpressionMap;
83+
_ownedNavigationMap = ownedNavigationMap;
84+
_complexPropertyCache = complexPropertyCache;
85+
TableMap = tableMap;
86+
IsNullable = nullable;
87+
DiscriminatorExpression = discriminatorExpression;
88+
}
89+
4790
private StructuralTypeProjectionExpression(
4891
ITypeBase type,
4992
IReadOnlyDictionary<IProperty, ColumnExpression> propertyExpressionMap,

0 commit comments

Comments
 (0)