efcore.pg icon indicating copy to clipboard operation
efcore.pg copied to clipboard

Migration does not specify a default value for non-nullable List<> properties

Open hackf5 opened this issue 1 year ago • 2 comments

Version: Npgsql.EntityFrameworkCore.PostgreSQL 8.0.4

Reproduction:

Add property public List<string> NewArray { get; set; } = []; to entity model.

Add Migration.

Actual:

migrationBuilder.AddColumn<List<string>>(
      name: "new_array",
      table: "some_table",
      type: "text[]",
      nullable: false);

Expected:

migrationBuilder.AddColumn<List<string>>(
      name: "new_array",
      table: "some_table",
      type: "text[]",
      defaultValue: new List<string>(),
      nullable: false);

Because the migration does not specify a default value, when the migration is run, when the table some_table is not empty the migration fails because it attempts to insert null into the non-nullable column.

hackf5 avatar Aug 16 '24 09:08 hackf5

@hackf5 thanks for filing, but can you please show where this is actually affecting your program, e.g. causing an error?

roji avatar Aug 17 '24 16:08 roji

Note added: migration fails due to attempt to insert null into non-nullable column.

hackf5 avatar Aug 17 '24 17:08 hackf5

Sorry for taking so long to answer this.

Unfortunately EF's own infrastructure around handling default values for new columns is lacking - I opened https://github.com/dotnet/efcore/issues/34790 to track that. In the meantime, you can get around this by explicitly specifying a default value on the new column:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property(b => b.NewArray).HasDefaultValue(new List<string> { "bla" });
}

roji avatar Sep 30 '24 05:09 roji

Thanks for opening the issue on ef core.

Explicitly setting a default value is a decent workaround.

On Mon, 30 Sept 2024, 06:37 Shay Rojansky, @.***> wrote:

Sorry for taking so long to answer this.

Unfortunately EF's own infrastructure around handling default values for new columns is lacking - I opened dotnet/efcore#34790 https://github.com/dotnet/efcore/issues/34790 to track that. In the meantime, you can get around this by explicitly specifying a default value on the new column:

protected override void OnModelCreating(ModelBuilder modelBuilder){ modelBuilder.Entity<Blog>().Property(b => b.NewArray).HasDefaultValue(new List { "bla" });}

— Reply to this email directly, view it on GitHub https://github.com/npgsql/efcore.pg/issues/3244#issuecomment-2382152910, or unsubscribe https://github.com/notifications/unsubscribe-auth/ALU56HJYAPOK6DBDOEPOOITZZDPRXAVCNFSM6AAAAABMTV4VGWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGOBSGE2TEOJRGA . You are receiving this because you were mentioned.Message ID: @.***>

hackf5 avatar Sep 30 '24 07:09 hackf5

@roji workaround adds default value to table definition, but now model differ always reports model is changed and generates bogus migrations

migrationBuilder.AlterColumn<List<Guid>>(
                name: "Risk",
                schema: "schema",
                table: "MyTable",
                type: "uuid[]",
                nullable: false,
                defaultValue: new List<Guid>(),
                oldClrType: typeof(List<Guid>),
                oldType: "uuid[]",
                oldDefaultValue: new List<Guid>());

Probably due to List is reference type with deafault Equals. I tried to hack it around with public class EquatableList : List<Guid>, IEquatable<EquatableList>, IEquatable<List<Guid>> But it refuses to generate migration

System.NotSupportedException: The type mapping for 'EquatableList' has not implemented code literal generation.
   at Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping.GenerateCodeLiteral(Object value)
   at Microsoft.EntityFrameworkCore.Design.Internal.CSharpHelper.UnknownLiteral(Object value)
   at Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationOperationGenerator.Generate(AlterColumnOperation operation, IndentedStringBuilder builder)
... 

OwnageIsMagic avatar Jul 20 '25 07:07 OwnageIsMagic

I ended up with

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    base.ConfigureConventions(configurationBuilder);
    configurationBuilder.Conventions.Add(_ => new NotNullPrimitiveCollectionConvention());
}
internal sealed class NotNullPrimitiveCollectionConvention : IModelFinalizingConvention
{
public void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
{
foreach (var prop in modelBuilder.Metadata.GetEntityTypes()
    .SelectMany(x => x.GetProperties().Where(x => x.IsPrimitiveCollection && !x.IsNullable)))
{
    if (prop.GetDefaultValueSql() == null)
    {
        // prop.SetDefaultValueSql("ARRAY[]::" + prop.GetColumnType());
        // TypeMapping is not available in conventions :(
        Type type = prop.GetElementType()!.ClrType;
        string sqlType;
        if (type == typeof(Guid))
            sqlType = "uuid";
        else
            throw new NotSupportedException(type.FullName);

        prop.SetDefaultValueSql($"ARRAY[]::{sqlType}[]");
    }
}
}
}

OwnageIsMagic avatar Jul 20 '25 08:07 OwnageIsMagic