Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for code generation for List<X> types #2402

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/EFCore.PG/Internal/EnumerableMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,32 @@ protected override void ConfigureParameter(DbParameter parameter)
}
}

#region Code Generation
public override Expression GenerateCodeLiteral(object value)
{
var values = (IList)value;
List<Expression> 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
Expand Down
103 changes: 103 additions & 0 deletions test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>("Id");
e.HasKey("Id");
e.Property<List<string>>("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<int>("Id");
e.HasKey("Id");
e.Property<int[]>("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();
Expand Down Expand Up @@ -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<int>("Id");
e.HasKey("Id");
}),
_ => { },
builder => builder.Entity(
"People", e =>
{
e.Property<List<string>>("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();
Expand Down Expand Up @@ -2553,6 +2619,43 @@ SELECT setval(
false);");
}

[ConditionalFact]
public virtual async Task InsertDataOperation_StringList()
{
await Test(
builder =>
{
builder.Entity(
"Person", e =>
{
e.Property<int>("Id");
e.Property<List<string>>("Values");
e.HasKey("Id");
});
},
_ => { },
builder =>
{
builder.Entity("Person").HasData(
new { Id = 1, Values = new List<string> { "1", "2" }},
new { Id = 2, Values = new List<string> { "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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> { 1, 2, 3})
);
}


[Fact]
public void Interval_array_is_properly_mapped()
{
Expand Down