diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 28362b77761..40573c91264 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Globalization; using System.Text; using Microsoft.EntityFrameworkCore.SqlServer.Internal; @@ -1599,17 +1600,6 @@ protected override void ColumnDefinition( var isPeriodStartColumn = operation[SqlServerAnnotationNames.TemporalIsPeriodStartColumn] as bool? == true; var isPeriodEndColumn = operation[SqlServerAnnotationNames.TemporalIsPeriodEndColumn] as bool? == true; - // falling back to legacy annotations, in case the migration was generated using pre-9.0 bits - if (!isPeriodStartColumn && !isPeriodEndColumn) - { - if (operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] is string periodStartColumnName - && operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] is string periodEndColumnName) - { - isPeriodStartColumn = operation.Name == periodStartColumnName; - isPeriodEndColumn = operation.Name == periodEndColumnName; - } - } - if (isPeriodStartColumn || isPeriodEndColumn) { builder.Append(" GENERATED ALWAYS AS ROW "); @@ -2363,11 +2353,140 @@ private string Uniquify(string variableName, bool increase = true) return _variableCounter == 0 ? variableName : variableName + _variableCounter; } + private IReadOnlyList FixLegacyTemporalAnnotations(IReadOnlyList migrationOperations) + { + // short-circuit for non-temporal migrations (which is the majority) + if (migrationOperations.All(o => o[SqlServerAnnotationNames.IsTemporal] as bool? != true)) + { + return migrationOperations; + } + + var resultOperations = new List(migrationOperations.Count); + foreach (var migrationOperation in migrationOperations) + { + var isTemporal = migrationOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true; + if (!isTemporal) + { + resultOperations.Add(migrationOperation); + continue; + } + + switch (migrationOperation) + { + case CreateTableOperation createTableOperation: + + foreach (var column in createTableOperation.Columns) + { + NormalizeTemporalAnnotationsForAddColumnOperation(column); + } + + resultOperations.Add(migrationOperation); + break; + + case AddColumnOperation addColumnOperation: + NormalizeTemporalAnnotationsForAddColumnOperation(addColumnOperation); + resultOperations.Add(addColumnOperation); + break; + + case AlterColumnOperation alterColumnOperation: + RemoveLegacyTemporalColumnAnnotations(alterColumnOperation); + RemoveLegacyTemporalColumnAnnotations(alterColumnOperation.OldColumn); + if (!CanSkipAlterColumnOperation(alterColumnOperation, alterColumnOperation.OldColumn)) + { + resultOperations.Add(alterColumnOperation); + } + + break; + + case DropColumnOperation dropColumnOperation: + RemoveLegacyTemporalColumnAnnotations(dropColumnOperation); + resultOperations.Add(dropColumnOperation); + break; + + case RenameColumnOperation renameColumnOperation: + RemoveLegacyTemporalColumnAnnotations(renameColumnOperation); + resultOperations.Add(renameColumnOperation); + break; + + default: + resultOperations.Add(migrationOperation); + break; + } + } + + return resultOperations; + + static void NormalizeTemporalAnnotationsForAddColumnOperation(AddColumnOperation addColumnOperation) + { + var periodStartColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; + var periodEndColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + if (periodStartColumnName == addColumnOperation.Name) + { + addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true); + } + else if (periodEndColumnName == addColumnOperation.Name) + { + addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true); + } + + RemoveLegacyTemporalColumnAnnotations(addColumnOperation); + } + + static void RemoveLegacyTemporalColumnAnnotations(MigrationOperation operation) + { + operation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); + } + + static bool CanSkipAlterColumnOperation(ColumnOperation column, ColumnOperation oldColumn) + => ColumnPropertiesAreTheSame(column, oldColumn) && AnnotationsAreTheSame(column, oldColumn); + + // 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 column, ColumnOperation oldColumn) + => column.ClrType == oldColumn.ClrType + && column.Collation == oldColumn.Collation + && column.ColumnType == oldColumn.ColumnType + && column.Comment == oldColumn.Comment + && column.ComputedColumnSql == oldColumn.ComputedColumnSql + && Equals(column.DefaultValue, oldColumn.DefaultValue) + && column.DefaultValueSql == oldColumn.DefaultValueSql + && column.IsDestructiveChange == oldColumn.IsDestructiveChange + && column.IsFixedLength == oldColumn.IsFixedLength + && column.IsNullable == oldColumn.IsNullable + && column.IsReadOnly == oldColumn.IsReadOnly + && column.IsRowVersion == oldColumn.IsRowVersion + && column.IsStored == oldColumn.IsStored + && column.IsUnicode == oldColumn.IsUnicode + && column.MaxLength == oldColumn.MaxLength + && column.Precision == oldColumn.Precision + && column.Scale == oldColumn.Scale; + + static bool AnnotationsAreTheSame(ColumnOperation column, ColumnOperation oldColumn) + { + var columnAnnotations = column.GetAnnotations().ToList(); + var oldColumnAnnotations = oldColumn.GetAnnotations().ToList(); + + if (columnAnnotations.Count != oldColumnAnnotations.Count) + { + return false; + } + + return columnAnnotations.Zip(oldColumnAnnotations) + .All(x => x.First.Name == x.Second.Name + && StructuralComparisons.StructuralEqualityComparer.Equals(x.First.Value, x.Second.Value)); + } + } + private IReadOnlyList RewriteOperations( IReadOnlyList migrationOperations, IModel? model, MigrationsSqlGenerationOptions options) { + migrationOperations = FixLegacyTemporalAnnotations(migrationOperations); + var operations = new List(); var availableSchemas = new List(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index bce6e263c9a..b7998252e12 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -10768,6 +10768,1078 @@ public override async Task Add_required_primitive_collection_to_existing_table() """); } + [ConditionalFact] + public virtual async Task Temporal_convert_regular_table_to_temporal_and_add_rowversion_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + 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("Number"); + e.Property("MyRowVersion").IsRowVersion(); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + 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("Number", c.Name), + c => Assert.Equal("MyRowVersion", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [MyRowVersion] rowversion NULL; +""", + // + """ +ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_create_temporal_table_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), + PeriodEnd = table.Column(type: "datetime2", nullable: false) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), + PeriodStart = table.Column(type: "datetime2", nullable: false) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null!) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => { }, + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", 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 @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomersHistory]))'); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_convert_regular_table_to_temporal_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.AlterTable( + name: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1"); + + migrationBuilder.AddColumn( + name: "PeriodEnd", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AddColumn( + name: "PeriodStart", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodStart] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodEnd] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomersHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_convert_regular_table_with_rowversion_to_temporal_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.AlterTable( + name: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "MyRowVersion", + table: "Customers", + type: "rowversion", + rowVersion: true, + nullable: false, + oldClrType: typeof(byte[]), + oldType: "rowversion", + oldRowVersion: true) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1"); + + migrationBuilder.AddColumn( + name: "PeriodEnd", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AddColumn( + name: "PeriodStart", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("MyRowVersion").IsRowVersion(); + e.ToTable("Customers"); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("MyRowVersion", 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] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodStart] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodEnd] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomersHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_rename_temporal_table_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Customers", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.RenameTable( + name: "Customers", + newName: "RenamedCustomers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null); + + migrationBuilder.AlterTable( + name: "RenamedCustomers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "PeriodStart", + table: "RenamedCustomers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "PeriodEnd", + table: "RenamedCustomers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "RenamedCustomers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "RenamedCustomers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AddPrimaryKey( + name: "PK_RenamedCustomers", + table: "RenamedCustomers", + column: "Id"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "RenamedCustomers"); + Assert.Equal("RenamedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("RenamedCustomersHistory", 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) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; +""", + // + """ +EXEC sp_rename N'[CustomersHistory]', N'RenamedCustomersHistory', 'OBJECT'; +""", + // + """ +ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[RenamedCustomersHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_convert_temporal_table_to_regular_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.DropColumn( + name: "PeriodEnd", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.DropColumn( + name: "PeriodStart", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterTable( + name: "Customers") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + 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) +""", + // + """ +ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +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'[Customers]') AND [c].[name] = N'PeriodEnd'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [PeriodEnd]; +""", + // + """ +DECLARE @var2 sysname; +SELECT @var2 = [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'PeriodStart'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [PeriodStart]; +""", + // + """ +DROP TABLE [CustomersHistory]; +""", + // + """ +DECLARE @var3 sysname; +SELECT @var3 = [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'Name'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [Customers] ALTER COLUMN [Name] nvarchar(max) NOT NULL; +""", + // + """ +DECLARE @var4 sysname; +SELECT @var4 = [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'Id'); +IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var4 + '];'); +ALTER TABLE [Customers] ALTER COLUMN [Id] int NOT NULL; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_add_column_to_temporal_table_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.AddColumn( + name: "MyRowVersion", + table: "Customers", + type: "rowversion", + rowVersion: true, + nullable: false, + defaultValue: new byte[0]) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("MyRowVersion", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [MyRowVersion] rowversion NOT NULL; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_remove_temporal_table_column_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.DropColumn( + name: "IsVip", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("IsVip"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + 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 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'[Customers]') AND [c].[name] = N'IsVip'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [IsVip]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_rename_temporal_table_column_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.RenameColumn( + name: "Name", + table: "Customers", + newName: "FullName") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + 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'[Customers].[Name]', N'FullName', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_rename_temporal_table_period_columns_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.RenameColumn( + name: "PeriodStart", + table: "Customers", + newName: "NewPeriodStart") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.RenameColumn( + name: "PeriodEnd", + table: "Customers", + newName: "NewPeriodEnd") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterTable( + name: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "NewPeriodStart", + table: "Customers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "NewPeriodEnd", + table: "Customers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + 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].[PeriodStart]', N'NewPeriodStart', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Customers].[PeriodEnd]', N'NewPeriodEnd', 'COLUMN'; +"""); + } + [ConditionalFact] public override async Task Add_required_primitive_collection_with_custom_default_value_to_existing_table() {