diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index d57bfa6b187..78859480516 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -3118,7 +3118,12 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema var changeToSparse = alterColumnOperation.OldColumn[SqlServerAnnotationNames.Sparse] as bool? != true && alterColumnOperation[SqlServerAnnotationNames.Sparse] as bool? == true; - if (changeToNonNullable || changeToSparse) + // for alter column removing default value we also need to disable versioning + // because the default constraint needs to be removed from both main and history tables + var removingDefaultValue = (alterColumnOperation.OldColumn.DefaultValue is not null || alterColumnOperation.OldColumn.DefaultValueSql is not null) + && alterColumnOperation.DefaultValue is null && alterColumnOperation.DefaultValueSql is null; + + if (changeToNonNullable || changeToSparse || removingDefaultValue) { DisableVersioning( tableName!, diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index 76f8b865a1e..a79aa6cd441 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -10174,6 +10174,274 @@ CREATE TABLE [Customers] ( [Name] nvarchar(max) NULL, CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) ); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_with_default_constraint_can_alter_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasMaxLength(50).HasDefaultValue("DefaultName"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasMaxLength(100); // Remove default value + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", 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 @var2 nvarchar(max); +SELECT @var2 = QUOTENAME([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'Name'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT ' + @var2 + ';'); +ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(100) NULL; +""", + // + """ +DECLARE @var3 nvarchar(max); +SELECT @var3 = QUOTENAME([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'[CustomerHistory]') AND [c].[name] = N'Name'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [CustomerHistory] DROP CONSTRAINT ' + @var3 + ';'); +ALTER TABLE [CustomerHistory] ALTER COLUMN [Name] nvarchar(100) NULL; +""", + // + """ +DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + '.[CustomerHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_add_default_constraint_to_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasMaxLength(50); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasMaxLength(50).HasDefaultValue("DefaultName"); // Add default value + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @var1 nvarchar(max); +SELECT @var1 = QUOTENAME([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'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT ' + @var1 + ';'); +ALTER TABLE [Customer] ADD DEFAULT N'DefaultName' FOR [Name]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_change_default_constraint_value() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasMaxLength(50).HasDefaultValue("OldDefault"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasMaxLength(50).HasDefaultValue("NewDefault"); // Change default value + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @var1 nvarchar(max); +SELECT @var1 = QUOTENAME([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'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT ' + @var1 + ';'); +ALTER TABLE [Customer] ADD DEFAULT N'NewDefault' FOR [Name]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_remove_default_value_sql_from_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("CreatedDate").HasDefaultValueSql("GETDATE()"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("CreatedDate"); // Remove default value SQL + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("CreatedDate", 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 @var2 nvarchar(max); +SELECT @var2 = QUOTENAME([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'CreatedDate'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT ' + @var2 + ';'); +""", + // + """ +DECLARE @var3 nvarchar(max); +SELECT @var3 = QUOTENAME([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'[CustomerHistory]') AND [c].[name] = N'CreatedDate'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [CustomerHistory] DROP CONSTRAINT ' + @var3 + ';'); +""", + // + """ +DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + '.[CustomerHistory]))') """); } }