diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index 7734540838a..e8c2670cd64 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -264,13 +264,12 @@ public override IEnumerable For(IColumn column, bool designTime) ? periodEndProperty.GetColumnName(storeObjectIdentifier) : periodEndPropertyName; - if (column.Name == periodStartColumnName - || column.Name == periodEndColumnName) - { - yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true); - yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName, periodStartColumnName); - yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName); - } + // TODO: issue #27459 - we want to avoid having those annotations on every column + yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true); + yield return new Annotation(SqlServerAnnotationNames.TemporalHistoryTableName, entityType.GetHistoryTableName()); + yield return new Annotation(SqlServerAnnotationNames.TemporalHistoryTableSchema, entityType.GetHistoryTableSchema()); + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName, periodStartColumnName); + yield return new Annotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName, periodEndColumnName); } } } diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs index 9ef5261bad3..f94295e8c5d 100644 --- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs @@ -104,4 +104,35 @@ public override IEnumerable ForRename(ITable table) table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); } } + + /// + public override IEnumerable ForRename(IColumn column) + { + if (column.Table[SqlServerAnnotationNames.IsTemporal] as bool? == true) + { + yield return new Annotation(SqlServerAnnotationNames.IsTemporal, true); + + yield return new Annotation( + SqlServerAnnotationNames.TemporalHistoryTableName, + column.Table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + yield return new Annotation( + SqlServerAnnotationNames.TemporalHistoryTableSchema, + column.Table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + if (column[SqlServerAnnotationNames.TemporalPeriodStartColumnName] is string periodStartColumnName) + { + yield return new Annotation( + SqlServerAnnotationNames.TemporalPeriodStartColumnName, + periodStartColumnName); + } + + if (column[SqlServerAnnotationNames.TemporalPeriodEndColumnName] is string periodEndColumnName) + { + yield return new Annotation( + SqlServerAnnotationNames.TemporalPeriodEndColumnName, + periodEndColumnName); + } + } + } } diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 1645b116e79..c51474cd220 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -2355,21 +2355,47 @@ 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)) { + operations.Add(operation); + // when modifying a period column, we need to perform the operations as a normal column first, and only later enable period // removing the period information now, so that when we generate SQL that modifies the column we won't be making them auto generated as period // (making column auto generated is not allowed in ALTER COLUMN statement) // in later operation we enable the period and the period columns get set to auto generated automatically - if (alterColumnOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true - && alterColumnOperation.OldColumn[SqlServerAnnotationNames.IsTemporal] is null) + // + // if the column is not period we just remove temporal information - it's no longer needed and could affect the generated sql + // we will generate all the necessary operations involved with temporal tables here + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema); + + // this is the case where we are not converting from normal table to temporal + // just a normal modification to a column on a temporal table + // in that case we need to double check if we need have disabled versioning earlier in this migration + // if so, we need to mirror the operation to the history table + if (alterColumnOperation.OldColumn[SqlServerAnnotationNames.IsTemporal] as bool? == true) { - alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); - alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); - alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema); + + if (versioningMap.ContainsKey((table, schema))) + { + var alterHistoryTableColumn = CopyColumnOperation(alterColumnOperation); + alterHistoryTableColumn.Table = historyTableName!; + alterHistoryTableColumn.Schema = historyTableSchema; + alterHistoryTableColumn.OldColumn = CopyColumnOperation(alterColumnOperation.OldColumn); + alterHistoryTableColumn.OldColumn.Table = historyTableName!; + alterHistoryTableColumn.OldColumn.Schema = historyTableSchema; + + operations.Add(alterHistoryTableColumn); + } // TODO: test what happens if default value just changes (from temporal to temporal) } - - operations.Add(operation); } break; @@ -2396,6 +2422,8 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema break; case AddColumnOperation addColumnOperation: + operations.Add(addColumnOperation); + // when adding a period column, we need to add it as a normal column first, and only later enable period // removing the period information now, so that when we generate SQL that adds the column we won't be making them auto generated as period // it won't work, unless period is enabled @@ -2403,6 +2431,8 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema if (addColumnOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true) { addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName); + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema); addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); @@ -2411,14 +2441,48 @@ alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema { addColumnOperation.DefaultValue = DateTime.MaxValue; } + + // when adding (non-period) column to an exisiting temporal table we need to check if we have disabled the period + // due to some other operations in the same migration (e.g. delete column) + // if so, we need to also add the same column to history table + if (addColumnOperation.Name != periodStartColumnName + && addColumnOperation.Name != periodEndColumnName) + { + if (versioningMap.ContainsKey((table, schema))) + { + var addHistoryTableColumnOperation = CopyColumnOperation(addColumnOperation); + addHistoryTableColumnOperation.Table = historyTableName!; + addHistoryTableColumnOperation.Schema = historyTableSchema; + + operations.Add(addHistoryTableColumnOperation); + } + } + } + + break; + + case RenameColumnOperation renameColumnOperation: + operations.Add(renameColumnOperation); + + // if we disabled period for the temporal table and now we are renaming the column, + // we need to also rename this same column in history table + if (versioningMap.ContainsKey((table, schema))) + { + var renameHistoryTableColumnOperation = new RenameColumnOperation + { + IsDestructiveChange = renameColumnOperation.IsDestructiveChange, + Name = renameColumnOperation.Name, + NewName = renameColumnOperation.NewName, + Table = historyTableName!, + Schema = historyTableSchema + }; + + operations.Add(renameHistoryTableColumnOperation); } - operations.Add(addColumnOperation); break; default: - // CreateTableOperation - // RenameColumnOperation operations.Add(operation); break; } @@ -2607,6 +2671,7 @@ static bool CanSkipAlterColumnOperation(ColumnOperation first, ColumnOperation s && ColumnOperationsOnlyDifferByTemporalTableAnnotation(first, second) && ColumnOperationsOnlyDifferByTemporalTableAnnotation(second, first); + // don't compare name, table or schema - they are not being set in the model differ (since they should always be the same) static bool ColumnPropertiesAreTheSame(ColumnOperation first, ColumnOperation second) => first.ClrType == second.ClrType && first.Collation == second.Collation @@ -2651,5 +2716,39 @@ static bool ColumnOperationsOnlyDifferByTemporalTableAnnotation(ColumnOperation || a.Name == SqlServerAnnotationNames.TemporalPeriodStartColumnName || a.Name == SqlServerAnnotationNames.TemporalPeriodEndColumnName); } + + static TOperation CopyColumnOperation(ColumnOperation source) + where TOperation : ColumnOperation, new() + { + var result = new TOperation + { + ClrType = source.ClrType, + Collation = source.Collation, + ColumnType = source.ColumnType, + Comment = source.Comment, + ComputedColumnSql = source.ComputedColumnSql, + DefaultValue = source.DefaultValue, + DefaultValueSql = source.DefaultValueSql, + IsDestructiveChange = source.IsDestructiveChange, + IsFixedLength = source.IsFixedLength, + IsNullable = source.IsNullable, + IsRowVersion = source.IsRowVersion, + IsStored = source.IsStored, + IsUnicode = source.IsUnicode, + MaxLength = source.MaxLength, + Name = source.Name, + Precision = source.Precision, + Scale = source.Scale, + Table = source.Table, + Schema = source.Schema + }; + + foreach (var annotation in source.GetAnnotations()) + { + result.AddAnnotation(annotation.Name, annotation.Value); + } + + return result; + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 36394b3efb6..dada1fa78a0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -3229,6 +3229,88 @@ await Test( EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); } + [ConditionalFact] + public virtual async Task Rename_temporal_table_rename_and_modify_column_in_same_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Discount"); + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("DoB"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Discount").HasComment("for VIP only"); + e.Property("DateOfBirth"); + e.ToTable("RenamedCustomers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("RenamedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Discount", c.Name), + c => Assert.Equal("DateOfBirth", 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 [Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];", + // + @"EXEC sp_rename N'[Customers]', N'RenamedCustomers';", + // + @"EXEC sp_rename N'[RenamedCustomers].[DoB]', N'DateOfBirth', N'COLUMN';", + // + @"EXEC sp_rename N'[HistoryTable].[DoB]', N'DateOfBirth', N'COLUMN';", + // + @"DECLARE @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'for VIP only'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'RenamedCustomers', 'COLUMN', N'Discount';", + // + @"DECLARE @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'for VIP only'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'HistoryTable', 'COLUMN', N'Discount';", + // + @"ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); + } + [ConditionalFact] public virtual async Task Rename_temporal_table_with_custom_history_table_schema() { @@ -5296,6 +5378,104 @@ await Test( @"EXEC sp_rename N'[Customer].[End]', N'ModifiedEnd', N'COLUMN';"); } + [ConditionalFact] + public virtual async Task Alter_period_column_of_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => { }, + builder => builder.Entity("Customer").Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + 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 @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'Customers', 'COLUMN', N'End';"); + } + + [ConditionalFact] + public virtual async Task Rename_regular_columns_of_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable(tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + }), + 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("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + @"EXEC sp_rename N'[Customer].[Name]', N'FullName', N'COLUMN';"); + } + [ConditionalFact] public virtual async Task Create_temporal_table_with_comments() { @@ -6008,6 +6188,585 @@ await Test( @"ALTER TABLE [myModifiedDefaultSchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myModifiedDefaultSchema].[CustomersHistory]))"); } + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_delete_columns_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + e.Property("Dob"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + e.Property("DateOfBirth"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("DateOfBirth", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + @"ALTER TABLE [Customers] 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'[Customers]') AND [c].[name] = N'Number'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number];", + // + @"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'Number'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Number];", + // + @"EXEC sp_rename N'[Customers].[Name]', N'FullName', N'COLUMN';", + // + @"EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', N'COLUMN';", + // + @"EXEC sp_rename N'[Customers].[Dob]', N'DateOfBirth', N'COLUMN';", + // + @"EXEC sp_rename N'[HistoryTable].[Dob]', N'DateOfBirth', N'COLUMN';", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); + } + + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_delete_columns_and_also_rename_table_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable("ModifiedCustomers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("ModifiedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + @"ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers];", + // + @"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'[Customers]') AND [c].[name] = N'Number'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number];", + // + @"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'Number'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Number];", + // + @"EXEC sp_rename N'[Customers]', N'ModifiedCustomers';", + // + @"EXEC sp_rename N'[ModifiedCustomers].[Name]', N'FullName', N'COLUMN';", + // + @"EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', N'COLUMN';", + // + @"ALTER TABLE [ModifiedCustomers] ADD CONSTRAINT [PK_ModifiedCustomers] PRIMARY KEY ([Id]);", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [ModifiedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); + } + + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_delete_columns_and_also_rename_history_table_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("ModifiedHistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("ModifiedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + @"ALTER TABLE [Customers] 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'[Customers]') AND [c].[name] = N'Number'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number];", + // + @"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'Number'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Number];", + // + @"EXEC sp_rename N'[Customers].[Name]', N'FullName', N'COLUMN';", + // + @"EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', N'COLUMN';", + // + @"EXEC sp_rename N'[HistoryTable]', N'ModifiedHistoryTable';", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[ModifiedHistoryTable]))')"); + } + + [ConditionalFact] + public virtual async Task Temporal_table_delete_column_and_add_another_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("DateOfBirth"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("DateOfBirth", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + @"ALTER TABLE [Customers] 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'[Customers]') AND [c].[name] = N'Number'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number];", + // + @"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'Number'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Number];", + // + @"ALTER TABLE [Customers] ADD [DateOfBirth] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000';", + // + @"ALTER TABLE [HistoryTable] ADD [DateOfBirth] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000';", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); + } + + [ConditionalFact] + public virtual async Task Temporal_table_delete_column_and_alter_another_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + e.Property("DateOfBirth"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasComment("My comment"); + e.Property("DateOfBirth"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("DateOfBirth", 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 [Customers] 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'[Customers]') AND [c].[name] = N'Number'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number];", + // + @"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'Number'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Number];", + // + @"DECLARE @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'Customers', 'COLUMN', N'Name';", + // + @"DECLARE @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'HistoryTable', 'COLUMN', N'Name';", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); + } + + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_alter_period_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + 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( + @"EXEC sp_rename N'[Customers].[Start]', N'ModifiedStart', N'COLUMN';", + // + @"DECLARE @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'Customers', 'COLUMN', N'End';"); + } + + [ConditionalFact] + public virtual async Task Temporal_table_delete_column_rename_and_alter_period_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("DateOfBirth"); + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + 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 [Customers] 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'[Customers]') AND [c].[name] = N'DateOfBirth'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [DateOfBirth];", + // + @"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'DateOfBirth'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [DateOfBirth];", + // + @"EXEC sp_rename N'[Customers].[Start]', N'ModifiedStart', N'COLUMN';", + // + @"EXEC sp_rename N'[HistoryTable].[Start]', N'ModifiedStart', N'COLUMN';", + // + @"DECLARE @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'Customers', 'COLUMN', N'End';", + // + @"DECLARE @defaultSchema AS sysname; +SET @defaultSchema = SCHEMA_NAME(); +DECLARE @description AS sql_variant; +SET @description = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description, 'SCHEMA', @defaultSchema, 'TABLE', N'HistoryTable', 'COLUMN', N'End';", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); + } + protected override string NonDefaultCollation => _nonDefaultCollation ??= GetDatabaseCollation() == "German_PhoneBook_CI_AS" ? "French_CI_AS"