From 669fef0658ca45ab24983ae81e5ff92dba4fa916 Mon Sep 17 00:00:00 2001 From: maumar Date: Fri, 14 Jan 2022 14:09:11 -0800 Subject: [PATCH] Fix to #26676 - EF Core 6 - IsTemporal adds an unnecessary migration when DefaultSchema is configured at the DbContext Level Problem was a discrepancy between RelationalModel build in runtime and the current model (from OnModelCreating) - relational model would set history table schema annotation using the table schema or default schema, however current model wouldn't set those annotations. Model differ would then pick up on those differences and create migration to set the schema for history table. When building actual migration sql we already compensated for the difference and not generate actual sql code, but the problem persists. Fix is to add missing default annotations in the model finalization step. This is a temporary measure until we implement default value conventions (#9329) Fixes #26676 --- .../SqlServerEntityTypeExtensions.cs | 2 +- .../SqlServerConventionSetBuilder.cs | 1 + .../SqlServerTemporalConvention.cs | 24 +- .../SqlServerMigrationsSqlGenerator.cs | 7 +- .../Migrations/ModelSnapshotSqlServerTest.cs | 3 +- .../Internal/CSharpDbContextGeneratorTest.cs | 1 + .../Migrations/MigrationsSqlServerTest.cs | 1253 +++++++++++++++-- 7 files changed, 1158 insertions(+), 133 deletions(-) diff --git a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs index f145b0bc5c6..a6642454502 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs @@ -253,7 +253,7 @@ public static void SetHistoryTableName(this IMutableEntityType entityType, strin => (entityType is RuntimeEntityType) ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) : entityType[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string - ?? entityType[RelationalAnnotationNames.Schema] as string; + ?? entityType[RelationalAnnotationNames.Schema] as string; /// /// Sets a value representing the schema of the history table associated with the entity mapped to a temporal table. diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs index 6967f1d515b..655f1ca1963 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerConventionSetBuilder.cs @@ -118,6 +118,7 @@ public override ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions, (SharedTableConvention)new SqlServerSharedTableConvention(Dependencies, RelationalDependencies)); conventionSet.ModelFinalizingConventions.Add(new SqlServerDbFunctionConvention(Dependencies, RelationalDependencies)); + conventionSet.ModelFinalizingConventions.Add(sqlServerTemporalConvention); ReplaceConvention( conventionSet.ModelFinalizedConventions, diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs index 04c694daddb..f5d842fba58 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerTemporalConvention.cs @@ -13,10 +13,10 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// Accessing SQL Server and SQL Azure databases with EF Core /// for more information and examples. /// -public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConvention, ISkipNavigationForeignKeyChangedConvention +public class SqlServerTemporalConvention : IEntityTypeAnnotationChangedConvention, ISkipNavigationForeignKeyChangedConvention, IModelFinalizingConvention { - private const string PeriodStartDefaultName = "PeriodStart"; - private const string PeriodEndDefaultName = "PeriodEnd"; + private const string DefaultPeriodStartName = "PeriodStart"; + private const string DefaultPeriodEndName = "PeriodEnd"; /// /// Creates a new instance of . @@ -56,12 +56,12 @@ public virtual void ProcessEntityTypeAnnotationChanged( { if (entityTypeBuilder.Metadata.GetPeriodStartPropertyName() == null) { - entityTypeBuilder.HasPeriodStart(PeriodStartDefaultName); + entityTypeBuilder.HasPeriodStart(DefaultPeriodStartName); } if (entityTypeBuilder.Metadata.GetPeriodEndPropertyName() == null) { - entityTypeBuilder.HasPeriodEnd(PeriodEndDefaultName); + entityTypeBuilder.HasPeriodEnd(DefaultPeriodEndName); } foreach (var skipLevelNavigation in entityTypeBuilder.Metadata.GetSkipNavigations()) @@ -138,4 +138,18 @@ public virtual void ProcessSkipNavigationForeignKeyChanged( joinEntityType.SetIsTemporal(true); } } + + /// + public virtual void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes().Where(e => e.IsTemporal())) + { + // Needed for the annotation to show up in the model snapshot - issue #9329 + // history table name will always be non-null for temporal table case + entityType.Builder.UseHistoryTableName(entityType.GetHistoryTableName()!); + entityType.Builder.UseHistoryTableSchema(entityType.GetHistoryTableSchema()); + } + } } diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 255e4fdc683..45b01396e09 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -533,7 +533,8 @@ protected override void Generate( if (operation[SqlServerAnnotationNames.IsTemporal] as bool? == true) { var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string - ?? model?.GetDefaultSchema(); + ?? operation.Schema; + var needsExec = historyTableSchema == null; var subBuilder = needsExec ? new MigrationCommandListBuilder(Dependencies) @@ -1261,7 +1262,7 @@ protected override void Generate( { var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string - ?? model?.GetDefaultSchema(); + ?? operation.Schema ?? model?.GetDefaultSchema(); var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; @@ -2261,7 +2262,7 @@ private IReadOnlyList RewriteOperations( schema ??= model?.GetDefaultSchema(); var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string - ?? model?.GetDefaultSchema(); + ?? schema; var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 2779ba35261..e715b2c5bbe 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -2123,6 +2123,7 @@ public virtual void Temporal_table_information_is_stored_in_snapshot_minimal_set b.ToTable(tb => tb.IsTemporal(ttb => { + ttb.UseHistoryTable(""EntityWithStringPropertyHistory""); ttb .HasPeriodStart(""PeriodStart"") .HasColumnName(""PeriodStart""); @@ -2138,7 +2139,7 @@ public virtual void Temporal_table_information_is_stored_in_snapshot_minimal_set "Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringProperty"); var annotations = temporalEntity.GetAnnotations().ToList(); - Assert.Equal(5, annotations.Count); + Assert.Equal(6, annotations.Count); Assert.Contains(annotations, a => a.Name == SqlServerAnnotationNames.IsTemporal && a.Value as bool? == true); Assert.Contains( annotations, diff --git a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs index b0a56787422..68edda4fa08 100644 --- a/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Scaffolding/Internal/CSharpDbContextGeneratorTest.cs @@ -1062,6 +1062,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { entity.ToTable(tb => tb.IsTemporal(ttb => { + ttb.UseHistoryTable(""CustomerHistory""); ttb .HasPeriodStart(""PeriodStart"") .HasColumnName(""PeriodStart""); diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 9e317426c62..c3388e4958d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -2433,6 +2433,253 @@ PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) ) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[CustomersHistory]));"); } + [ConditionalFact] + public virtual async Task Create_temporal_table_with_default_model_schema() + { + await Test( + builder => { }, + builder => + { + builder.HasDefaultSchema("myDefaultSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("myDefaultSchema", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("myDefaultSchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + 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( + @"IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');", + // + @"CREATE TABLE [myDefaultSchema].[Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomersHistory]));"); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_default_model_schema_specified_after_entity_definition() + { + await Test( + builder => { }, + builder => + { + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + + builder.Entity("Customer", e => e.ToTable("Customers", "mySchema1")); + builder.Entity("Customer", e => e.ToTable("Customers")); + builder.HasDefaultSchema("myDefaultSchema"); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("myDefaultSchema", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("myDefaultSchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + 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( + @"IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');", + // + @"CREATE TABLE [myDefaultSchema].[Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomersHistory]));"); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_default_model_schema_specified_after_entity_definition_and_history_table_schema_specified_explicitly() + { + await Test( + builder => { }, + builder => + { + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("History", "myHistorySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + + builder.Entity("Customer", e => e.ToTable("Customers", "mySchema1")); + builder.Entity("Customer", e => e.ToTable("Customers")); + builder.HasDefaultSchema("myDefaultSchema"); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("myDefaultSchema", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("History", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("myHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + 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( + @"IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');", + // + @"IF SCHEMA_ID(N'myHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [myHistorySchema];');", + // + @"CREATE TABLE [myDefaultSchema].[Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[History]));"); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_default_model_schema_changed_after_entity_definition() + { + await Test( + builder => { }, + builder => + { + builder.HasDefaultSchema("myFakeSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + + builder.HasDefaultSchema("myDefaultSchema"); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("myDefaultSchema", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("myDefaultSchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + 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( + @"IF SCHEMA_ID(N'myDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myDefaultSchema];');", + // + @"CREATE TABLE [myDefaultSchema].[Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [SystemTimeEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [SystemTimeStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myDefaultSchema].[CustomersHistory]));"); + } + [ConditionalFact] public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_explicit_history_table_schema_not_provided() @@ -2841,46 +3088,709 @@ await Test( // @"ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);", // - @"ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]))"); + @"ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]))"); + } + + public virtual async Task Rename_temporal_table_schema_when_history_table_doesnt_have_its_schema_specified() + { + 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.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema2"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema2", table.Schema); + 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.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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( + @"IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');", + // + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[Customers];", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[HistoryTable];", + // + @"ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[HistoryTable]))"); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_schema_when_history_table_has_its_schema_specified() + { + 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.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable", "myHistorySchema"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema2"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema2", table.Schema); + 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.Equal("myHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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( + @"IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');", + // + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[Customers];", + // + @"ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable]))"); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_schema_and_history_table_name_when_history_table_doesnt_have_its_schema_specified() + { + 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"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema2", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable2"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema2", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable2", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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( + @"IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');", + // + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[Customers];", + // + @"EXEC sp_rename N'[mySchema].[HistoryTable]', N'HistoryTable2'; +ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[HistoryTable2];", + // + @"ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[HistoryTable2]))"); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_schema_and_history_table_name_when_history_table_doesnt_have_its_schema_specified_convention_with_default_global_schema22() + { + await Test( + builder => + { + builder.HasDefaultSchema("defaultSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id"); + e.Property("Name"); + e.HasKey("Id"); + }); + }, + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema2", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable2"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema2", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable2", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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( + @"IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');", + // + @"ALTER TABLE [defaultSchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [defaultSchema].[Customers];", + // + @"EXEC sp_rename N'[defaultSchema].[HistoryTable]', N'HistoryTable2'; +ALTER SCHEMA [mySchema2] TRANSFER [defaultSchema].[HistoryTable2];", + // + @"ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[HistoryTable2]))"); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_schema_and_history_table_name_when_history_table_doesnt_have_its_schema_specified_convention_with_default_global_schema_and_table_schema_corrected() + { + await Test( + builder => + { + builder.HasDefaultSchema("defaultSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id"); + e.Property("Name"); + e.HasKey("Id"); + }); + }, + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + + e.ToTable("Customers", "modifiedSchema"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema2", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable2"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema2", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable2", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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( + @"IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');", + // + @"ALTER TABLE [modifiedSchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [modifiedSchema].[Customers];", + // + @"EXEC sp_rename N'[modifiedSchema].[HistoryTable]', N'HistoryTable2'; +ALTER SCHEMA [mySchema2] TRANSFER [modifiedSchema].[HistoryTable2];", + // + @"ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[HistoryTable2]))"); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_schema_when_history_table_doesnt_have_its_schema_specified_convention_with_default_global_schema_and_table_name_corrected() + { + await Test( + builder => + { + builder.HasDefaultSchema("defaultSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id"); + e.Property("Name"); + e.HasKey("Id"); + }); + }, + builder => builder.Entity( + "Customer", e => + { + e.ToTable("MockCustomers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + + e.ToTable("Customers", "mySchema"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers", "mySchema2", tb => tb.IsTemporal(ttb => + { + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema2", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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( + @"IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];');", + // + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[Customers];", + // + @"ALTER SCHEMA [mySchema2] TRANSFER [mySchema].[CustomersHistory];", + // + @"ALTER TABLE [mySchema2].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[CustomersHistory]))"); + } + + [ConditionalFact] + public virtual async Task Rename_history_table() + { + 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"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("RenamedHistoryTable"); + 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("RenamedHistoryTable", 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'[HistoryTable]', N'RenamedHistoryTable';"); + } + + [ConditionalFact] + public virtual async Task Change_history_table_schema() + { + 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"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "modifiedHistorySchema"); + 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("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("modifiedHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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( + @"IF SCHEMA_ID(N'modifiedHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [modifiedHistorySchema];');", + // + @"ALTER SCHEMA [modifiedHistorySchema] TRANSFER [historySchema].[HistoryTable];"); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_history_table_and_their_schemas() + { + 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"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable( + "Customers", "schema", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable( + "RenamedCustomers", "newSchema", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("RenamedHistoryTable", "newHistorySchema"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("RenamedCustomers", table.Name); + Assert.Equal("newSchema", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("RenamedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("newHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + 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];", + // + @"IF SCHEMA_ID(N'newSchema') IS NULL EXEC(N'CREATE SCHEMA [newSchema];');", + // + @"EXEC sp_rename N'[Customers]', N'RenamedCustomers'; +ALTER SCHEMA [newSchema] TRANSFER [RenamedCustomers];", + // + @"IF SCHEMA_ID(N'newHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [newHistorySchema];');", + // + @"EXEC sp_rename N'[historySchema].[HistoryTable]', N'RenamedHistoryTable'; +ALTER SCHEMA [newHistorySchema] TRANSFER [historySchema].[RenamedHistoryTable];", + // + @"ALTER TABLE [newSchema].[RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);", + // + @"ALTER TABLE [newSchema].[RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [newHistorySchema].[RenamedHistoryTable]))"); + } + + [ConditionalFact] + public virtual async Task Remove_columns_from_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( + "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 => + { + }, + 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)); + 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'Name'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Name];", + // + @"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'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Name];", + // + @"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'Number'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number];", + // + @"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'[HistoryTable]') AND [c].[name] = N'Number'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Number];", + // + @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); } [ConditionalFact] - public virtual async Task Rename_history_table() + public virtual async Task Remove_columns_from_temporal_table_with_history_table_schema() { 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.ToTable("Customers", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable", "myHistorySchema"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); }), builder => builder.Entity( "Customer", e => { - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("RenamedHistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); + e.Property("Name"); + e.Property("Number"); }), + builder => + { + }, model => { var table = Assert.Single(model.Tables); @@ -2888,58 +3798,82 @@ await Test( Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("RenamedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + 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("Id", c.Name)); Assert.Same( table.Columns.Single(c => c.Name == "Id"), Assert.Single(table.PrimaryKey!.Columns)); }); AssertSql( - @"EXEC sp_rename N'[HistoryTable]', N'RenamedHistoryTable';"); + @"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'Name'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Name];", + // + @"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'[myHistorySchema].[HistoryTable]') AND [c].[name] = N'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [myHistorySchema].[HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [myHistorySchema].[HistoryTable] DROP COLUMN [Name];", + // + @"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'Number'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number];", + // + @"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'[myHistorySchema].[HistoryTable]') AND [c].[name] = N'Number'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [myHistorySchema].[HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [myHistorySchema].[HistoryTable] DROP COLUMN [Number];", + // + @"ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable]))"); } [ConditionalFact] - public virtual async Task Change_history_table_schema() + public virtual async Task Remove_columns_from_temporal_table_with_table_schema() { 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.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); }), builder => builder.Entity( "Customer", e => { - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "modifiedHistorySchema"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); + e.Property("Name"); + e.Property("Number"); }), + builder => + { + }, model => { var table = Assert.Single(model.Tables); @@ -2948,107 +3882,148 @@ await Test( Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("modifiedHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); Assert.Collection( table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); + c => Assert.Equal("Id", c.Name)); Assert.Same( table.Columns.Single(c => c.Name == "Id"), Assert.Single(table.PrimaryKey!.Columns)); }); AssertSql( - @"IF SCHEMA_ID(N'modifiedHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [modifiedHistorySchema];');", + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", // - @"ALTER SCHEMA [modifiedHistorySchema] TRANSFER [historySchema].[HistoryTable];"); + @"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'[mySchema].[Customers]') AND [c].[name] = N'Name'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [mySchema].[Customers] DROP COLUMN [Name];", + // + @"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'[mySchema].[HistoryTable]') AND [c].[name] = N'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [mySchema].[HistoryTable] DROP COLUMN [Name];", + // + @"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'[mySchema].[Customers]') AND [c].[name] = N'Number'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [mySchema].[Customers] DROP COLUMN [Number];", + // + @"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'[mySchema].[HistoryTable]') AND [c].[name] = N'Number'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [mySchema].[HistoryTable] DROP COLUMN [Number];", + // + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[HistoryTable]))"); } [ConditionalFact] - public virtual async Task Rename_temporal_table_history_table_and_their_schemas() + public virtual async Task Remove_columns_from_temporal_table_with_default_schema() { await Test( - builder => builder.Entity( + builder => + { + builder.HasDefaultSchema("myDefaultSchema"); + builder.Entity( "Customer", e => { e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); e.Property("Start").ValueGeneratedOnAddOrUpdate(); e.Property("End").ValueGeneratedOnAddOrUpdate(); e.HasKey("Id"); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable( - "Customers", "schema", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - e.ToTable("Customers"); - }), + e.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }); + }, builder => builder.Entity( "Customer", e => { - e.ToTable( - "RenamedCustomers", "newSchema", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("RenamedHistoryTable", "newHistorySchema"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); + e.Property("Name"); + e.Property("Number"); }), + builder => + { + }, model => { var table = Assert.Single(model.Tables); - Assert.Equal("RenamedCustomers", table.Name); - Assert.Equal("newSchema", table.Schema); + 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("RenamedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("newHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + 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("Id", 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];", - // - @"IF SCHEMA_ID(N'newSchema') IS NULL EXEC(N'CREATE SCHEMA [newSchema];');", + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", // - @"EXEC sp_rename N'[Customers]', N'RenamedCustomers'; -ALTER SCHEMA [newSchema] TRANSFER [RenamedCustomers];", + @"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'[mySchema].[Customers]') AND [c].[name] = N'Name'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [mySchema].[Customers] DROP COLUMN [Name];", // - @"IF SCHEMA_ID(N'newHistorySchema') IS NULL EXEC(N'CREATE SCHEMA [newHistorySchema];');", + @"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'[mySchema].[HistoryTable]') AND [c].[name] = N'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [mySchema].[HistoryTable] DROP COLUMN [Name];", // - @"EXEC sp_rename N'[historySchema].[HistoryTable]', N'RenamedHistoryTable'; -ALTER SCHEMA [newHistorySchema] TRANSFER [historySchema].[RenamedHistoryTable];", + @"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'[mySchema].[Customers]') AND [c].[name] = N'Number'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [mySchema].[Customers] DROP COLUMN [Number];", // - @"ALTER TABLE [newSchema].[RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]);", + @"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'[mySchema].[HistoryTable]') AND [c].[name] = N'Number'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [mySchema].[HistoryTable] DROP COLUMN [Number];", // - @"ALTER TABLE [newSchema].[RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [newHistorySchema].[RenamedHistoryTable]))"); + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[HistoryTable]))"); } [ConditionalFact] - public virtual async Task Remove_columns_from_temporal_table() + public virtual async Task Remove_columns_from_temporal_table_with_different_schemas_on_each_level() { await Test( - builder => builder.Entity( + builder => + { + builder.HasDefaultSchema("myDefaultSchema"); + builder.Entity( "Customer", e => { e.Property("Id").ValueGeneratedOnAdd(); @@ -3056,15 +4031,14 @@ await Test( e.Property("End").ValueGeneratedOnAddOrUpdate(); e.HasKey("Id"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), + e.ToTable("Customers", "mySchema", tb => tb.IsTemporal(ttb => + { + ttb.UseHistoryTable("HistoryTable", "myHistorySchema"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }); + }, builder => builder.Entity( "Customer", e => { @@ -3092,42 +4066,41 @@ await Test( }); AssertSql( - @"ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF)", + @"ALTER TABLE [mySchema].[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'Name'); -IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var0 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Name];", +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Name'); +IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var0 + '];'); +ALTER TABLE [mySchema].[Customers] DROP COLUMN [Name];", // @"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'Name'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [HistoryTable] DROP COLUMN [Name];", +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[myHistorySchema].[HistoryTable]') AND [c].[name] = N'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [myHistorySchema].[HistoryTable] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [myHistorySchema].[HistoryTable] DROP COLUMN [Name];", // @"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'Number'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Number];", +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Number'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [mySchema].[Customers] DROP COLUMN [Number];", // @"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'[HistoryTable]') AND [c].[name] = N'Number'); -IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); -ALTER TABLE [HistoryTable] DROP COLUMN [Number];", +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[myHistorySchema].[HistoryTable]') AND [c].[name] = N'Number'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [myHistorySchema].[HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [myHistorySchema].[HistoryTable] DROP COLUMN [Number];", // - @"DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))')"); + @"ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable]))"); } [ConditionalFact] @@ -4435,6 +5408,40 @@ await Test( @"ALTER SCHEMA [mySchema] TRANSFER [mySchema2].[OrdersHistoryTable];"); } + [ConditionalFact] + public virtual async Task Temporal_table_with_default_global_schema_noop_migtation_doesnt_generate_unnecessary_steps() + { + await Test( + builder => + { + builder.HasDefaultSchema("myDefaultSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id"); + e.Property("Name"); + + e.ToTable( + "Customers", tb => tb.IsTemporal()); + }); + }, + builder => + { + }, + builder => + { + }, + model => + { + Assert.Equal(1, model.Tables.Count); + var customers = model.Tables.First(t => t.Name == "Customers"); + Assert.Equal("myDefaultSchema", customers.Schema); + Assert.Equal("myDefaultSchema", customers[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + }); + + AssertSql(); + } + protected override string NonDefaultCollation => _nonDefaultCollation ??= GetDatabaseCollation() == "German_PhoneBook_CI_AS" ? "French_CI_AS"