From 9b69535eb68fd6bec7a3648d6fea792d62bbb91a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 01:23:51 +0000 Subject: [PATCH 1/8] Initial plan From d7a6c8336a9c84af3fde0ff22c4f4cc3177366ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 01:46:06 +0000 Subject: [PATCH 2/8] Fix DEFAULT constraint being applied to temporal history tables Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Migrations/SqlServerMigrationsSqlGenerator.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index d57bfa6b187..9f3b2583bb7 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -3427,6 +3427,13 @@ static TOperation CopyColumnOperation(ColumnOperation source) foreach (var annotation in source.GetAnnotations()) { + // For history tables in temporal tables, exclude DEFAULT constraint annotations + // because history tables should not have DEFAULT constraints + if (annotation.Name == RelationalAnnotationNames.DefaultConstraintName) + { + continue; + } + result.AddAnnotation(annotation.Name, annotation.Value); } From e61598d591b33de4310b14f8a25c45ddd98fc25b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:45:11 +0000 Subject: [PATCH 3/8] Add test for temporal table default constraint fix Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../MigrationsSqlServerTest.TemporalTables.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index 76f8b865a1e..afe820ebeed 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -10174,6 +10174,90 @@ 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 => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50).HasDefaultValue("DefaultName"); + 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("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(100).HasDefaultValue("DefaultName"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + 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 @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'CREATE TABLE [Customer] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(50) NOT NULL DEFAULT N''DefaultName'', + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); +""", + // + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(100) NOT NULL; +""", + // + """ +ALTER TABLE [CustomerHistory] ALTER COLUMN [Name] nvarchar(100) NOT NULL; +""", + // + """ +DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') """); } } From bc51f0dfa5da39bf5678f6892da47f95e07d2755 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:18:01 +0000 Subject: [PATCH 4/8] Fix temporal table default constraint removal to disable versioning Modified AlterColumnOperation handling to detect when default values are being removed and disable versioning in that case. Updated test to remove default value in target model and adjusted expected SQL output. Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Migrations/SqlServerMigrationsSqlGenerator.cs | 14 ++++++-------- .../MigrationsSqlServerTest.TemporalTables.cs | 11 ++++++++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 9f3b2583bb7..c388cd9521f 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 != null || alterColumnOperation.OldColumn.DefaultValueSql != null) + && alterColumnOperation.DefaultValue == null && alterColumnOperation.DefaultValueSql == null; + + if (changeToNonNullable || changeToSparse || removingDefaultValue) { DisableVersioning( tableName!, @@ -3427,13 +3432,6 @@ static TOperation CopyColumnOperation(ColumnOperation source) foreach (var annotation in source.GetAnnotations()) { - // For history tables in temporal tables, exclude DEFAULT constraint annotations - // because history tables should not have DEFAULT constraints - if (annotation.Name == RelationalAnnotationNames.DefaultConstraintName) - { - continue; - } - result.AddAnnotation(annotation.Name, annotation.Value); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index afe820ebeed..514526903e3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -10201,7 +10201,7 @@ await Test( "Customer", e => { e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name").HasMaxLength(100).HasDefaultValue("DefaultName"); + e.Property("Name").HasMaxLength(100); // Remove default value e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); e.HasKey("Id"); @@ -10245,6 +10245,15 @@ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) // """ ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @var sysname; +SELECT @var = [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 @var IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var + '];'); """, // """ From 996ea21bda35906a32e7a8f3ced894db826ed5a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:22:07 +0000 Subject: [PATCH 5/8] Add additional tests for temporal table column facet modifications Added tests for adding default constraints and changing default constraint values on temporal table columns to provide comprehensive coverage of default constraint handling scenarios. Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../MigrationsSqlServerTest.TemporalTables.cs | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index 514526903e3..393a1175a5e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -10267,6 +10267,191 @@ FROM [sys].[default_constraints] [d] """ DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_add_default_constraint_to_column() + { + await Test( + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50); + 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("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50).HasDefaultValue("DefaultName"); // Add default value + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + 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 @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'CREATE TABLE [Customer] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(50) NULL, + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); +""", + // + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(50) NOT NULL; +""", + // + """ +ALTER TABLE [CustomerHistory] ALTER COLUMN [Name] nvarchar(50) NOT NULL; +""", + // + """ +ALTER TABLE [Customer] ADD DEFAULT N'DefaultName' FOR [Name]; +""", + // + """ +DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_change_default_constraint_value() + { + await Test( + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50).HasDefaultValue("OldDefault"); + 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("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50).HasDefaultValue("NewDefault"); // Change default value + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + 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 @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'CREATE TABLE [Customer] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(50) NOT NULL DEFAULT N'OldDefault', + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); +""", + // + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @var sysname; +SELECT @var = [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 @var IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var + '];'); +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(50) NOT NULL; +""", + // + """ +ALTER TABLE [CustomerHistory] ALTER COLUMN [Name] nvarchar(50) NOT NULL; +""", + // + """ +ALTER TABLE [Customer] ADD DEFAULT N'NewDefault' FOR [Name]; +""", + // + """ +DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') """); } } From 1870d5c359089643423868630d062e2736cdd68c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 23:54:29 +0000 Subject: [PATCH 6/8] Refactor temporal table tests to use common setup and add HasDefaultValueSql test Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../MigrationsSqlServerTest.TemporalTables.cs | 137 +++++++++++++----- 1 file changed, 101 insertions(+), 36 deletions(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index 393a1175a5e..d43f1c8824c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -10181,12 +10181,10 @@ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) public virtual async Task Temporal_table_with_default_constraint_can_alter_column() { await Test( - builder => { }, builder => builder.Entity( "Customer", e => { e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name").HasMaxLength(50).HasDefaultValue("DefaultName"); e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); e.HasKey("Id"); @@ -10200,17 +10198,12 @@ await Test( builder => builder.Entity( "Customer", e => { - e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50).HasDefaultValue("DefaultName"); + }), + builder => builder.Entity( + "Customer", e => + { e.Property("Name").HasMaxLength(100); // Remove default value - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable(tb => tb.IsTemporal(ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); }), model => { @@ -10274,12 +10267,10 @@ DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) public virtual async Task Temporal_table_add_default_constraint_to_column() { await Test( - builder => { }, builder => builder.Entity( "Customer", e => { e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name").HasMaxLength(50); e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); e.HasKey("Id"); @@ -10293,17 +10284,12 @@ await Test( builder => builder.Entity( "Customer", e => { - e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50); + }), + builder => builder.Entity( + "Customer", e => + { e.Property("Name").HasMaxLength(50).HasDefaultValue("DefaultName"); // Add default value - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable(tb => tb.IsTemporal(ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); }), model => { @@ -10362,12 +10348,10 @@ DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) public virtual async Task Temporal_table_change_default_constraint_value() { await Test( - builder => { }, builder => builder.Entity( "Customer", e => { e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name").HasMaxLength(50).HasDefaultValue("OldDefault"); e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); e.HasKey("Id"); @@ -10381,17 +10365,12 @@ await Test( builder => builder.Entity( "Customer", e => { - e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasMaxLength(50).HasDefaultValue("OldDefault"); + }), + builder => builder.Entity( + "Customer", e => + { e.Property("Name").HasMaxLength(50).HasDefaultValue("NewDefault"); // Change default value - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable(tb => tb.IsTemporal(ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); }), model => { @@ -10452,6 +10431,92 @@ FROM [sys].[default_constraints] [d] """ DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') +"""); + } + + [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( + """ +DECLARE @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'CREATE TABLE [Customer] ( + [Id] int NOT NULL IDENTITY, + [CreatedDate] datetime2 NOT NULL DEFAULT (GETDATE()), + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); +""", + // + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @var sysname; +SELECT @var = [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 @var IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var + '];'); +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [CreatedDate] datetime2 NOT NULL; +""", + // + """ +ALTER TABLE [CustomerHistory] ALTER COLUMN [CreatedDate] datetime2 NOT NULL; +""", + // + """ +DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') """); } } From 7af5f146429bede2125d85ae03901496762a4eaa Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 27 Aug 2025 18:21:44 -0700 Subject: [PATCH 7/8] Fix baselines --- .../MigrationsSqlServerTest.TemporalTables.cs | 137 ++++-------------- 1 file changed, 31 insertions(+), 106 deletions(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs index d43f1c8824c..a79aa6cd441 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -10225,41 +10225,32 @@ await Test( AssertSql( """ -DECLARE @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) -EXEC(N'CREATE TABLE [Customer] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(50) NOT NULL DEFAULT N''DefaultName'', - [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, - [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, - CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); -""", - // - """ ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) """, // """ -DECLARE @var sysname; -SELECT @var = [d].[name] +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 @var IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var + '];'); -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(100) NOT NULL; +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT ' + @var2 + ';'); +ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(100) NULL; """, // """ -ALTER TABLE [CustomerHistory] ALTER COLUMN [Name] nvarchar(100) NOT 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 + N'.[CustomerHistory]))') +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + '.[CustomerHistory]))') """); } @@ -10311,36 +10302,13 @@ await Test( AssertSql( """ -DECLARE @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) -EXEC(N'CREATE TABLE [Customer] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(50) NULL, - [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, - [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, - CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); -""", - // - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(50) NOT NULL; -""", - // - """ -ALTER TABLE [CustomerHistory] ALTER COLUMN [Name] nvarchar(50) NOT NULL; -""", - // - """ +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]; -""", - // - """ -DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') """); } @@ -10392,45 +10360,13 @@ await Test( AssertSql( """ -DECLARE @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) -EXEC(N'CREATE TABLE [Customer] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(50) NOT NULL DEFAULT N'OldDefault', - [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, - [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, - CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); -""", - // - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @var sysname; -SELECT @var = [d].[name] +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 @var IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var + '];'); -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(50) NOT NULL; -""", - // - """ -ALTER TABLE [CustomerHistory] ALTER COLUMN [Name] nvarchar(50) NOT NULL; -""", - // - """ +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT ' + @var1 + ';'); ALTER TABLE [Customer] ADD DEFAULT N'NewDefault' FOR [Name]; -""", - // - """ -DECLARE @historyTableSchema1 nvarchar(max) = QUOTENAME(SCHEMA_NAME()) -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + N'.[CustomerHistory]))') """); } @@ -10482,41 +10418,30 @@ await Test( AssertSql( """ -DECLARE @historyTableSchema nvarchar(max) = QUOTENAME(SCHEMA_NAME()) -EXEC(N'CREATE TABLE [Customer] ( - [Id] int NOT NULL IDENTITY, - [CreatedDate] datetime2 NOT NULL DEFAULT (GETDATE()), - [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, - [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, - CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema + N'.[CustomerHistory]))'); -""", - // - """ ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) """, // """ -DECLARE @var sysname; -SELECT @var = [d].[name] +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 @var IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var + '];'); -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [CreatedDate] datetime2 NOT NULL; +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT ' + @var2 + ';'); """, // """ -ALTER TABLE [CustomerHistory] ALTER COLUMN [CreatedDate] datetime2 NOT 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'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 + N'.[CustomerHistory]))') +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + @historyTableSchema1 + '.[CustomerHistory]))') """); } } From 8d1fbe6f9de6ccaa9ae1831348b132c235fb94a5 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 28 Aug 2025 16:52:11 -0700 Subject: [PATCH 8/8] Update src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs Co-authored-by: Shay Rojansky --- .../Migrations/SqlServerMigrationsSqlGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index c388cd9521f..78859480516 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -3120,8 +3120,8 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema // 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 != null || alterColumnOperation.OldColumn.DefaultValueSql != null) - && alterColumnOperation.DefaultValue == null && alterColumnOperation.DefaultValueSql == null; + 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) {