diff --git a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs index f145b0bc5c6..1621ab3248e 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerEntityTypeExtensions.cs @@ -252,8 +252,7 @@ public static void SetHistoryTableName(this IMutableEntityType entityType, strin public static string? GetHistoryTableSchema(this IReadOnlyEntityType entityType) => (entityType is RuntimeEntityType) ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) - : entityType[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string - ?? entityType[RelationalAnnotationNames.Schema] as string; + : entityType[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? entityType.GetSchema(); /// /// 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..b392d9010d2 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -534,6 +534,7 @@ protected override void Generate( { var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? model?.GetDefaultSchema(); + 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..e0bc2efc3f1 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() @@ -2844,8 +3091,7 @@ await Test( @"ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [historySchema].[HistoryTable]))"); } - [ConditionalFact] - public virtual async Task Rename_history_table() + public virtual async Task Rename_temporal_table_schema_when_history_table_doesnt_have_its_schema_specified() { await Test( builder => builder.Entity( @@ -2856,31 +3102,695 @@ await Test( 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", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); + e.ToTable("Customers", "mySchema2"); }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("RenamedHistoryTable"); - ttb.HasPeriodStart("Start"); + 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 Remove_columns_from_temporal_table_with_history_table_schema() + { + 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", "myHistorySchema"); + 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); @@ -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,84 @@ 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(); + } + + [ConditionalFact] + public virtual async Task Temporal_table_with_default_global_schema_changing_global_schema() + { + await Test( + builder => + { + builder.Entity( + "Customer", e => + { + e.Property("Id"); + e.Property("Name"); + + e.ToTable( + "Customers", tb => tb.IsTemporal()); + }); + }, + builder => + { + builder.HasDefaultSchema("myDefaultSchema"); + }, + builder => + { + builder.HasDefaultSchema("myModifiedDefaultSchema"); + }, + model => + { + Assert.Equal(1, model.Tables.Count); + var customers = model.Tables.First(t => t.Name == "Customers"); + Assert.Equal("myModifiedDefaultSchema", customers.Schema); + Assert.Equal("myModifiedDefaultSchema", customers[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + }); + + AssertSql( + @"IF SCHEMA_ID(N'myModifiedDefaultSchema') IS NULL EXEC(N'CREATE SCHEMA [myModifiedDefaultSchema];');", + // + @"ALTER TABLE [myDefaultSchema].[Customers] SET (SYSTEM_VERSIONING = OFF)", + // + @"ALTER SCHEMA [myModifiedDefaultSchema] TRANSFER [myDefaultSchema].[Customers];", + // + @"ALTER SCHEMA [myModifiedDefaultSchema] TRANSFER [myDefaultSchema].[CustomersHistory];", + // + @"ALTER TABLE [myModifiedDefaultSchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myModifiedDefaultSchema].[CustomersHistory]))"); + } + protected override string NonDefaultCollation => _nonDefaultCollation ??= GetDatabaseCollation() == "German_PhoneBook_CI_AS" ? "French_CI_AS"