Skip to content

Commit e390cf8

Browse files
committed
Add support for code generation for List<X> types
Fixes #2401
1 parent d9a236c commit e390cf8

File tree

5 files changed

+160
-2
lines changed

5 files changed

+160
-2
lines changed

src/EFCore.PG/Internal/EnumerableMethods.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ internal static class EnumerableMethods
176176
//public static MethodInfo ToHashSet { get; }
177177
//public static MethodInfo ToHashSetWithComparer { get; }
178178

179-
// public static MethodInfo ToList { get; }
179+
public static MethodInfo ToList { get; }
180180

181181
//public static MethodInfo ToLookupWithKeySelector { get; }
182182
//public static MethodInfo ToLookupWithKeySelectorAndComparer { get; }
@@ -553,7 +553,7 @@ static EnumerableMethods()
553553

554554
// ToArray = GetMethod(nameof(Enumerable.ToArray), 1, types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) });
555555

556-
// ToList = GetMethod(nameof(Enumerable.ToList), 1, types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) });
556+
ToList = GetMethod(nameof(Enumerable.ToList), 1, types => new[] { typeof(IEnumerable<>).MakeGenericType(types[0]) });
557557

558558
// Take = GetMethod(nameof(Enumerable.Take), 1,
559559
// types => new[]

src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayListTypeMapping.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ public override NpgsqlArrayTypeMapping MakeNonNullable()
8888
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters, RelationalTypeMapping elementMapping)
8989
=> new NpgsqlArrayListTypeMapping(parameters, elementMapping);
9090

91+
#region Code Generation
92+
93+
public override Expression GenerateCodeLiteral(object value)
94+
{
95+
var arrayExpr = base.GenerateCodeLiteral(value);
96+
return Expression.Call(PostgreSQL.Internal.EnumerableMethods.ToList.MakeGenericMethod(ElementMapping.ClrType), arrayExpr);
97+
}
98+
99+
#endregion
100+
91101
#region Value Comparison
92102

93103
// Note that the value comparison code is largely duplicated from NpgsqlArrayTypeMapping.

src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,32 @@ protected override void ConfigureParameter(DbParameter parameter)
148148
}
149149
}
150150

151+
#region Code Generation
152+
public override Expression GenerateCodeLiteral(object value)
153+
{
154+
var values = (IList)value;
155+
List<Expression> elements = new(values.Count);
156+
var generated = true;
157+
foreach (var element in values)
158+
{
159+
if (generated)
160+
{
161+
try
162+
{
163+
elements.Add(ElementMapping.GenerateCodeLiteral(element)); // attempt to convert if required
164+
continue;
165+
}
166+
catch (NotSupportedException)
167+
{
168+
generated = false; // if we can't generate one element, we probably can't generate any
169+
}
170+
}
171+
elements.Add(Expression.Constant(element));
172+
}
173+
return Expression.NewArrayInit(ElementMapping.ClrType, elements);
174+
}
175+
#endregion
176+
151177
// isElementNullable is provided for reference-type properties by decoding NRT information from the property, since that's not
152178
// available on the CLR type. Note, however, that because of value conversion we may get a discrepancy between the model property's
153179
// nullability and the provider types' (e.g. array of nullable reference property value-converted to array of non-nullable value

test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,50 @@ await Test(
297297
);");
298298
}
299299

300+
[Fact]
301+
public virtual async Task Create_table_with_string_list_column()
302+
{
303+
await Test(
304+
_ => { },
305+
builder => builder.Entity(
306+
"People", e =>
307+
{
308+
e.Property<int>("Id");
309+
e.HasKey("Id");
310+
e.Property<List<string>>("Values");
311+
}),
312+
asserter: null); // We don't scaffold unlogged
313+
314+
AssertSql(
315+
@"CREATE TABLE ""People"" (
316+
""Id"" integer GENERATED BY DEFAULT AS IDENTITY,
317+
""Values"" text[] NULL,
318+
CONSTRAINT ""PK_People"" PRIMARY KEY (""Id"")
319+
);");
320+
}
321+
322+
[Fact]
323+
public virtual async Task Create_table_with_required_int_array_column()
324+
{
325+
await Test(
326+
_ => { },
327+
builder => builder.Entity(
328+
"People", e =>
329+
{
330+
e.Property<int>("Id");
331+
e.HasKey("Id");
332+
e.Property<int[]>("Values").IsRequired();
333+
}),
334+
asserter: null); // We don't scaffold unlogged
335+
336+
AssertSql(
337+
@"CREATE TABLE ""People"" (
338+
""Id"" integer GENERATED BY DEFAULT AS IDENTITY,
339+
""Values"" integer[] NOT NULL,
340+
CONSTRAINT ""PK_People"" PRIMARY KEY (""Id"")
341+
);");
342+
}
343+
300344
public override async Task Drop_table()
301345
{
302346
await base.Drop_table();
@@ -683,6 +727,28 @@ await Test(
683727
}
684728
#pragma warning restore CS0618
685729

730+
[Fact]
731+
public virtual async Task Add_string_list_column_with_default()
732+
{
733+
await Test(
734+
builder => builder.Entity(
735+
"People", e =>
736+
{
737+
e.Property<int>("Id");
738+
e.HasKey("Id");
739+
}),
740+
_ => { },
741+
builder => builder.Entity(
742+
"People", e =>
743+
{
744+
e.Property<List<string>>("Values").HasDefaultValue(new[] { "1", "2" }.ToList());
745+
}),
746+
asserter: null); // We don't scaffold unlogged
747+
748+
AssertSql(
749+
@"ALTER TABLE ""People"" ADD ""Values"" text[] NULL DEFAULT ARRAY['1','2']::text[];");
750+
}
751+
686752
public override async Task Add_column_shared()
687753
{
688754
await base.Add_column_shared();
@@ -2553,6 +2619,43 @@ SELECT setval(
25532619
false);");
25542620
}
25552621

2622+
[ConditionalFact]
2623+
public virtual async Task InsertDataOperation_StringList()
2624+
{
2625+
await Test(
2626+
builder =>
2627+
{
2628+
builder.Entity(
2629+
"Person", e =>
2630+
{
2631+
e.Property<int>("Id");
2632+
e.Property<List<string>>("Values");
2633+
e.HasKey("Id");
2634+
});
2635+
},
2636+
_ => { },
2637+
builder =>
2638+
{
2639+
builder.Entity("Person").HasData(
2640+
new { Id = 1, Values = new List<string> { "1", "2" }},
2641+
new { Id = 2, Values = new List<string> { "2", "3" }});
2642+
},
2643+
_ => { });
2644+
2645+
AssertSql(
2646+
@"INSERT INTO ""Person"" (""Id"", ""Values"")
2647+
VALUES (1, ARRAY['1','2']::text[]);
2648+
INSERT INTO ""Person"" (""Id"", ""Values"")
2649+
VALUES (2, ARRAY['2','3']::text[]);",
2650+
//
2651+
@"SELECT setval(
2652+
pg_get_serial_sequence('""Person""', 'Id'),
2653+
GREATEST(
2654+
(SELECT MAX(""Id"") FROM ""Person"") + 1,
2655+
nextval(pg_get_serial_sequence('""Person""', 'Id'))),
2656+
false);");
2657+
}
2658+
25562659
#endregion
25572660

25582661
#region PostgreSQL extensions

test/EFCore.PG.NodaTime.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,25 @@ public void GenerateCodeLiteral_returns_tstzrange_Interval_literal()
208208
CodeLiteral(new Interval(new LocalDateTime(2020, 01, 01, 12, 0, 0).InUtc().ToInstant(), null)));
209209
}
210210

211+
[Fact]
212+
public void GenerateCodeLiteral_returns_string_array_literal()
213+
{
214+
Assert.Equal(
215+
@"new[] { ""1"", ""2"", ""3"" }",
216+
CodeLiteral(new[] { "1", "2", "3"})
217+
);
218+
}
219+
220+
[Fact]
221+
public void GenerateCodeLiteral_returns_int_list_literal()
222+
{
223+
Assert.Equal(
224+
@"System.Linq.Enumerable.ToList(new string[] { ""1"", ""2"", ""3"" })",
225+
CodeLiteral(new List<string> { "1", "2", "3"})
226+
);
227+
}
228+
229+
211230
[Fact]
212231
public void Interval_array_is_properly_mapped()
213232
{

0 commit comments

Comments
 (0)