From 8e4acf4ff6c780b6f6c42abf0bb3ec6504f330b8 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 26 Jun 2023 18:49:35 +0200 Subject: [PATCH] Perform alias uniquification in ExecuteUpdate setters (#31133) Fixes #31078 --- ...yableMethodTranslatingExpressionVisitor.cs | 5 +- .../Query/SqlExpressions/SelectExpression.cs | 9 ++- .../NonSharedModelBulkUpdatesTestBase.cs | 60 +++++++++++++++++++ .../NonSharedModelBulkUpdatesSqlServerTest.cs | 16 +++++ .../NonSharedModelBulkUpdatesSqliteTest.cs | 15 +++++ 5 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index c8abe3fc3d5..727a54681af 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -1544,7 +1544,10 @@ static bool AreOtherNonOwnedEntityTypesInTheTable(IEntityType rootType, ITableBa OperatorType: ExpressionType.Equal, Left: ColumnExpression column } sqlBinaryExpression) { - columnValueSetters.Add(new ColumnValueSetter(column, sqlBinaryExpression.Right)); + columnValueSetters.Add( + new ColumnValueSetter( + column, + selectExpression.AssignUniqueAliases(sqlBinaryExpression.Right))); } else { diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index f9a6294b358..36c3df4a2eb 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -4188,7 +4188,14 @@ private void AddTable(TableExpressionBase tableExpressionBase, TableReferenceExp _tableReferences.Add(tableReferenceExpression); } - private SqlExpression AssignUniqueAliases(SqlExpression expression) + /// + /// 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. + /// + [EntityFrameworkInternal] + public SqlExpression AssignUniqueAliases(SqlExpression expression) => (SqlExpression)new AliasUniquifier(_usedAliases).Visit(expression); private static string GenerateUniqueAlias(HashSet usedAliases, string currentAlias) diff --git a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs index 65597cc529c..fa128a8c0d0 100644 --- a/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/BulkUpdates/NonSharedModelBulkUpdatesTestBase.cs @@ -197,6 +197,66 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Update_with_alias_uniquification_in_setter_subquery(bool async) + { + var contextFactory = await InitializeAsync(); + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.Orders.Where(o => o.Id == 1) + .Select(o => new + { + Order = o, + Total = o.OrderProducts.Sum(op => op.Amount) + }), + s => s.SetProperty(x => x.Order.Total, x => x.Total), + rowsAffectedCount: 1); + } + + protected class Context31078 : DbContext + { + public Context31078(DbContextOptions options) + : base(options) + { + } + + public DbSet Orders + => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + b => + { + b.Property(o => o.Id).ValueGeneratedNever(); + b.HasData(new Order { Id = 1 }); + }); + + modelBuilder.Entity(b => + { + b.Property(op => op.Id).ValueGeneratedNever(); + b.HasData( + new OrderProduct { Id = 1, Amount = 8 }, + new OrderProduct { Id = 2, Amount = 9 }); + }); + } + } + + public class Order + { + public int Id { get; set; } + public int Total { get; set; } + public ICollection OrderProducts { get; set; } = new List(); + } + + public class OrderProduct + { + public int Id { get; set; } + public int Amount { get; set; } + } + public class Blog { public int Id { get; set; } diff --git a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs index 29a52b537fc..21f550176c6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs @@ -90,6 +90,22 @@ WHERE [b].[Title] IS NOT NULL AND [b].[Title] LIKE N'Arthur%' """); } + public override async Task Update_with_alias_uniquification_in_setter_subquery(bool async) + { + await base.Update_with_alias_uniquification_in_setter_subquery(async); + + AssertSql( +""" +UPDATE [o] +SET [o].[Total] = ( + SELECT COALESCE(SUM([o0].[Amount]), 0) + FROM [OrderProduct] AS [o0] + WHERE [o].[Id] = [o0].[OrderId]) +FROM [Orders] AS [o] +WHERE [o].[Id] = 1 +"""); + } + private void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs index ece889ef9f2..6c9fb62a4e1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs @@ -79,6 +79,21 @@ SELECT 1 """); } + public override async Task Update_with_alias_uniquification_in_setter_subquery(bool async) + { + await base.Update_with_alias_uniquification_in_setter_subquery(async); + + AssertSql( +""" +UPDATE "Orders" AS "o" +SET "Total" = ( + SELECT COALESCE(SUM("o0"."Amount"), 0) + FROM "OrderProduct" AS "o0" + WHERE "o"."Id" = "o0"."OrderId") +WHERE "o"."Id" = 1 +"""); + } + protected override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) => base.AddOptions(builder).ConfigureWarnings(wcb => wcb.Log(SqliteEventId.CompositeKeyWithValueGeneration));