From 91ccdf67bde982ca56e3e05c7e13bc96800621b1 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Mon, 4 Dec 2023 15:41:16 +0100 Subject: [PATCH] Bring back enumerable Concat/Append translations for ExecuteUpdate (#3005) Fixes #3001 --- .../Internal/NpgsqlArrayMethodTranslator.cs | 44 +++++++++++++++++++ .../NonSharedModelBulkUpdatesNpgsqlTest.cs | 35 +++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs index 834dcd949..2dd0c5db3 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlArrayMethodTranslator.cs @@ -1,6 +1,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using static Npgsql.EntityFrameworkCore.PostgreSQL.Utilities.Statics; +using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; @@ -33,6 +34,17 @@ public class NpgsqlArrayMethodTranslator : IMethodCallTranslator private static readonly MethodInfo Enumerable_SequenceEqual = typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) .Single(m => m.Name == nameof(Enumerable.SequenceEqual) && m.GetParameters().Length == 2); + + // TODO: Enumerable Append and Concat are only here because primitive collections aren't handled in ExecuteUpdate, + // https://github.com/dotnet/efcore/issues/32494 + private static readonly MethodInfo Enumerable_Append = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(Enumerable.Append) && m.GetParameters().Length == 2); + + private static readonly MethodInfo Enumerable_Concat = + typeof(Enumerable).GetTypeInfo().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Single(m => m.Name == nameof(Enumerable.Concat) && m.GetParameters().Length == 2); + // ReSharper restore InconsistentNaming #endregion Methods @@ -155,6 +167,38 @@ static bool IsMappedToNonArray(SqlExpression arrayOrList) _sqlExpressionFactory.Constant(-1)); } + // TODO: Enumerable Append and Concat are only here because primitive collections aren't handled in ExecuteUpdate, + // https://github.com/dotnet/efcore/issues/32494 + if (method.IsClosedFormOf(Enumerable_Append)) + { + var (item, array) = _sqlExpressionFactory.ApplyTypeMappingsOnItemAndArray(arguments[0], arrayOrList); + + return _sqlExpressionFactory.Function( + "array_append", + new[] { array, item }, + nullable: true, + TrueArrays[2], + arrayOrList.Type, + arrayOrList.TypeMapping); + } + + if (method.IsClosedFormOf(Enumerable_Concat)) + { + var inferredMapping = ExpressionExtensions.InferTypeMapping(arrayOrList, arguments[0]); + + return _sqlExpressionFactory.Function( + "array_cat", + new[] + { + _sqlExpressionFactory.ApplyTypeMapping(arrayOrList, inferredMapping), + _sqlExpressionFactory.ApplyTypeMapping(arguments[0], inferredMapping) + }, + nullable: true, + TrueArrays[2], + arrayOrList.Type, + inferredMapping); + } + return null; } } diff --git a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs index 943d6c8f9..5c1bda899 100644 --- a/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesNpgsqlTest.cs @@ -151,6 +151,41 @@ public override async Task Update_with_alias_uniquification_in_setter_subquery(b """); } + [ConditionalTheory] // #3001 + [MemberData(nameof(IsAsyncData))] + public virtual async Task Update_with_primitive_collection_in_value_selector(bool async) + { + var contextFactory = await InitializeAsync( + seed: ctx => + { + ctx.AddRange(new EntityWithPrimitiveCollection { Tags = new List { "tag1", "tag2" }}); + ctx.SaveChanges(); + }); + + await AssertUpdate( + async, + contextFactory.CreateContext, + ss => ss.EntitiesWithPrimitiveCollection, + s => s.SetProperty(x => x.Tags, x => x.Tags.Append("another_tag")), + rowsAffectedCount: 1); + } + + protected class Context3001 : DbContext + { + public Context3001(DbContextOptions options) + : base(options) + { + } + + public DbSet EntitiesWithPrimitiveCollection { get; set; } + } + + protected class EntityWithPrimitiveCollection + { + public int Id { get; set; } + public List Tags { get; set; } + } + private void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected);