From 6280fd2e53ed3636b8060aabc608856771a778ab Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Mar 2026 09:51:39 +0000
Subject: [PATCH 1/4] Initial plan
From bfda368f56a26018a2b57d39f86c7b1b105a4380 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Mar 2026 10:10:03 +0000
Subject: [PATCH 2/4] Update SQLite bulk update SQL baselines for unaliased
UPDATE target
In multi-table UPDATE statements (with FROM clause), the UPDATE target
table no longer gets an alias, and column references use the table name
instead of the alias.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: roji <1862641+roji@users.noreply.github.com>
---
.../NorthwindBulkUpdatesSqliteTest.cs | 102 +++++++++---------
1 file changed, 51 insertions(+), 51 deletions(-)
diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs
index e72e6c1d147..b739323bcd2 100644
--- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs
@@ -781,7 +781,7 @@ public override async Task Update_Where_Skip_set_constant(bool async)
@p1='Updated' (Size = 7)
@p='4'
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p1
FROM (
SELECT "c"."CustomerID"
@@ -789,7 +789,7 @@ public override async Task Update_Where_Skip_set_constant(bool async)
WHERE "c"."CustomerID" LIKE 'F%'
LIMIT -1 OFFSET @p
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -802,7 +802,7 @@ public override async Task Update_Where_Take_set_constant(bool async)
@p1='Updated' (Size = 7)
@p='4'
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p1
FROM (
SELECT "c"."CustomerID"
@@ -810,7 +810,7 @@ public override async Task Update_Where_Take_set_constant(bool async)
WHERE "c"."CustomerID" LIKE 'F%'
LIMIT @p
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -824,7 +824,7 @@ public override async Task Update_Where_Skip_Take_set_constant(bool async)
@p1='4'
@p='2'
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p2
FROM (
SELECT "c"."CustomerID"
@@ -832,7 +832,7 @@ public override async Task Update_Where_Skip_Take_set_constant(bool async)
WHERE "c"."CustomerID" LIKE 'F%'
LIMIT @p1 OFFSET @p
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -844,14 +844,14 @@ public override async Task Update_Where_OrderBy_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "c"."CustomerID"
FROM "Customers" AS "c"
WHERE "c"."CustomerID" LIKE 'F%'
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -864,7 +864,7 @@ public override async Task Update_Where_OrderBy_Skip_set_constant(bool async)
@p1='Updated' (Size = 7)
@p='4'
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p1
FROM (
SELECT "c"."CustomerID"
@@ -873,7 +873,7 @@ public override async Task Update_Where_OrderBy_Skip_set_constant(bool async)
ORDER BY "c"."City"
LIMIT -1 OFFSET @p
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -886,7 +886,7 @@ public override async Task Update_Where_OrderBy_Take_set_constant(bool async)
@p1='Updated' (Size = 7)
@p='4'
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p1
FROM (
SELECT "c"."CustomerID"
@@ -895,7 +895,7 @@ public override async Task Update_Where_OrderBy_Take_set_constant(bool async)
ORDER BY "c"."City"
LIMIT @p
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -909,7 +909,7 @@ public override async Task Update_Where_OrderBy_Skip_Take_set_constant(bool asyn
@p1='4'
@p='2'
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p2
FROM (
SELECT "c"."CustomerID"
@@ -918,7 +918,7 @@ public override async Task Update_Where_OrderBy_Skip_Take_set_constant(bool asyn
ORDER BY "c"."City"
LIMIT @p1 OFFSET @p
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -932,7 +932,7 @@ public override async Task Update_Where_OrderBy_Skip_Take_Skip_Take_set_constant
@p1='6'
@p='2'
-UPDATE "Customers" AS "c1"
+UPDATE "Customers"
SET "ContactName" = @p4
FROM (
SELECT "c0"."CustomerID"
@@ -946,7 +946,7 @@ LIMIT @p1 OFFSET @p
ORDER BY "c0"."City"
LIMIT @p OFFSET @p
) AS "c2"
-WHERE "c1"."CustomerID" = "c2"."CustomerID"
+WHERE "Customers"."CustomerID" = "c2"."CustomerID"
""");
}
@@ -1031,14 +1031,14 @@ public override async Task Update_Where_Distinct_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT DISTINCT "c"."CustomerID", "c"."Address", "c"."City", "c"."CompanyName", "c"."ContactName", "c"."ContactTitle", "c"."Country", "c"."Fax", "c"."Phone", "c"."PostalCode", "c"."Region"
FROM "Customers" AS "c"
WHERE "c"."CustomerID" LIKE 'F%'
) AS "c1"
-WHERE "c0"."CustomerID" = "c1"."CustomerID"
+WHERE "Customers"."CustomerID" = "c1"."CustomerID"
""");
}
@@ -1048,7 +1048,7 @@ public override async Task Update_Where_using_navigation_set_null(bool async)
AssertExecuteUpdateSql(
"""
-UPDATE "Orders" AS "o0"
+UPDATE "Orders"
SET "OrderDate" = NULL
FROM (
SELECT "o"."OrderID"
@@ -1056,7 +1056,7 @@ public override async Task Update_Where_using_navigation_set_null(bool async)
LEFT JOIN "Customers" AS "c" ON "o"."CustomerID" = "c"."CustomerID"
WHERE "c"."City" = 'Seattle'
) AS "s"
-WHERE "o0"."OrderID" = "s"."OrderID"
+WHERE "Orders"."OrderID" = "s"."OrderID"
""");
}
@@ -1068,11 +1068,11 @@ public override async Task Update_Where_using_navigation_2_set_constant(bool asy
"""
@p='1'
-UPDATE "Order Details" AS "o"
+UPDATE "Order Details"
SET "Quantity" = CAST(@p AS INTEGER)
FROM "Orders" AS "o0"
LEFT JOIN "Customers" AS "c" ON "o0"."CustomerID" = "c"."CustomerID"
-WHERE "o"."OrderID" = "o0"."OrderID" AND "c"."City" = 'Seattle'
+WHERE "Order Details"."OrderID" = "o0"."OrderID" AND "c"."City" = 'Seattle'
""");
}
@@ -1082,10 +1082,10 @@ public override async Task Update_Where_SelectMany_set_null(bool async)
AssertExecuteUpdateSql(
"""
-UPDATE "Orders" AS "o"
+UPDATE "Orders"
SET "OrderDate" = NULL
FROM "Customers" AS "c"
-WHERE "c"."CustomerID" = "o"."CustomerID" AND "c"."CustomerID" LIKE 'F%'
+WHERE "c"."CustomerID" = "Orders"."CustomerID" AND "c"."CustomerID" LIKE 'F%'
""");
}
@@ -1205,7 +1205,7 @@ public override async Task Update_Union_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c1"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "c"."CustomerID", "c"."Address", "c"."City", "c"."CompanyName", "c"."ContactName", "c"."ContactTitle", "c"."Country", "c"."Fax", "c"."Phone", "c"."PostalCode", "c"."Region"
@@ -1216,7 +1216,7 @@ public override async Task Update_Union_set_constant(bool async)
FROM "Customers" AS "c0"
WHERE "c0"."CustomerID" LIKE 'A%'
) AS "u"
-WHERE "c1"."CustomerID" = "u"."CustomerID"
+WHERE "Customers"."CustomerID" = "u"."CustomerID"
""");
}
@@ -1228,7 +1228,7 @@ public override async Task Update_Concat_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c1"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "c"."CustomerID"
@@ -1239,7 +1239,7 @@ UNION ALL
FROM "Customers" AS "c0"
WHERE "c0"."CustomerID" LIKE 'A%'
) AS "u"
-WHERE "c1"."CustomerID" = "u"."CustomerID"
+WHERE "Customers"."CustomerID" = "u"."CustomerID"
""");
}
@@ -1251,7 +1251,7 @@ public override async Task Update_Except_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c1"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "c"."CustomerID", "c"."Address", "c"."City", "c"."CompanyName", "c"."ContactName", "c"."ContactTitle", "c"."Country", "c"."Fax", "c"."Phone", "c"."PostalCode", "c"."Region"
@@ -1262,7 +1262,7 @@ public override async Task Update_Except_set_constant(bool async)
FROM "Customers" AS "c0"
WHERE "c0"."CustomerID" LIKE 'A%'
) AS "e"
-WHERE "c1"."CustomerID" = "e"."CustomerID"
+WHERE "Customers"."CustomerID" = "e"."CustomerID"
""");
}
@@ -1274,7 +1274,7 @@ public override async Task Update_Intersect_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c1"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "c"."CustomerID", "c"."Address", "c"."City", "c"."CompanyName", "c"."ContactName", "c"."ContactTitle", "c"."Country", "c"."Fax", "c"."Phone", "c"."PostalCode", "c"."Region"
@@ -1285,7 +1285,7 @@ public override async Task Update_Intersect_set_constant(bool async)
FROM "Customers" AS "c0"
WHERE "c0"."CustomerID" LIKE 'A%'
) AS "i"
-WHERE "c1"."CustomerID" = "i"."CustomerID"
+WHERE "Customers"."CustomerID" = "i"."CustomerID"
""");
}
@@ -1297,14 +1297,14 @@ public override async Task Update_with_join_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "o"."CustomerID"
FROM "Orders" AS "o"
WHERE "o"."OrderID" < 10300
) AS "o0"
-WHERE "c"."CustomerID" = "o0"."CustomerID" AND "c"."CustomerID" LIKE 'F%'
+WHERE "Customers"."CustomerID" = "o0"."CustomerID" AND "Customers"."CustomerID" LIKE 'F%'
""");
}
@@ -1316,7 +1316,7 @@ public override async Task Update_with_LeftJoin(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "c"."CustomerID"
@@ -1328,7 +1328,7 @@ LEFT JOIN (
) AS "o0" ON "c"."CustomerID" = "o0"."CustomerID"
WHERE "c"."CustomerID" LIKE 'F%'
) AS "s"
-WHERE "c0"."CustomerID" = "s"."CustomerID"
+WHERE "Customers"."CustomerID" = "s"."CustomerID"
""");
}
@@ -1340,7 +1340,7 @@ public override async Task Update_with_LeftJoin_via_flattened_GroupJoin(bool asy
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c0"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT "c"."CustomerID"
@@ -1352,7 +1352,7 @@ LEFT JOIN (
) AS "o0" ON "c"."CustomerID" = "o0"."CustomerID"
WHERE "c"."CustomerID" LIKE 'F%'
) AS "s"
-WHERE "c0"."CustomerID" = "s"."CustomerID"
+WHERE "Customers"."CustomerID" = "s"."CustomerID"
""");
}
@@ -1364,7 +1364,7 @@ public override async Task Update_with_RightJoin(bool async)
"""
@p='2020-01-01T00:00:00.0000000Z' (Nullable = true) (DbType = DateTime)
-UPDATE "Orders" AS "o0"
+UPDATE "Orders"
SET "OrderDate" = @p
FROM (
SELECT "o"."OrderID"
@@ -1376,7 +1376,7 @@ RIGHT JOIN (
) AS "c0" ON "o"."CustomerID" = "c0"."CustomerID"
WHERE "o"."OrderID" < 10300
) AS "s"
-WHERE "o0"."OrderID" = "s"."OrderID"
+WHERE "Orders"."OrderID" = "s"."OrderID"
""");
}
@@ -1388,14 +1388,14 @@ public override async Task Update_with_cross_join_set_constant(bool async)
"""
@p='Updated' (Size = 7)
-UPDATE "Customers" AS "c"
+UPDATE "Customers"
SET "ContactName" = @p
FROM (
SELECT 1
FROM "Orders" AS "o"
WHERE "o"."OrderID" < 10300
) AS "o0"
-WHERE "c"."CustomerID" LIKE 'F%'
+WHERE "Customers"."CustomerID" LIKE 'F%'
""");
}
@@ -1416,7 +1416,7 @@ public override async Task Update_with_cross_join_left_join_set_constant(bool as
AssertExecuteUpdateSql(
"""
-UPDATE "Customers" AS "c"
+UPDATE "Customers"
SET "ContactName" = 'Updated'
FROM (
SELECT "c0"."CustomerID", "c0"."Address", "c0"."City", "c0"."CompanyName", "c0"."ContactName", "c0"."ContactTitle", "c0"."Country", "c0"."Fax", "c0"."Phone", "c0"."PostalCode", "c0"."Region"
@@ -1427,8 +1427,8 @@ LEFT JOIN (
SELECT "o"."OrderID", "o"."CustomerID", "o"."EmployeeID", "o"."OrderDate"
FROM "Orders" AS "o"
WHERE "o"."OrderID" < 10300
-) AS "t0" ON "c"."CustomerID" = "t0"."CustomerID"
-WHERE "c"."CustomerID" LIKE 'F%'
+) AS "t0" ON "Customers"."CustomerID" = "t0"."CustomerID"
+WHERE "Customers"."CustomerID" LIKE 'F%'
""");
}
@@ -1457,7 +1457,7 @@ public override async Task Update_Where_SelectMany_subquery_set_null(bool async)
AssertExecuteUpdateSql(
"""
-UPDATE "Orders" AS "o1"
+UPDATE "Orders"
SET "OrderDate" = NULL
FROM (
SELECT "o0"."OrderID"
@@ -1469,7 +1469,7 @@ WHERE CAST(strftime('%Y', "o"."OrderDate") AS INTEGER) = 1997
) AS "o0" ON "c"."CustomerID" = "o0"."CustomerID"
WHERE "c"."CustomerID" LIKE 'F%'
) AS "s"
-WHERE "o1"."OrderID" = "s"."OrderID"
+WHERE "Orders"."OrderID" = "s"."OrderID"
""");
}
@@ -1496,14 +1496,14 @@ public override async Task Update_Where_Join_set_property_from_joined_table(bool
AssertExecuteUpdateSql(
"""
-UPDATE "Customers" AS "c"
+UPDATE "Customers"
SET "City" = "c1"."City"
FROM (
SELECT "c0"."City"
FROM "Customers" AS "c0"
WHERE "c0"."CustomerID" = 'ALFKI'
) AS "c1"
-WHERE "c"."CustomerID" LIKE 'F%'
+WHERE "Customers"."CustomerID" LIKE 'F%'
""");
}
@@ -1545,7 +1545,7 @@ public override async Task Update_with_PK_pushdown_and_join_and_multiple_setters
@p='1'
@p2='10'
-UPDATE "Order Details" AS "o2"
+UPDATE "Order Details"
SET "Quantity" = CAST(@p AS INTEGER),
"UnitPrice" = @p2
FROM (
@@ -1559,7 +1559,7 @@ ORDER BY "o"."OrderID"
INNER JOIN "Orders" AS "o0" ON "o1"."OrderID" = "o0"."OrderID"
WHERE "o0"."CustomerID" = 'ALFKI'
) AS "s"
-WHERE "o2"."OrderID" = "s"."OrderID" AND "o2"."ProductID" = "s"."ProductID"
+WHERE "Order Details"."OrderID" = "s"."OrderID" AND "Order Details"."ProductID" = "s"."ProductID"
""");
}
From 4b757d4d8cc4d5ce29f121e0e9245485e1573109 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Mar 2026 10:15:12 +0000
Subject: [PATCH 3/4] Update SQLite bulk update test baselines for alias-free
UPDATE target
- Multi-table UPDATE statements no longer alias the target table
- Column references to the target alias now use the table name
- Fix Update_base_type_with_OfType and Update_base_property_on_derived_type
tests in TPT and TPTFilters to call correct base methods with proper
SQL baselines instead of expecting SqliteException (#31402)
- Remove unused Microsoft.Data.Sqlite import from TPT test files
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: roji <1862641+roji@users.noreply.github.com>
---
.../Query/Internal/SqliteQuerySqlGenerator.cs | 68 +++++++++++++++++++
...FiltersInheritanceBulkUpdatesSqliteTest.cs | 46 +++++++++----
.../TPTInheritanceBulkUpdatesSqliteTest.cs | 54 ++++++++++-----
.../NonSharedModelBulkUpdatesSqliteTest.cs | 16 ++---
4 files changed, 148 insertions(+), 36 deletions(-)
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
index 26c92c1f900..98ac4c2fadb 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
@@ -14,6 +14,74 @@ namespace Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
///
public class SqliteQuerySqlGenerator(QuerySqlGeneratorDependencies dependencies) : QuerySqlGenerator(dependencies)
{
+ private string? _updateTargetAlias;
+ private string? _updateTargetTableName;
+ private string? _updateTargetSchema;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitUpdate(UpdateExpression updateExpression)
+ {
+ if (updateExpression.SelectExpression.Tables.Count > 1)
+ {
+ _updateTargetAlias = updateExpression.Table.Alias;
+ _updateTargetTableName = updateExpression.Table.Name;
+ _updateTargetSchema = updateExpression.Table.Schema;
+ }
+
+ try
+ {
+ return base.VisitUpdate(updateExpression);
+ }
+ finally
+ {
+ _updateTargetAlias = null;
+ _updateTargetTableName = null;
+ _updateTargetSchema = null;
+ }
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitTable(TableExpression tableExpression)
+ {
+ if (_updateTargetAlias is not null && tableExpression.Alias == _updateTargetAlias)
+ {
+ Sql.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableExpression.Name, tableExpression.Schema));
+ return tableExpression;
+ }
+
+ return base.VisitTable(tableExpression);
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitColumn(ColumnExpression columnExpression)
+ {
+ if (_updateTargetAlias is not null && columnExpression.TableAlias == _updateTargetAlias)
+ {
+ Sql
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(_updateTargetTableName!, _updateTargetSchema))
+ .Append(".")
+ .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(columnExpression.Name));
+ return columnExpression;
+ }
+
+ return base.VisitColumn(columnExpression);
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs
index 807c47f66ba..a1a6c3cc4ec 100644
--- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs
@@ -1,8 +1,6 @@
// 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.Sqlite;
-
namespace Microsoft.EntityFrameworkCore.BulkUpdates.Inheritance;
#nullable disable
@@ -102,20 +100,32 @@ public override async Task Update_base_type(bool async)
"""
@p='Animal' (Size = 6)
-UPDATE "Animals" AS "a0"
+UPDATE "Animals"
SET "Name" = @p
FROM (
SELECT "a"."Id"
FROM "Animals" AS "a"
WHERE "a"."CountryId" = 1 AND "a"."Name" = 'Great spotted kiwi'
) AS "s"
-WHERE "a0"."Id" = "s"."Id"
+WHERE "Animals"."Id" = "s"."Id"
""");
}
- // #31402
- public override Task Update_base_type_with_OfType(bool async)
- => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async));
+ public override async Task Update_base_type_with_OfType(bool async)
+ {
+ await base.Update_base_type_with_OfType(async);
+
+ AssertExecuteUpdateSql(
+ """
+@p='NewBird' (Size = 7)
+
+UPDATE "Animals"
+SET "Name" = @p
+FROM "Birds" AS "b"
+INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
+WHERE "Animals"."Id" = "b"."Id" AND "Animals"."CountryId" = 1
+""");
+ }
public override async Task Update_where_hierarchy_subquery(bool async)
{
@@ -124,9 +134,21 @@ public override async Task Update_where_hierarchy_subquery(bool async)
AssertExecuteUpdateSql();
}
- // #31402
- public override Task Update_base_property_on_derived_type(bool async)
- => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async));
+ public override async Task Update_base_property_on_derived_type(bool async)
+ {
+ await base.Update_base_property_on_derived_type(async);
+
+ AssertExecuteUpdateSql(
+ """
+@p='SomeOtherKiwi' (Size = 13)
+
+UPDATE "Animals"
+SET "Name" = @p
+FROM "Birds" AS "b"
+INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
+WHERE "Animals"."Id" = "b"."Id" AND "Animals"."CountryId" = 1
+""");
+ }
public override async Task Update_derived_property_on_derived_type(bool async)
{
@@ -136,11 +158,11 @@ public override async Task Update_derived_property_on_derived_type(bool async)
"""
@p='0'
-UPDATE "Kiwi" AS "k"
+UPDATE "Kiwi"
SET "FoundOn" = @p
FROM "Animals" AS "a"
INNER JOIN "Birds" AS "b" ON "a"."Id" = "b"."Id"
-WHERE "a"."Id" = "k"."Id" AND "a"."CountryId" = 1
+WHERE "a"."Id" = "Kiwi"."Id" AND "a"."CountryId" = 1
""");
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs
index c466179c165..5f4768afeeb 100644
--- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs
@@ -1,8 +1,6 @@
// 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.Sqlite;
-
namespace Microsoft.EntityFrameworkCore.BulkUpdates.Inheritance;
#nullable disable
@@ -87,20 +85,32 @@ public override async Task Update_base_type(bool async)
"""
@p='Animal' (Size = 6)
-UPDATE "Animals" AS "a0"
+UPDATE "Animals"
SET "Name" = @p
FROM (
SELECT "a"."Id"
FROM "Animals" AS "a"
WHERE "a"."Name" = 'Great spotted kiwi'
) AS "s"
-WHERE "a0"."Id" = "s"."Id"
+WHERE "Animals"."Id" = "s"."Id"
""");
}
- // #31402
- public override Task Update_base_type_with_OfType(bool async)
- => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async));
+ public override async Task Update_base_type_with_OfType(bool async)
+ {
+ await base.Update_base_type_with_OfType(async);
+
+ AssertExecuteUpdateSql(
+ """
+@p='NewBird' (Size = 7)
+
+UPDATE "Animals"
+SET "Name" = @p
+FROM "Birds" AS "b"
+INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
+WHERE "Animals"."Id" = "b"."Id"
+""");
+ }
public override async Task Update_where_hierarchy_subquery(bool async)
{
@@ -109,9 +119,21 @@ public override async Task Update_where_hierarchy_subquery(bool async)
AssertExecuteUpdateSql();
}
- // #31402
- public override Task Update_base_property_on_derived_type(bool async)
- => Assert.ThrowsAsync(() => base.Update_base_property_on_derived_type(async));
+ public override async Task Update_base_property_on_derived_type(bool async)
+ {
+ await base.Update_base_property_on_derived_type(async);
+
+ AssertExecuteUpdateSql(
+ """
+@p='SomeOtherKiwi' (Size = 13)
+
+UPDATE "Animals"
+SET "Name" = @p
+FROM "Birds" AS "b"
+INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
+WHERE "Animals"."Id" = "b"."Id"
+""");
+ }
public override async Task Update_derived_property_on_derived_type(bool async)
{
@@ -121,11 +143,11 @@ public override async Task Update_derived_property_on_derived_type(bool async)
"""
@p='0'
-UPDATE "Kiwi" AS "k"
+UPDATE "Kiwi"
SET "FoundOn" = @p
FROM "Animals" AS "a"
INNER JOIN "Birds" AS "b" ON "a"."Id" = "b"."Id"
-WHERE "a"."Id" = "k"."Id"
+WHERE "a"."Id" = "Kiwi"."Id"
""");
}
@@ -186,10 +208,10 @@ public override async Task Update_with_interface_in_property_expression(bool asy
"""
@p='0'
-UPDATE "Coke" AS "c"
+UPDATE "Coke"
SET "SugarGrams" = @p
FROM "Drinks" AS "d"
-WHERE "d"."Id" = "c"."Id"
+WHERE "d"."Id" = "Coke"."Id"
""");
}
@@ -201,10 +223,10 @@ public override async Task Update_with_interface_in_EF_Property_in_property_expr
"""
@p='0'
-UPDATE "Coke" AS "c"
+UPDATE "Coke"
SET "SugarGrams" = @p
FROM "Drinks" AS "d"
-WHERE "d"."Id" = "c"."Id"
+WHERE "d"."Id" = "Coke"."Id"
""");
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs
index d3c2d8e9b60..d6c07682f5c 100644
--- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs
@@ -67,10 +67,10 @@ public override async Task Replace_ColumnExpression_in_column_setter(bool async)
"""
@p='SomeValue' (Size = 9)
-UPDATE "OwnedCollection" AS "o0"
+UPDATE "OwnedCollection"
SET "Value" = @p
FROM "Owner" AS "o"
-WHERE "o"."Id" = "o0"."OwnerId"
+WHERE "o"."Id" = "OwnedCollection"."OwnerId"
""");
}
@@ -106,10 +106,10 @@ public override async Task Update_non_owned_property_on_entity_with_owned_in_joi
"""
@p='NewValue' (Size = 8)
-UPDATE "Owner" AS "o"
+UPDATE "Owner"
SET "Title" = @p
FROM "Owner" AS "o0"
-WHERE "o"."Id" = "o0"."Id"
+WHERE "Owner"."Id" = "o0"."Id"
""");
}
@@ -142,11 +142,11 @@ public override async Task Update_non_main_table_in_entity_with_entity_splitting
AssertSql(
"""
-UPDATE "BlogsPart1" AS "b0"
-SET "Title" = CAST("b0"."Rating" AS TEXT),
- "Rating" = length("b0"."Title")
+UPDATE "BlogsPart1"
+SET "Title" = CAST("BlogsPart1"."Rating" AS TEXT),
+ "Rating" = length("BlogsPart1"."Title")
FROM "Blogs" AS "b"
-WHERE "b"."Id" = "b0"."Id"
+WHERE "b"."Id" = "BlogsPart1"."Id"
""");
}
From 44acc3ca4f2fc784967237c0b8416b687d657449 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 27 Mar 2026 10:31:12 +0000
Subject: [PATCH 4/4] Fix SQLite ExecuteUpdate with navigation properties and
TPT inheritance
SQLite doesn't support referencing the UPDATE target table in JOIN ON
clauses within the FROM clause of UPDATE...FROM statements. This fix:
1. Overrides VisitUpdate in SqliteQuerySqlGenerator to not alias the
UPDATE target table in multi-table UPDATE statements
2. Replaces column references to the target alias with the table name
3. Lifts JOIN predicates that reference the target table into the
WHERE clause, emitting comma-separated tables instead of JOINs
This fixes both the navigation property issue and the TPT inheritance
issue (#31402).
Agent-Logs-Url: https://github.com/dotnet/efcore/sessions/c9a7c8f9-9ec9-4305-abb2-e31de6da3f9d
Co-authored-by: roji <1862641+roji@users.noreply.github.com>
---
.../Query/Internal/SqliteQuerySqlGenerator.cs | 143 ++++++++++++++++--
...FiltersInheritanceBulkUpdatesSqliteTest.cs | 15 +-
.../TPTInheritanceBulkUpdatesSqliteTest.cs | 15 +-
3 files changed, 150 insertions(+), 23 deletions(-)
diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
index 98ac4c2fadb..1f2c182b1ad 100644
--- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
+++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQuerySqlGenerator.cs
@@ -26,25 +26,146 @@ public class SqliteQuerySqlGenerator(QuerySqlGeneratorDependencies dependencies)
///
protected override Expression VisitUpdate(UpdateExpression updateExpression)
{
- if (updateExpression.SelectExpression.Tables.Count > 1)
+ var selectExpression = updateExpression.SelectExpression;
+
+ if (selectExpression.Tables.Count > 1
+ && selectExpression is
+ {
+ Offset: null,
+ Limit: null,
+ Having: null,
+ Orderings: [],
+ GroupBy: [],
+ Projection: [],
+ }
+ && (!ReferenceEquals(selectExpression.Tables[0], updateExpression.Table)
+ || selectExpression.Tables[1] is InnerJoinExpression
+ || selectExpression.Tables[1] is CrossJoinExpression))
{
+ // SQLite doesn't support referencing the UPDATE target table (by alias or name) in JOIN ON clauses
+ // within the FROM clause. To work around this, we:
+ // 1. Don't alias the UPDATE target table
+ // 2. Replace column references to the target alias with the table name (via VisitColumn/VisitTable overrides)
+ // 3. Lift predicates from JOINs that reference the target table into the WHERE clause,
+ // emitting those tables as comma-separated instead of JOINed
_updateTargetAlias = updateExpression.Table.Alias;
_updateTargetTableName = updateExpression.Table.Name;
_updateTargetSchema = updateExpression.Table.Schema;
- }
- try
- {
- return base.VisitUpdate(updateExpression);
- }
- finally
- {
- _updateTargetAlias = null;
- _updateTargetTableName = null;
- _updateTargetSchema = null;
+ try
+ {
+ Sql.Append("UPDATE ");
+ Visit(updateExpression.Table);
+
+ Sql.AppendLine();
+ Sql.Append("SET ");
+
+ for (var i = 0; i < updateExpression.ColumnValueSetters.Count; i++)
+ {
+ if (i == 1)
+ {
+ Sql.IncrementIndent();
+ }
+
+ if (i > 0)
+ {
+ Sql.AppendLine(",");
+ }
+
+ var (column, value) = updateExpression.ColumnValueSetters[i];
+
+ Sql.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(column.Name)).Append(" = ");
+ Visit(value);
+ }
+
+ if (updateExpression.ColumnValueSetters.Count > 1)
+ {
+ Sql.DecrementIndent();
+ }
+
+ var predicate = selectExpression.Predicate;
+ var firstTablePrinted = false;
+ Sql.AppendLine().Append("FROM ");
+ for (var i = 0; i < selectExpression.Tables.Count; i++)
+ {
+ var table = selectExpression.Tables[i];
+ var joinExpression = table as JoinExpressionBase;
+
+ if (updateExpression.Table.Alias == (joinExpression?.Table.Alias ?? table.Alias))
+ {
+ LiftPredicate(table);
+ continue;
+ }
+
+ if (firstTablePrinted)
+ {
+ // For joins whose ON predicate references the target table, lift the predicate to WHERE
+ // and emit just the table (comma-separated) instead of a JOIN
+ if (joinExpression is PredicateJoinExpressionBase predicateJoin
+ && ReferencesTargetTable(predicateJoin.JoinPredicate))
+ {
+ Sql.Append(", ");
+ LiftPredicate(table);
+ Visit(joinExpression.Table);
+ }
+ else
+ {
+ Sql.AppendLine();
+ Visit(table);
+ }
+ }
+ else
+ {
+ firstTablePrinted = true;
+ LiftPredicate(table);
+ table = joinExpression?.Table ?? table;
+ Visit(table);
+ }
+ }
+
+ if (predicate != null)
+ {
+ Sql.AppendLine().Append("WHERE ");
+ Visit(predicate);
+ }
+
+ return updateExpression;
+
+ void LiftPredicate(TableExpressionBase joinTable)
+ {
+ if (joinTable is PredicateJoinExpressionBase predicateJoinExpression)
+ {
+ predicate = predicate == null
+ ? predicateJoinExpression.JoinPredicate
+ : new SqlBinaryExpression(
+ ExpressionType.AndAlso,
+ predicateJoinExpression.JoinPredicate,
+ predicate,
+ typeof(bool),
+ predicate.TypeMapping);
+ }
+ }
+ }
+ finally
+ {
+ _updateTargetAlias = null;
+ _updateTargetTableName = null;
+ _updateTargetSchema = null;
+ }
}
+
+ return base.VisitUpdate(updateExpression);
}
+ private bool ReferencesTargetTable(Expression expression)
+ => expression switch
+ {
+ ColumnExpression column => column.TableAlias == _updateTargetAlias,
+ SqlBinaryExpression binary => ReferencesTargetTable(binary.Left) || ReferencesTargetTable(binary.Right),
+ SqlUnaryExpression unary => ReferencesTargetTable(unary.Operand),
+ _ => false
+ };
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs
index a1a6c3cc4ec..a8d8593db8d 100644
--- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTFiltersInheritanceBulkUpdatesSqliteTest.cs
@@ -121,9 +121,13 @@ public override async Task Update_base_type_with_OfType(bool async)
UPDATE "Animals"
SET "Name" = @p
-FROM "Birds" AS "b"
-INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
-WHERE "Animals"."Id" = "b"."Id" AND "Animals"."CountryId" = 1
+FROM "Birds" AS "b", "Kiwi" AS "k0", (
+ SELECT "a"."Id"
+ FROM "Animals" AS "a"
+ LEFT JOIN "Kiwi" AS "k" ON "a"."Id" = "k"."Id"
+ WHERE "a"."CountryId" = 1 AND "k"."Id" IS NOT NULL
+) AS "s"
+WHERE "Animals"."Id" = "s"."Id" AND "Animals"."Id" = "k0"."Id" AND "Animals"."Id" = "b"."Id"
""");
}
@@ -144,9 +148,8 @@ public override async Task Update_base_property_on_derived_type(bool async)
UPDATE "Animals"
SET "Name" = @p
-FROM "Birds" AS "b"
-INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
-WHERE "Animals"."Id" = "b"."Id" AND "Animals"."CountryId" = 1
+FROM "Birds" AS "b", "Kiwi" AS "k"
+WHERE "Animals"."Id" = "k"."Id" AND "Animals"."Id" = "b"."Id" AND "Animals"."CountryId" = 1
""");
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs
index 5f4768afeeb..b1000c42e6c 100644
--- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/Inheritance/TPTInheritanceBulkUpdatesSqliteTest.cs
@@ -106,9 +106,13 @@ public override async Task Update_base_type_with_OfType(bool async)
UPDATE "Animals"
SET "Name" = @p
-FROM "Birds" AS "b"
-INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
-WHERE "Animals"."Id" = "b"."Id"
+FROM "Birds" AS "b", "Kiwi" AS "k0", (
+ SELECT "a"."Id"
+ FROM "Animals" AS "a"
+ LEFT JOIN "Kiwi" AS "k" ON "a"."Id" = "k"."Id"
+ WHERE "k"."Id" IS NOT NULL
+) AS "s"
+WHERE "Animals"."Id" = "s"."Id" AND "Animals"."Id" = "k0"."Id" AND "Animals"."Id" = "b"."Id"
""");
}
@@ -129,9 +133,8 @@ public override async Task Update_base_property_on_derived_type(bool async)
UPDATE "Animals"
SET "Name" = @p
-FROM "Birds" AS "b"
-INNER JOIN "Kiwi" AS "k" ON "Animals"."Id" = "k"."Id"
-WHERE "Animals"."Id" = "b"."Id"
+FROM "Birds" AS "b", "Kiwi" AS "k"
+WHERE "Animals"."Id" = "k"."Id" AND "Animals"."Id" = "b"."Id"
""");
}