Bug #118572 Incorrect column alias used in CASE-WHEN when querying Table-Per-Type inheritance with subclass column name clash
Submitted: 2 Jul 8:05 Modified: 3 Jul 6:36
Reporter: Kristoffer Sjöberg (OCA) Email Updates:
Status: Verified Impact on me:
None 
Category:Connector / NET Severity:S3 (Non-critical)
Version:8.x, 9.x OS:Any
Assigned to: CPU Architecture:Any
Tags: Contribution, Entity Framework 6

[2 Jul 8:05] Kristoffer Sjöberg
Description:
When using Table-Per-Type with a base class and multiple implementations, when this is queried via a join, subclass-specific columns like Status from multiple tables (e.g., Cars, Bikes) are given unique aliases during projection, but those aliases are not correctly referenced in downstream expressions.
In the example below, both Cars.Status and Bikes.Status are projected into the subquery with distinct aliases:

`Project1`.`Status`,              -- from Bikes
`Project2`.`Status` AS `STATUS1` -- from Cars

However, the CASE-WHEN construction that determines which subclass property to return uses the unaliased column Join2.Status, which refers to the Bikes.Status, even when the current row is from Cars:
CASE 
  WHEN ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL)) THEN (`Join2`.`Status`)
  ...
  ELSE (`Join2`.`Status`)
END AS `C3` / `C5`
This results in the Cars.Status value (aliased as STATUS1) being completely ignored.
This has the consequence that subclass-specific values are lost or incorrect in the final result set. For example, when querying a Car, the Status field will incorrectly return NULL or a Bike’s status (if any), rather than the expected value from Cars.Status.

## Reproduction SQL

```SELECT 
CASE WHEN ((NOT ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL))) AND (NOT ((`Join2`.`C11` = 1) AND (`Join2`.`C11` IS NOT NULL)))) THEN ('2X')  WHEN ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL)) THEN ('2X0X')  ELSE ('2X1X') END AS `C1`, 
`Join2`.`Id`, 
`Join2`.`Year`, 
`Join2`.`Name`, 
`Join2`.`ManufacturerId`, 
CASE WHEN ((NOT ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL))) AND (NOT ((`Join2`.`C11` = 1) AND (`Join2`.`C11` IS NOT NULL)))) THEN (NULL)  WHEN ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL)) THEN (`Join2`.`BikeProperty`) END AS `C2`, 
CASE WHEN ((NOT ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL))) AND (NOT ((`Join2`.`C11` = 1) AND (`Join2`.`C11` IS NOT NULL)))) THEN (NULL)  WHEN ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL)) THEN (`Join2`.`Status`) END AS `C3`, 
CASE WHEN ((NOT ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL))) AND (NOT ((`Join2`.`C11` = 1) AND (`Join2`.`C11` IS NOT NULL)))) THEN (NULL)  WHEN ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL)) THEN (NULL)  ELSE (`Join2`.`CarProperty`) END AS `C4`, 
CASE WHEN ((NOT ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL))) AND (NOT ((`Join2`.`C11` = 1) AND (`Join2`.`C11` IS NOT NULL)))) THEN (NULL)  WHEN ((`Join2`.`C1` = 1) AND (`Join2`.`C1` IS NOT NULL)) THEN (NULL)  ELSE (`Join2`.`Status`) END AS `C5`
 FROM `Manufacturer4` AS `Extent1` INNER JOIN (SELECT 
`Extent2`.`Id`, 
`Extent2`.`Year`, 
`Extent2`.`Name`, 
`Extent2`.`ManufacturerId`, 
`Project1`.`Id` AS `ID1`, 
`Project1`.`BikeProperty`, 
`Project1`.`Status`, 
`Project1`.`C1`, 
`Project2`.`Id` AS `ID2`, 
`Project2`.`CarProperty`, 
`Project2`.`Status` AS `STATUS1`, 
`Project2`.`C1` AS `C11`
 FROM `Vehicles` AS `Extent2` LEFT OUTER JOIN (SELECT 
`Extent3`.`Id`, 
`Extent3`.`BikeProperty`, 
`Extent3`.`Status`, 
1 AS `C1`
 FROM `Bikes` AS `Extent3`) AS `Project1` ON `Extent2`.`Id` = `Project1`.`Id` LEFT OUTER JOIN (SELECT 
`Extent4`.`Id`, 
`Extent4`.`CarProperty`, 
`Extent4`.`Status`, 
1 AS `C1`
 FROM `Cars` AS `Extent4`) AS `Project2` ON `Extent2`.`Id` = `Project2`.`Id`) AS `Join2` ON `Extent1`.`ManufacturerId` = `Join2`.`ManufacturerId````

How to repeat:
Applying this patch adds a conflicting "Status" column to `Car4` and `Bike4`, both which inherits from `Vehicle4`. This reproduces the issue, as the `Car4`/`Bike4` loaded entities from DB will both load their `Status` column value from the same table (Bike4).

```diff --git forkSrcPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/CodeFirstTests.cs forkDstPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/CodeFirstTests.cs
index 5796c3e1e68a8c0b809b3edc2a6b093fcd8e6dbf..698f99e4f3b8732342d489bb565275fc4a43728a 100644
--- forkSrcPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/CodeFirstTests.cs
+++ forkDstPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/CodeFirstTests.cs
@@ -204,8 +204,8 @@ namespace MySql.Data.EntityFramework.CodeFirst.Tests
         context.Database.Delete();
         context.Database.Initialize(true);
         var manuf = context.Manufacturers.Add(new Manufacturer4 { Name = "ACME" });
-        context.Vehicles.Add(new Car4 { Id = 1, Name = "Mustang", Year = 2012, CarProperty = "Car", Manufacturer = manuf });
-        context.Vehicles.Add(new Bike4 { Id = 101, Name = "Mountain", Year = 2011, BikeProperty = "Bike", Manufacturer = manuf });
+        context.Vehicles.Add(new Car4 { Id = 1, Name = "Mustang", Year = 2012, Status = "Foo", CarProperty = "Car", Manufacturer = manuf });
+        context.Vehicles.Add(new Bike4 { Id = 101, Name = "Mountain", Year = 2011, Status = "Bar", BikeProperty = "Bike", Manufacturer = manuf });
         context.SaveChanges();
 
         vehicles = context.Manufacturers.SelectMany(v => v.Vehicles).ToList();
@@ -224,7 +224,9 @@ namespace MySql.Data.EntityFramework.CodeFirst.Tests
       {
         var vehiclesfromdb = context.Manufacturers.SelectMany(v => v.Vehicles).ToList();
         Assert.That(vehiclesfromdb.OfType<Car4>().Single().CarProperty, Is.EqualTo(vehicles.OfType<Car4>().Single().CarProperty));
+        Assert.That(vehiclesfromdb.OfType<Car4>().Single().Status, Is.EqualTo(vehicles.OfType<Car4>().Single().Status));
         Assert.That(vehiclesfromdb.OfType<Bike4>().Single().BikeProperty, Is.EqualTo(vehicles.OfType<Bike4>().Single().BikeProperty));
+        Assert.That(vehiclesfromdb.OfType<Bike4>().Single().Status, Is.EqualTo(vehicles.OfType<Bike4>().Single().Status));
       }
     }
 
diff --git forkSrcPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/Vehicle.cs forkDstPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/Vehicle.cs
index f95bbbfa13d8239edcfe89b5f313a099e4045bdf..f6b816ef96723c3abc2656c128ed254121895f0c 100644
--- forkSrcPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/Vehicle.cs
+++ forkDstPrefix/EntityFramework/tests/MySql.EntityFramework.CodeFirst.Tests/Vehicle.cs
@@ -121,11 +121,13 @@ namespace MySql.Data.EntityFramework.CodeFirst.Tests
   public class Car4 : Vehicle4
   {
     public string CarProperty { get; set; }
+    public string Status { get; set; }
   }
 
   public class Bike4 : Vehicle4
   {
     public string BikeProperty { get; set; }
+    public string Status { get; set; }
   }
   public class Manufacturer4
   {
```

Suggested fix:
Applying the following patch:

```diff
--- forkSrcPrefix/EntityFramework/src/Statements/SelectStatement.cs
+++ forkDstPrefix/EntityFramework/src/Statements/SelectStatement.cs
@@ -233,8 +233,6 @@ namespace MySql.Data.EntityFramework
           {
             newColumn.ColumnAlias = cf.ColumnAlias;
             newColumn.PushInput(cf.ActualColumnName);
-            if (cf.TableName != null)
-              newColumn.PushInput(cf.TableName);
           }
           else
           {
```

appears to resolve the issue. The problem stems from a mismatch in the `PropertyFragments` used to resolve column references during SQL generation.

In `SqlGenerator.Visit(DbPropertyExpression)`, a `ColumnFragment` is constructed using the accessed property name. Its `PropertyFragments` are built by traversing the expression tree—for example, something like `Join3.Join2.Project2.Status`. These fragments are passed to `scope.FindInputFromProperties`, which attempts to locate the matching `InputFragment` in the current `SelectStatement`.

However, the columns in the `SelectStatement` may have been renamed or aliased, and their stored `PropertyFragments` differ. In the case analyzed, the `SelectStatement` contains a column with fragments:

```
["Project2", "Extent4", "Status"]
```

But the expression tree generates:

```
["Join3", "Join2", "Project2", "Status"]
```

This discrepancy causes `ColumnFragment.Equals` to return `false` because `PropertyFragment.Equals` fails—the lists do not match, especially due to the presence of `Extent4`.

The patch resolves this by preventing `TableName` from being pushed into the `PropertyFragments`, resulting in a cleaner fragment like:

```
["Project2", "Status"]
```

This structure is now sufficient to satisfy the equality test in `PropertyFragment.Equals`, allowing the correct column match to be found during SQL generation.
[2 Jul 8:28] MySQL Verification Team
Hello Kristoffer,

Thank you for the report and contribution.
Please don't forget to resend the patch via "contribution" tab of this bug page. Thank you.

regards,
Umesh
[2 Jul 18:24] Kristoffer Sjöberg
Reproduction of bug 118572

(*) I confirm the code being submitted is offered under the terms of the OCA, and that I am authorized to contribute it.

Contribution: bug118572-repro.patch (application/octet-stream, text), 3.60 KiB.

[2 Jul 18:24] Kristoffer Sjöberg
Possible fix of 118572

(*) I confirm the code being submitted is offered under the terms of the OCA, and that I am authorized to contribute it.

Contribution: bug118572-fix.patch (application/octet-stream, text), 729 bytes.

[3 Jul 6:36] Kristoffer Sjöberg
Since submitting the original report, I noted that the `ShipTest` started failing with the originally submitted fix, which was to stop pushing the `cf.TableName` unconditionally to the inputs of a column. The updated (and attached) patch adds a condition to said push so it becomes:
```--- forkSrcPrefix/EntityFramework/src/Statements/SelectStatement.cs
+++ forkDstPrefix/EntityFramework/src/Statements/SelectStatement.cs
@@ -233,7 +233,7 @@ namespace MySql.Data.EntityFramework
           {
             newColumn.ColumnAlias = cf.ColumnAlias;
             newColumn.PushInput(cf.ActualColumnName);
-            if (cf.TableName != null)
+            if (cf.TableName != null && !select.From.Scoped)
               newColumn.PushInput(cf.TableName);
           }
           else```

This works for both the new test case and the existing `ShipTest` test, which differ in the value of the `select.From.Scoped` boolean property.
[3 Jul 6:43] MySQL Verification Team
Hello Kristoffer,

Thank you for your contribution.

regards,
Umesh