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

[main] Fix to #32062 - Altering a nullable column to not null generates invalid SQL commands for migration #32102

Merged
merged 1 commit into from
Oct 24, 2023
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 @@ -2473,6 +2473,15 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema
// if only difference is in temporal annotations being removed or history table changed etc - we can ignore this operation
if (!CanSkipAlterColumnOperation(alterColumnOperation.OldColumn, alterColumnOperation))
{
// for alter column operation converting column from nullable to non-nullable in the temporal table
// we must disable versioning in order to properly handle it
// specifically, for null -> non-null, switching values in history table from
// null to the default value for the non-nullable column
if (alterColumnOperation.OldColumn.IsNullable && !alterColumnOperation.IsNullable)
{
DisableVersioning(table!, schema, historyTableName!, historyTableSchema, suppressTransaction);
}

operations.Add(operation);

// when modifying a period column, we need to perform the operations as a normal column first, and only later enable period
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6637,6 +6637,162 @@ await Test(
""");
}

[ConditionalFact]
public virtual async Task Convert_regular_column_of_temporal_table_from_nullable_to_non_nullable()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<DateTime>("Start").ValueGeneratedOnAddOrUpdate();
e.Property<DateTime>("End").ValueGeneratedOnAddOrUpdate();
e.HasKey("Id");

e.ToTable(
tb => tb.IsTemporal(
ttb =>
{
ttb.UseHistoryTable("HistoryTable");
ttb.HasPeriodStart("Start");
ttb.HasPeriodEnd("End");
}));

// adding data to make sure default for null value can be applied correctly
e.HasData(
new { Id = 1, IsVip = (bool?)true },
new { Id = 2, IsVip = (bool?)false },
new { Id = 3, IsVip = (bool?)null });
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<bool?>("IsVip");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<bool>("IsVip");
}),
model =>
{
var table = Assert.Single(model.Tables);
Assert.Equal("Customer", table.Name);
Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]);
Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);

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

AssertSql(
"""
ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF)
""",
//
"""
DECLARE @var0 sysname;
SELECT @var0 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'IsVip');
IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var0 + '];');
UPDATE [Customer] SET [IsVip] = CAST(0 AS bit) WHERE [IsVip] IS NULL;
ALTER TABLE [Customer] ALTER COLUMN [IsVip] bit NOT NULL;
ALTER TABLE [Customer] ADD DEFAULT CAST(0 AS bit) FOR [IsVip];
""",
//
"""
DECLARE @var1 sysname;
SELECT @var1 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[HistoryTable]') AND [c].[name] = N'IsVip');
IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];');
UPDATE [HistoryTable] SET [IsVip] = CAST(0 AS bit) WHERE [IsVip] IS NULL;
ALTER TABLE [HistoryTable] ALTER COLUMN [IsVip] bit NOT NULL;
ALTER TABLE [HistoryTable] ADD DEFAULT CAST(0 AS bit) FOR [IsVip];
""",
//
"""
DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')
""");
}

[ConditionalFact]
public virtual async Task Convert_regular_column_of_temporal_table_to_sparse()
{
await Test(
builder => builder.Entity(
"Customer", e =>
{
e.Property<int>("Id").ValueGeneratedOnAdd();
e.Property<DateTime>("Start").ValueGeneratedOnAddOrUpdate();
e.Property<DateTime>("End").ValueGeneratedOnAddOrUpdate();
e.HasKey("Id");

e.ToTable(
tb => tb.IsTemporal(
ttb =>
{
ttb.UseHistoryTable("HistoryTable");
ttb.HasPeriodStart("Start");
ttb.HasPeriodEnd("End");
}));
e.HasData(
new { MyColumn = 1 },
new { MyColumn = 2 },
new { MyColumn = (int?)null },
new { MyColumn = (int?)null });
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int?>("MyColumn");
}),
builder => builder.Entity(
"Customer", e =>
{
e.Property<int?>("MyColumn").IsSparse();
}),
model =>
{
var table = Assert.Single(model.Tables);
Assert.Equal("Customer", table.Name);
Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]);
Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]);
Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]);
Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]);

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

AssertSql(
"""
DECLARE @var0 sysname;
SELECT @var0 = [d].[name]
FROM [sys].[default_constraints] [d]
INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'MyColumn');
IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var0 + '];');
ALTER TABLE [Customer] ALTER COLUMN [MyColumn] int SPARSE NULL;
""");
}

[ConditionalFact]
public virtual async Task Create_temporal_table_with_comments()
{
Expand Down
Loading