From 4b29d6cbee188986e70ec1a7051c08e082a483aa Mon Sep 17 00:00:00 2001 From: Matt Morrissette Date: Thu, 9 Jun 2022 17:38:25 -0700 Subject: [PATCH] Add support for code generation for List types Fixes #2401 --- src/EFCore.PG/Internal/EnumerableMethods.cs | 4 +- .../Mapping/NpgsqlArrayListTypeMapping.cs | 10 ++ .../Mapping/NpgsqlArrayTypeMapping.cs | 26 +++++ .../Migrations/MigrationsNpgsqlTest.cs | 103 ++++++++++++++++++ .../NpgsqlNodaTimeTypeMappingTest.cs | 19 ++++ 5 files changed, 160 insertions(+), 2 deletions(-) diff --git a/src/EFCore.PG/Internal/EnumerableMethods.cs b/src/EFCore.PG/Internal/EnumerableMethods.cs index 8ef148691..347212173 100644 --- a/src/EFCore.PG/Internal/EnumerableMethods.cs +++ b/src/EFCore.PG/Internal/EnumerableMethods.cs @@ -176,7 +176,7 @@ internal static class EnumerableMethods //public static MethodInfo ToHashSet { get; } //public static MethodInfo ToHashSetWithComparer { get; } - // public static MethodInfo ToList { get; } + public static MethodInfo ToList { get; } //public static MethodInfo ToLookupWithKeySelector { get; } //public static MethodInfo ToLookupWithKeySelectorAndComparer { get; } @@ -553,7 +553,7 @@ static EnumerableMethods() // ToArray = GetMethod(nameof(Enumerable.ToArray), 1, types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) }); - // ToList = GetMethod(nameof(Enumerable.ToList), 1, types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) }); + ToList = GetMethod(nameof(Enumerable.ToList), 1, types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) }); // Take = GetMethod(nameof(Enumerable.Take), 1, // types => new[] diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs index 1389bd8a2..e78274a2c 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs @@ -88,6 +88,16 @@ public override NpgsqlArrayTypeMapping MakeNonNullable() protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping) => new NpgsqlArrayListTypeMapping(parameters, elementMapping); + #region Code Generation + + public override Expression GenerateCodeLiteral(object value) + { + var arrayExpr = base.GenerateCodeLiteral(value); + return Expression.Call(PostgreSQL.Internal.EnumerableMethods.ToList.MakeGenericMethod(ElementMapping.ClrType), arrayExpr); + } + + #endregion + #region Value Comparison // Note that the value comparison code is largely duplicated from NpgsqlArrayTypeMapping. diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs index 4ae2f34db..cc93a554f 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs @@ -148,6 +148,32 @@ protected override void ConfigureParameter(DbParameter parameter) } } + #region Code Generation + public override Expression GenerateCodeLiteral(object value) + { + var values = (IList)value; + List elements = new(values.Count); + var generated = true; + foreach (var element in values) + { + if (generated) + { + try + { + elements.Add(ElementMapping.GenerateCodeLiteral(element)); // attempt to convert if required + continue; + } + catch (NotSupportedException) + { + generated = false; // if we can't generate one element, we probably can't generate any + } + } + elements.Add(Expression.Constant(element)); + } + return Expression.NewArrayInit(ElementMapping.ClrType, elements); + } + #endregion + // isElementNullable is provided for reference-type properties by decoding NRT information from the property, since that's not // available on the CLR type. Note, however, that because of value conversion we may get a discrepancy between the model property's // nullability and the provider types' (e.g. array of nullable reference property value-converted to array of non-nullable value diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index fdcddca5a..7054131c1 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -297,6 +297,50 @@ await Test( );"); } + [Fact] + public virtual async Task Create_table_with_string_list_column() + { + await Test( + _ => { }, + builder => builder.Entity( + "People", e => + { + e.Property("Id"); + e.HasKey("Id"); + e.Property>("Values"); + }), + asserter: null); // We don't scaffold unlogged + + AssertSql( + @"CREATE TABLE ""People"" ( + ""Id"" integer GENERATED BY DEFAULT AS IDENTITY, + ""Values"" text[] NULL, + CONSTRAINT ""PK_People"" PRIMARY KEY (""Id"") +);"); + } + + [Fact] + public virtual async Task Create_table_with_required_int_array_column() + { + await Test( + _ => { }, + builder => builder.Entity( + "People", e => + { + e.Property("Id"); + e.HasKey("Id"); + e.Property("Values").IsRequired(); + }), + asserter: null); // We don't scaffold unlogged + + AssertSql( + @"CREATE TABLE ""People"" ( + ""Id"" integer GENERATED BY DEFAULT AS IDENTITY, + ""Values"" integer[] NOT NULL, + CONSTRAINT ""PK_People"" PRIMARY KEY (""Id"") +);"); + } + public override async Task Drop_table() { await base.Drop_table(); @@ -683,6 +727,28 @@ await Test( } #pragma warning restore CS0618 + [Fact] + public virtual async Task Add_string_list_column_with_default() + { + await Test( + builder => builder.Entity( + "People", e => + { + e.Property("Id"); + e.HasKey("Id"); + }), + _ => { }, + builder => builder.Entity( + "People", e => + { + e.Property>("Values").HasDefaultValue(new[] { "1", "2" }.ToList()); + }), + asserter: null); // We don't scaffold unlogged + + AssertSql( + @"ALTER TABLE ""People"" ADD ""Values"" text[] NULL DEFAULT ARRAY['1','2']::text[];"); + } + public override async Task Add_column_shared() { await base.Add_column_shared(); @@ -2553,6 +2619,43 @@ SELECT setval( false);"); } + [ConditionalFact] + public virtual async Task InsertDataOperation_StringList() + { + await Test( + builder => + { + builder.Entity( + "Person", e => + { + e.Property("Id"); + e.Property>("Values"); + e.HasKey("Id"); + }); + }, + _ => { }, + builder => + { + builder.Entity("Person").HasData( + new { Id = 1, Values = new List { "1", "2" }}, + new { Id = 2, Values = new List { "2", "3" }}); + }, + _ => { }); + + AssertSql( + @"INSERT INTO ""Person"" (""Id"", ""Values"") +VALUES (1, ARRAY['1','2']::text[]); +INSERT INTO ""Person"" (""Id"", ""Values"") +VALUES (2, ARRAY['2','3']::text[]);", + // + @"SELECT setval( + pg_get_serial_sequence('""Person""', 'Id'), + GREATEST( + (SELECT MAX(""Id"") FROM ""Person"") + 1, + nextval(pg_get_serial_sequence('""Person""', 'Id'))), + false);"); + } + #endregion #region PostgreSQL extensions diff --git a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs index 44cd4ac2c..237c76f3e 100644 --- a/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs +++ b/test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs @@ -208,6 +208,25 @@ public void GenerateCodeLiteral_returns_tstzrange_Interval_literal() CodeLiteral(new Interval(new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), null))); } + [Fact] + public void GenerateCodeLiteral_returns_string_array_literal() + { + Assert.Equal( + @"new[] { ""1"", ""2"", ""3"" }", + CodeLiteral(new[] { "1", "2", "3"}) + ); + } + + [Fact] + public void GenerateCodeLiteral_returns_int_list_literal() + { + Assert.Equal( + @"System.Linq.Enumerable.ToList(new int[] { 1, 2, 3 })", + CodeLiteral(new List { 1, 2, 3}) + ); + } + + [Fact] public void Interval_array_is_properly_mapped() {