Skip to content

Commit 3c556ea

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. Also modified VisitChildren and MakeNullable methods on StructuralTypeProjectionExpression to process/preserve complex type cache information, which was previously gobbled up/ignored. Fixes #32911
1 parent 229d1f3 commit 3c556ea

File tree

8 files changed

+1091
-79
lines changed

8 files changed

+1091
-79
lines changed

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

Lines changed: 86 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 (type is IComplexType)
21312132
{
21322133
var outerTypeMapping = column1.TypeMapping ?? column1.TypeMapping;
21332134
if (outerTypeMapping == null)
@@ -2141,52 +2142,62 @@ void ProcessStructuralType(
21412142
}
21422143
}
21432144

2144-
foreach (var complexProperty in GetAllComplexPropertiesInHierarchy(nestedProjection1.StructuralType))
2145+
foreach (var complexProperty in GetAllComplexPropertiesInHierarchy(type))
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");
21742168

2175-
if (outerIdentifiers.Length > 0 && outerProjection is { StructuralType: IEntityType entityType })
2176-
{
2177-
var primaryKey = entityType.FindPrimaryKey();
2169+
var tableMap = projection1.TableMap.ToDictionary(kvp => kvp.Key, _ => setOperationAlias);
21782170

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)
2171+
var discriminatorExpression = structuralProjection1.DiscriminatorExpression;
2172+
if (structuralProjection1.DiscriminatorExpression != null
2173+
&& structuralProjection2.DiscriminatorExpression != null)
21832174
{
2184-
entityProjectionIdentifiers.Add(outerProjection.BindProperty(property));
2185-
entityProjectionValueComparers.Add(property.GetKeyValueComparer());
2175+
var alias = GenerateUniqueColumnAlias(DiscriminatorColumnAlias);
2176+
var innerProjection = new ProjectionExpression(structuralProjection1.DiscriminatorExpression, alias);
2177+
select1._projection.Add(innerProjection);
2178+
select2._projection.Add(new ProjectionExpression(structuralProjection2.DiscriminatorExpression, alias));
2179+
discriminatorExpression = CreateColumnExpression(innerProjection, setOperationAlias);
21862180
}
2187-
}
21882181

2189-
_projectionMapping[projectionMember] = outerProjection;
2182+
var outerProjection = new StructuralTypeProjectionExpression(
2183+
type, propertyExpressions, complexPropertyCache, tableMap, nullable: false, discriminatorExpression);
2184+
2185+
if (outerIdentifiers.Length > 0 && outerProjection is { StructuralType: IEntityType entityType })
2186+
{
2187+
var primaryKey = entityType.FindPrimaryKey();
2188+
2189+
// We know that there are existing identifiers (see condition above); we know we must have a key since a keyless
2190+
// entity type would have wiped the identifiers when generating the join.
2191+
Check.DebugAssert(primaryKey != null, "primary key is null.");
2192+
foreach (var property in primaryKey.Properties)
2193+
{
2194+
entityProjectionIdentifiers.Add(outerProjection.BindProperty(property));
2195+
entityProjectionValueComparers.Add(property.GetKeyValueComparer());
2196+
}
2197+
}
2198+
2199+
return outerProjection;
2200+
}
21902201
}
21912202

21922203
string GenerateUniqueColumnAlias(string baseAlias)
@@ -2560,10 +2571,10 @@ public static StructuralTypeShaperExpression GenerateComplexPropertyShaperExpres
25602571
// We do not support complex type splitting, so we will only ever have a single table/view mapping to it.
25612572
// See Issue #32853 and Issue #31248
25622573
var complexTypeTable = complexProperty.ComplexType.GetViewOrTableMappings().Single().Table;
2563-
if (!containerProjection.TableMap.TryGetValue(complexTypeTable, out var tableReferenceExpression))
2574+
if (!containerProjection.TableMap.TryGetValue(complexTypeTable, out var tableAlias))
25642575
{
25652576
complexTypeTable = complexProperty.ComplexType.GetDefaultMappings().Single().Table;
2566-
tableReferenceExpression = containerProjection.TableMap[complexTypeTable];
2577+
tableAlias = containerProjection.TableMap[complexTypeTable];
25672578
}
25682579
var isComplexTypeNullable = containerProjection.IsNullable || complexProperty.IsNullable;
25692580

@@ -2581,14 +2592,14 @@ public static StructuralTypeShaperExpression GenerateComplexPropertyShaperExpres
25812592
// TODO: Reimplement EntityProjectionExpression via TableMap, and then use that here
25822593
var column = complexTypeTable.FindColumn(property)!;
25832594
propertyExpressionMap[property] = CreateColumnExpression(
2584-
property, column, tableReferenceExpression, isComplexTypeNullable || column.IsNullable);
2595+
property, column, tableAlias, isComplexTypeNullable || column.IsNullable);
25852596
}
25862597

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

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

@@ -3530,33 +3541,35 @@ StructuralTypeProjectionExpression LiftEntityProjectionFromSubquery(
35303541
string subqueryAlias)
35313542
{
35323543
var propertyExpressions = new Dictionary<IProperty, ColumnExpression>();
3544+
var complexPropertyCache = new Dictionary<IComplexProperty, StructuralTypeShaperExpression>();
35333545

3534-
HandleTypeProjection(projection);
3535-
3536-
void HandleTypeProjection(StructuralTypeProjectionExpression typeProjection)
3546+
foreach (var property in projection.StructuralType.GetAllPropertiesInHierarchy())
35373547
{
3538-
foreach (var property in typeProjection.StructuralType.GetAllPropertiesInHierarchy())
3548+
// json entity projection (i.e. JSON entity that was transformed into query root) may have synthesized keys
3549+
// but they don't correspond to any columns - we need to skip those
3550+
if (projection is { StructuralType: IEntityType entityType }
3551+
&& entityType.IsMappedToJson()
3552+
&& property.IsOrdinalKeyProperty())
35393553
{
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;
3554+
continue;
35533555
}
35543556

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

35623575
ColumnExpression? discriminatorExpression = null;
@@ -3570,7 +3583,7 @@ void HandleTypeProjection(StructuralTypeProjectionExpression typeProjection)
35703583
var tableMap = projection.TableMap.ToDictionary(kvp => kvp.Key, _ => subqueryAlias);
35713584

35723585
var newEntityProjection = new StructuralTypeProjectionExpression(
3573-
projection.StructuralType, propertyExpressions, tableMap, nullable: false, discriminatorExpression);
3586+
projection.StructuralType, propertyExpressions, complexPropertyCache, tableMap, nullable: false, discriminatorExpression);
35743587

35753588
if (projection.StructuralType is IEntityType entityType2)
35763589
{

0 commit comments

Comments
 (0)