Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -1243,9 +1243,20 @@ private void Initialize(

if (!column.TryGetDefaultValue(out var defaultValue))
{
defaultValue = null;
// for non-nullable collections of primitives that are mapped to JSON we set a default value corresponding to empty JSON collection
defaultValue = !inline
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this logic move into GetDefaultValue instead, which is the code responsible for determining default values? It would have to accept the column as opposed to just the CLR type, of course.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code looks messy because it's conflating the default value that should be used when a new row is added with the default value that should be used when a new required column is added or an existing column is made required.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roji This is a long standing issue with the way Migrations uses default values. See #27084.

&& column is { IsNullable: false, StoreTypeMapping: { ElementTypeMapping: not null, Converter: ValueConverter columnValueConverter } }
&& columnValueConverter.GetType() is Type { IsGenericType: true } columnValueConverterType
&& columnValueConverterType.GetGenericTypeDefinition() == typeof(CollectionToJsonStringConverter<>)
? "[]"
: null;
}

columnOperation.DefaultValue = defaultValue
?? (inline || isNullable
? null
: GetDefaultValue(columnOperation.ClrType));
columnOperation.DefaultValueSql = column.DefaultValueSql;
columnOperation.ColumnType = column.StoreType;
columnOperation.MaxLength = column.MaxLength;
columnOperation.Precision = column.Precision;
Expand All @@ -1254,11 +1265,6 @@ private void Initialize(
columnOperation.IsFixedLength = column.IsFixedLength;
columnOperation.IsRowVersion = column.IsRowVersion;
columnOperation.IsNullable = isNullable;
columnOperation.DefaultValue = defaultValue
?? (inline || isNullable
? null
: GetDefaultValue(columnOperation.ClrType));
columnOperation.DefaultValueSql = column.DefaultValueSql;
columnOperation.ComputedColumnSql = column.ComputedColumnSql;
columnOperation.IsStored = column.IsStored;
columnOperation.Comment = column.Comment;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10770,6 +10770,337 @@ CONSTRAINT [PK_HistoryTable] PRIMARY KEY ([Id])
""");
}

[ConditionalFact]
public virtual async Task Add_required_primitve_collection_to_existing_table()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers").IsRequired();
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'[]';
""");
}

[ConditionalFact]
public virtual async Task Add_required_primitve_collection_with_custom_default_value_to_existing_table()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers").IsRequired().HasDefaultValue(new List<int> { 1, 2, 3 });
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'[1,2,3]';
""");
}

[ConditionalFact]
public virtual async Task Add_required_primitve_collection_with_custom_default_value_sql_to_existing_table()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers").IsRequired().HasDefaultValueSql("N'[3, 2, 1]'");
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT (N'[3, 2, 1]');
""");
}

[ConditionalFact(Skip = "issue #33038")]
public virtual async Task Add_required_primitve_collection_with_custom_converter_to_existing_table()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers").HasConversion(new ValueConverter<List<int>, string>(
convertToProviderExpression: x => x != null && x.Count > 0 ? "some numbers" : "nothing",
convertFromProviderExpression: x => x == "nothing" ? new List<int> { } : new List<int> { 7, 8, 9 }))
.IsRequired();
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'nothing';
""");
}

[ConditionalFact]
public virtual async Task Add_required_primitve_collection_with_custom_converter_and_custom_default_value_to_existing_table()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers").HasConversion(new ValueConverter<List<int>, string>(
convertToProviderExpression: x => x != null && x.Count > 0 ? "some numbers" : "nothing",
convertFromProviderExpression: x => x == "nothing" ? new List<int> { } : new List<int> { 7, 8, 9 }))
.HasDefaultValue(new List<int> { 42 })
.IsRequired();
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NOT NULL DEFAULT N'some numbers';
""");
}

[ConditionalFact]
public virtual async Task Add_optional_primitive_collection_to_existing_table()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.ToTable("Customers");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers");
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
ALTER TABLE [Customers] ADD [Numbers] nvarchar(max) NULL;
""");
}

[ConditionalFact]
public virtual async Task Create_table_with_required_primitive_collection()
{
await Test(
builder => { },
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers").IsRequired();
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[Numbers] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
""");
}

[ConditionalFact]
public virtual async Task Create_table_with_optional_primitive_collection()
{
await Test(
builder => { },
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.HasKey("Id");
e.Property<string>("Name");
e.Property<List<int>>("Numbers");
e.ToTable("Customers");
}),
model =>
{
var customersTable = Assert.Single(model.Tables.Where(t => t.Name == "Customers"));

Assert.Collection(
customersTable.Columns,
c => Assert.Equal("Id", c.Name),
c => Assert.Equal("Name", c.Name),
c => Assert.Equal("Numbers", c.Name));
Assert.Same(
customersTable.Columns.Single(c => c.Name == "Id"),
Assert.Single(customersTable.PrimaryKey!.Columns));
});

AssertSql(
"""
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[Numbers] nvarchar(max) NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
""");
}

protected override string NonDefaultCollation
=> _nonDefaultCollation ??= GetDatabaseCollation() == "German_PhoneBook_CI_AS"
? "French_CI_AS"
Expand Down