From 8b52a067cb9658404541c27c6058ea1d70230396 Mon Sep 17 00:00:00 2001 From: maumar Date: Sun, 4 May 2025 02:41:35 -0700 Subject: [PATCH 1/2] Fix to #11502 - Allow to specify constraint name for default values Adding model builder API to allow specifying explicit constraint name when adding default value (sql) to a column. Also added switch on the model level (opt-in) to switch from system-generated constraints to generating them automatically by name and storing in the model. This is opt-in because we don't want to cause unnecessary migrations. Added de-duplication logic in the finalize model step, in case our generated names happen to clash with ones specified explicitly by user. Model builder APIs are exposed on the SQL Server level, but a lot of piping is in relational so that providers who support named constraints can take advantage of the feature. Also split temporal tables migration tests for SqlServer into a separate file - there were way to many tests in MigrationsSqlServerTest Fixes #11502 --- ...nalCSharpRuntimeAnnotationCodeGenerator.cs | 10 +- .../Extensions/RelationalModelExtensions.cs | 44 + .../RelationalPropertyExtensions.cs | 56 + .../RelationalConventionSetBuilder.cs | 6 + .../RelationalDefaultConstraintConvention.cs | 150 + .../Metadata/RelationalAnnotationNames.cs | 12 + .../Properties/RelationalStrings.Designer.cs | 16 + .../Properties/RelationalStrings.resx | 6 + .../SqlServerAnnotationCodeGenerator.cs | 46 + .../SqlServerModelBuilderExtensions.cs | 67 + .../SqlServerPropertyBuilderExtensions.cs | 215 + .../Internal/SqlServerAnnotationProvider.cs | 33 +- .../SqlServerMigrationsAnnotationProvider.cs | 5 + .../SqlServerMigrationsSqlGenerator.cs | 57 +- .../Internal/SqlServerDatabaseModelFactory.cs | 9 + .../Design/CSharpMigrationsGeneratorTest.cs | 20 +- ...nsSqlServerTest.NamedDefaultConstraints.cs | 1048 ++ .../MigrationsSqlServerTest.TemporalTables.cs | 8756 +++++++++++++++++ .../Migrations/MigrationsSqlServerTest.cs | 8748 +--------------- .../SqlServerModelValidatorTest.cs | 66 + 20 files changed, 10613 insertions(+), 8757 deletions(-) create mode 100644 src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index b49811c5e21..e772d17ff9f 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -751,7 +751,15 @@ private void Create( /// The column to which the annotations are applied. /// Additional parameters used during code generation. public virtual void Generate(IColumn column, CSharpRuntimeAnnotationCodeGeneratorParameters parameters) - => GenerateSimpleAnnotations(parameters); + { + if (!parameters.IsRuntime) + { + var annotations = parameters.Annotations; + annotations.Remove(RelationalAnnotationNames.DefaultConstraintName); + } + + GenerateSimpleAnnotations(parameters); + } private void Create( IViewColumn column, diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index d36f9b026d7..efc3a4f26ed 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -530,4 +530,48 @@ public static void SetCollation(this IMutableModel model, string? value) => model.FindAnnotation(RelationalAnnotationNames.Collation)?.GetConfigurationSource(); #endregion Collation + + #region UseNamedDefaultConstraints + + /// + /// Returns the value indicating whether named default constraints should be used. + /// + /// The model to get the value for. + public static bool? AreNamedDefaultConstraintsUsed(this IReadOnlyModel model) + => (model is RuntimeModel) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (bool?)model[RelationalAnnotationNames.UseNamedDefaultConstraints]; + + /// + /// Sets the value indicating whether named default constraints should be used. + /// + /// The model to get the value for. + /// The value to set. + public static void UseNamedDefaultConstraints(this IMutableModel model, bool value) + => model.SetOrRemoveAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints, value); + + /// + /// Sets the value indicating whether named default constraints should be used. + /// + /// The model to get the value for. + /// The value to set. + /// Indicates whether the configuration was specified using a data annotation. + public static bool? UseNamedDefaultConstraints( + this IConventionModel model, + bool value, + bool fromDataAnnotation = false) + => (bool?)model.SetOrRemoveAnnotation( + RelationalAnnotationNames.UseNamedDefaultConstraints, + value, + fromDataAnnotation)?.Value; + + /// + /// Returns the configuration source for the named default constraints setting. + /// + /// The model to find configuration source for. + /// The configuration source for the named default constraints setting. + public static ConfigurationSource? UseNamedDefaultConstraintsConfigurationSource(this IConventionModel model) + => model.FindAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints)?.GetConfigurationSource(); + + #endregion } diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index cd18f05e611..d6421eb70f2 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -2086,4 +2086,60 @@ public static void SetJsonPropertyName(this IMutableProperty property, string? n /// The for the JSON property name for a given entity property. public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionProperty property) => property.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); + + /// + /// Gets the default constraint name. + /// + /// The property. + public static string? GetDefaultConstraintName(this IReadOnlyProperty property) + => (property is RuntimeProperty) + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (string?)property[RelationalAnnotationNames.DefaultConstraintName]; + + /// + /// Generates the default constraint name based on the table and column name. + /// + /// The property. + /// The store object identifier to generate the name from. + public static string GenerateDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var candidate = $"DF_{storeObject.Name}_{property.GetColumnName(storeObject)}"; + + return candidate.Length > 120 ? candidate[..120] : candidate; + } + + /// + /// Sets the default constraint name. + /// + /// The property. + /// The name to be used. + public static void SetDefaultConstraintName(this IMutableProperty property, string? defaultConstraintName) + => property.SetAnnotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName); + + /// + /// Sets the default constraint name. + /// + /// The property. + /// The name to be used. + /// Indicates whether the configuration was specified using a data annotation. + public static string? SetDefaultConstraintName( + this IConventionProperty property, + string? defaultConstraintName, + bool fromDataAnnotation = false) + { + property.SetAnnotation( + RelationalAnnotationNames.DefaultConstraintName, + defaultConstraintName, + fromDataAnnotation); + + return defaultConstraintName; + } + + /// + /// Returns the configuration source for the default constraint name. + /// + /// The property. + /// The configuration source for the default constraint name. + public static ConfigurationSource? GetDefaultConstraintNameConfigurationSource(this IConventionProperty property) + => property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName)?.GetConfigurationSource(); } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index b8e49bb22c2..0531a79b407 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -89,6 +89,12 @@ public override ConventionSet CreateConventionSet() conventionSet.Replace( new RelationalRuntimeModelConvention(Dependencies, RelationalDependencies)); + var defaultConstraintConvention = new RelationalDefaultConstraintConvention(Dependencies, RelationalDependencies); + ConventionSet.AddAfter( + conventionSet.ModelFinalizingConventions, + defaultConstraintConvention, + typeof(SharedTableConvention)); + return conventionSet; } } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs new file mode 100644 index 00000000000..29e5f09f897 --- /dev/null +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that manipulates names of default constraints to avoid clashes. +/// +/// +/// See Model building conventions for more information and examples. +/// +public class RelationalDefaultConstraintConvention : IModelFinalizingConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + /// Parameter object containing relational dependencies for this convention. + public RelationalDefaultConstraintConvention( + ProviderConventionSetBuilderDependencies dependencies, + RelationalConventionSetBuilderDependencies relationalDependencies) + { + Dependencies = dependencies; + RelationalDependencies = relationalDependencies; + } + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + /// Relational provider-specific dependencies for this service. + /// + protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } + + /// + public void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + var explicitDefaultConstraintNames = new List(); + + // store all explicit names first - we don't want to change those in case they conflict with implicit names + foreach (var entity in modelBuilder.Metadata.GetEntityTypes()) + { + foreach (var property in entity.GetDeclaredProperties()) + { + if (property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName) is IConventionAnnotation annotation + && annotation.Value is string explicitDefaultConstraintName + && annotation.GetConfigurationSource() == ConfigurationSource.Explicit) + { + if (property.GetMappedStoreObjects(StoreObjectType.Table).Count() > 1) + { + // for TPC and some entity splitting scenarios (specifically composite key) we end up with multiple tables + // having to define the constraint. Since constraint has to be unique, we can't keep the same name for all + // Disabling this scenario until we have better place to configure the constraint name + // see issue #27970 + throw new InvalidOperationException( + RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc(explicitDefaultConstraintName)); + } + + explicitDefaultConstraintNames.Add(explicitDefaultConstraintName); + } + } + } + + var existingDefaultConstraintNames = new List(explicitDefaultConstraintNames); + var useNamedDefaultConstraints = modelBuilder.Metadata.AreNamedDefaultConstraintsUsed() == true; + + var suffixCounter = 1; + foreach (var entity in modelBuilder.Metadata.GetEntityTypes()) + { + foreach (var property in entity.GetDeclaredProperties()) + { + if (property.FindAnnotation(RelationalAnnotationNames.DefaultValue) is IConventionAnnotation defaultValueAnnotation + || property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql) is IConventionAnnotation defaultValueSqlAnnotation) + { + var defaultConstraintNameAnnotation = property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName); + if (defaultConstraintNameAnnotation != null && defaultConstraintNameAnnotation.GetConfigurationSource() != ConfigurationSource.Convention) + { + // explicit constraint name - we already stored those so nothing to do here + continue; + } + + if (useNamedDefaultConstraints) + { + var mappedTables = property.GetMappedStoreObjects(StoreObjectType.Table); + var mappedTablesCount = mappedTables.Count(); + + if (mappedTablesCount == 0) + { + continue; + } + + if (mappedTablesCount == 1) + { + var constraintNameCandidate = property.GenerateDefaultConstraintName(mappedTables.First()); + if (!existingDefaultConstraintNames.Contains(constraintNameCandidate)) + { + // name that we generate is unique - add it to the list of names but we don't need to store it as annotation + existingDefaultConstraintNames.Add(constraintNameCandidate); + } + else + { + // conflict - generate name that is unique and store is as annotation + while (existingDefaultConstraintNames.Contains(constraintNameCandidate + suffixCounter)) + { + suffixCounter++; + } + + existingDefaultConstraintNames.Add(constraintNameCandidate + suffixCounter); + property.SetDefaultConstraintName(constraintNameCandidate + suffixCounter); + } + + continue; + } + + // TPC or entity splitting - when column is mapped to multiple tables, we can deal with them + // as long as there are no name clashes with some other constraints + // by the time we actually need to generate the constraint name (to put it in the annotation for the migration op) + // we will know which store object the property we are processing is mapped to, so can pick the right name based on that + // here though, where we want to uniquefy the name duplicates, we work on the model level so can't pick the right de-duped name + // so in case of conflict, we have to throw + // see issue #27970 + var constraintNameCandidates = new List(); + foreach (var mappedTable in mappedTables) + { + var constraintNameCandidate = property.GenerateDefaultConstraintName(mappedTable); + if (constraintNameCandidate != null) + { + if (!existingDefaultConstraintNames.Contains(constraintNameCandidate)) + { + constraintNameCandidates.Add(constraintNameCandidate); + } + else + { + throw new InvalidOperationException( + RelationalStrings.ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(constraintNameCandidate)); + } + } + } + + existingDefaultConstraintNames.AddRange(constraintNameCandidates); + } + } + } + } + } +} diff --git a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs index efbf0a4e205..97084eee01a 100644 --- a/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs +++ b/src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs @@ -52,6 +52,16 @@ public static class RelationalAnnotationNames /// public const string DefaultValue = Prefix + "DefaultValue"; + /// + /// The name for default constraint annotations. + /// + public const string DefaultConstraintName = Prefix + "DefaultConstraintName"; + + /// + /// The name for using named default constraints annotations. + /// + public const string UseNamedDefaultConstraints = Prefix + "UseNamedDefaultConstraints"; + /// /// The name for table name annotations. /// @@ -360,6 +370,8 @@ public static class RelationalAnnotationNames ComputedColumnSql, IsStored, DefaultValue, + DefaultConstraintName, + UseNamedDefaultConstraints, TableName, Schema, ViewName, diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 0eee71da13f..0aa88b18eb3 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -827,6 +827,14 @@ public static string ExecuteUpdateSubqueryNotSupportedOverComplexTypes(object? c GetString("ExecuteUpdateSubqueryNotSupportedOverComplexTypes", nameof(complexType)), complexType); + /// + /// Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'. + /// + public static string ExplicitDefaultConstraintNamesNotSupportedForTpc(object? explicitDefaultConstraintName) + => string.Format( + GetString("ExplicitDefaultConstraintNamesNotSupportedForTpc", nameof(explicitDefaultConstraintName)), + explicitDefaultConstraintName); + /// /// The required column '{column}' was not present in the results of a 'FromSql' operation. /// @@ -857,6 +865,14 @@ public static string HasDataNotSupportedForEntitiesMappedToJson(object? entity) GetString("HasDataNotSupportedForEntitiesMappedToJson", nameof(entity)), entity); + /// + /// Named default constraints can't be used with TPC or entity splitting if they result in non-unique constraint name. Constraint name: '{constraintNameCandidate}'. + /// + public static string ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(object? constraintNameCandidate) + => string.Format( + GetString("ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash", nameof(constraintNameCandidate)), + constraintNameCandidate); + /// /// Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the comment '{comment}' does not match the comment '{otherComment}'. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index c5604c77a73..54f1313fe43 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -427,6 +427,9 @@ ExecuteUpdate is being used over a LINQ operator which isn't natively supported by the database; this cannot be translated because complex type '{complexType}' is projected out. Rewrite your query to project out the containing entity type instead. + + Can't use explicitly named default constraints with TPC inheritance or entity splitting. Constraint name: '{explicitDefaultConstraintName}'. + The required column '{column}' was not present in the results of a 'FromSql' operation. @@ -439,6 +442,9 @@ Can't use HasData for entity type '{entity}'. HasData is not supported for entities mapped to JSON. + + Named default constraints can't be used with TPC or entity splitting if they result in non-unique constraint name. Constraint name: '{constraintNameCandidate}'. + Cannot use table '{table}' for entity type '{entityType}' since it is being used for entity type '{otherEntityType}' and the comment '{comment}' does not match the comment '{otherComment}'. diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 01e6918c559..7ce2c2e1bb2 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -201,8 +201,54 @@ public override IReadOnlyList GenerateFluentApiCalls( IProperty property, IDictionary annotations) { + var defaultConstraintNameAnnotation = default(IAnnotation); + var defaultValueAnnotation = default(IAnnotation); + var defaultValueSqlAnnotation = default(IAnnotation); + + // named default constraint must be handled on the provider level - model builder methods live on provider rather than relational + // so removing the annotations before calling base + if (annotations.TryGetValue(RelationalAnnotationNames.DefaultConstraintName, out defaultConstraintNameAnnotation)) + { + if (defaultConstraintNameAnnotation.Value as string != string.Empty) + { + if (annotations.TryGetValue(RelationalAnnotationNames.DefaultValue, out defaultValueAnnotation)) + { + annotations.Remove(RelationalAnnotationNames.DefaultValue); + } + else + { + var defaultValueSqlAnnotationExists = annotations.TryGetValue(RelationalAnnotationNames.DefaultValueSql, out defaultValueSqlAnnotation); + Check.DebugAssert(defaultValueSqlAnnotationExists, "If default constaint name was set, one of DefaultValue or DefaultValueSql must also be set."); + annotations.Remove(RelationalAnnotationNames.DefaultValueSql); + } + } + + annotations.Remove(RelationalAnnotationNames.DefaultConstraintName); + } + var fragments = new List(base.GenerateFluentApiCalls(property, annotations)); + if (defaultConstraintNameAnnotation != null && defaultConstraintNameAnnotation.Value as string != string.Empty) + { + if (defaultValueAnnotation != null) + { + fragments.Add( + new MethodCallCodeFragment( + nameof(SqlServerPropertyBuilderExtensions.HasDefaultValue), + defaultValueAnnotation.Value, + defaultConstraintNameAnnotation.Value)); + } + else + { + Check.NotNull(defaultValueSqlAnnotation, "Both DefaultValue and DefaultValueSql annotations are null."); + fragments.Add( + new MethodCallCodeFragment( + nameof(SqlServerPropertyBuilderExtensions.HasDefaultValueSql), + defaultValueSqlAnnotation.Value, + defaultConstraintNameAnnotation.Value)); + } + } + var isPrimitiveCollection = property.IsPrimitiveCollection; if (GenerateValueGenerationStrategy(annotations, property.DeclaringType.Model, onModel: false, complexType: property.DeclaringType is IComplexType) is MethodCallCodeFragment diff --git a/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs index f615ffad5c3..4ab71074d5f 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerModelBuilderExtensions.cs @@ -651,4 +651,71 @@ public static bool CanSetPerformanceLevelSql( string? performanceLevel, bool fromDataAnnotation = false) => modelBuilder.CanSetAnnotation(SqlServerAnnotationNames.PerformanceLevelSql, performanceLevel, fromDataAnnotation); + + /// + /// Configures the model to use named default constraints. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. + /// + /// The model builder. + /// The value to use. + /// The same builder instance so that multiple calls can be chained. + public static ModelBuilder UseNamedDefaultConstraints(this ModelBuilder modelBuilder, bool value = true) + { + Check.NotNull(value, nameof(value)); + + modelBuilder.Model.UseNamedDefaultConstraints(value); + + return modelBuilder; + } + + /// + /// Configures the model to use named default constraints. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. + /// + /// The model builder. + /// The value to use. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionModelBuilder? UseNamedDefaultConstraints( + this IConventionModelBuilder modelBuilder, + bool value, + bool fromDataAnnotation = false) + { + if (modelBuilder.CanUseNamedDefaultConstraints(value, fromDataAnnotation)) + { + modelBuilder.Metadata.UseNamedDefaultConstraints(value, fromDataAnnotation); + return modelBuilder; + } + + return null; + } + + /// + /// Returns a value indicating whether named default constraints should be used in the model. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. + /// + /// The model builder. + /// Indicates whether the configuration was specified using a data annotation. + /// The value to use. + /// if the given value can be set as the configuration for named default constraints setting. + public static bool CanUseNamedDefaultConstraints( + this IConventionModelBuilder modelBuilder, + bool value, + bool fromDataAnnotation = false) + => modelBuilder.CanSetAnnotation(RelationalAnnotationNames.UseNamedDefaultConstraints, value, fromDataAnnotation); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs index 2f92f859692..9a7a17c1736 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs @@ -812,4 +812,219 @@ public static bool CanSetIsSparse( bool? sparse, bool fromDataAnnotation = false) => property.CanSetAnnotation(SqlServerAnnotationNames.Sparse, sparse, fromDataAnnotation); + + /// + /// Configures the default value for the column that the property maps to when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasDefaultValue( + this PropertyBuilder propertyBuilder, + object? value, + string defaultConstraintName) + { + Check.NotEmpty(defaultConstraintName, nameof(defaultConstraintName)); + + propertyBuilder.Metadata.SetDefaultValue(value); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return propertyBuilder; + } + + /// + /// Configures the default value for the column that the property maps to when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasDefaultValue( + this PropertyBuilder propertyBuilder, + object? value, + string defaultConstraintName) + => (PropertyBuilder)HasDefaultValue((PropertyBuilder)propertyBuilder, value, defaultConstraintName); + + /// + /// Configures the default value for the column that the property maps to when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// Indicates whether the configuration was specified using a data annotation. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionPropertyBuilder? HasDefaultValue( + this IConventionPropertyBuilder propertyBuilder, + object? value, + string defaultConstraintName, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetDefaultValue(value, defaultConstraintName, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetDefaultValue(value, fromDataAnnotation); + return propertyBuilder; + } + + /// + /// Returns a value indicating whether the given value can be set as default for the column. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as default for the column. + public static bool CanSetDefaultValue( + this IConventionPropertyBuilder propertyBuilder, + object? value, + string defaultConstraintName, + bool fromDataAnnotation = false) + => propertyBuilder.CanSetAnnotation( + RelationalAnnotationNames.DefaultValue, + value, + fromDataAnnotation) + && propertyBuilder.CanSetAnnotation( + RelationalAnnotationNames.DefaultConstraintName, + defaultConstraintName, + fromDataAnnotation); + + /// + /// Configures the default value expression for the column that the property maps to when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasDefaultValueSql( + this PropertyBuilder propertyBuilder, + string? sql, + string defaultConstraintName) + { + Check.NotEmpty(defaultConstraintName, nameof(defaultConstraintName)); + + propertyBuilder.Metadata.SetDefaultValueSql(sql); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return propertyBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PropertyBuilder HasDefaultValueSql( + this PropertyBuilder propertyBuilder, + string? sql, + string defaultConstraintName) + => (PropertyBuilder)HasDefaultValueSql((PropertyBuilder)propertyBuilder, sql, defaultConstraintName); + + /// + /// Configures the default value expression for the column that the property maps to when targeting SQL Server. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and Azure SQL databases with EF Core + /// for more information and examples. Also see + /// Sparse columns for + /// general information on SQL Server sparse columns. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// Indicates whether the configuration was specified using a data annotation. + /// The default constraint name. + /// + /// The same builder instance if the configuration was applied, + /// otherwise. + /// + public static IConventionPropertyBuilder? HasDefaultValueSql( + this IConventionPropertyBuilder propertyBuilder, + string? sql, + string defaultConstraintName, + bool fromDataAnnotation = false) + { + if (!propertyBuilder.CanSetDefaultValueSql(sql, defaultConstraintName, fromDataAnnotation)) + { + return null; + } + + propertyBuilder.Metadata.SetDefaultValueSql(sql, fromDataAnnotation); + return propertyBuilder; + } + + /// + /// Returns a value indicating whether the given default value expression can be set for the column. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// Indicates whether the configuration was specified using a data annotation. + /// The default constraint name. + /// if the given default value expression can be set for the column. + public static bool CanSetDefaultValueSql( + this IConventionPropertyBuilder propertyBuilder, + string? sql, + string defaultConstraintName, + bool fromDataAnnotation = false) + => propertyBuilder.CanSetAnnotation( + RelationalAnnotationNames.DefaultValueSql, + sql, + fromDataAnnotation) + && propertyBuilder.CanSetAnnotation( + RelationalAnnotationNames.DefaultConstraintName, + defaultConstraintName, + fromDataAnnotation); } diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index a6fecde4a4f..66478a046d8 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -250,11 +250,36 @@ public override IEnumerable For(IColumn column, bool designTime) } // JSON columns have no property mappings so all annotations that rely on property mappings should be skipped for them - if (column is not JsonColumn - && column.PropertyMappings.FirstOrDefault()?.Property.IsSparse() is bool isSparse) + if (column is not JsonColumn) { - // Model validation ensures that these facets are the same on all mapped properties - yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse); + if (column.PropertyMappings.FirstOrDefault()?.Property.IsSparse() is bool isSparse) + { + // Model validation ensures that these facets are the same on all mapped properties + yield return new Annotation(SqlServerAnnotationNames.Sparse, isSparse); + } + + var mappedProperty = column.PropertyMappings.FirstOrDefault()?.Property; + if (mappedProperty != null) + { + if (mappedProperty.GetDefaultConstraintName() is string defaultConstraintName) + { + // named constraint stored as annotation are either explicitly configured by user + // or generated by EF because of naming duplicates (SqlServerDefaultValueConvention) + yield return new Annotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName); + } + else if (mappedProperty.DeclaringType.Model.AreNamedDefaultConstraintsUsed() == true + && (mappedProperty.FindAnnotation(RelationalAnnotationNames.DefaultValue) != null + || mappedProperty.FindAnnotation(RelationalAnnotationNames.DefaultValueSql) != null)) + { + // named default constraints opt-in + default value (sql) was specified + // generate the default constraint name (based on table and column name) + // it's not stored as annotation, meaning it won't be clashing with other names + // (we already checked that in the finalize model convention step) + yield return new Annotation( + RelationalAnnotationNames.DefaultConstraintName, + mappedProperty.GenerateDefaultConstraintName(table)); + } + } } var entityType = (IEntityType)column.Table.EntityTypeMappings.First().TypeBase; diff --git a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs index c971efad482..8aae2c65c39 100644 --- a/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Migrations/Internal/SqlServerMigrationsAnnotationProvider.cs @@ -47,6 +47,11 @@ public override IEnumerable ForRemove(IColumn column) yield return new Annotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true); } } + + if (column[RelationalAnnotationNames.DefaultConstraintName] is string defaultConstraintName) + { + yield return new Annotation(RelationalAnnotationNames.DefaultConstraintName, defaultConstraintName); + } } /// diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 10ad36fcdb4..4bf6e2ae7aa 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -363,7 +363,9 @@ protected override void Generate( || !Equals(operation.DefaultValue, oldDefaultValue) || operation.DefaultValueSql != oldDefaultValueSql) { - DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, builder); + var oldDefaultConstraintName = operation.OldColumn[RelationalAnnotationNames.DefaultConstraintName] as string; + + DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, oldDefaultConstraintName, builder); (oldDefaultValue, oldDefaultValueSql) = (null, null); } @@ -459,11 +461,13 @@ protected override void Generate( if (!Equals(operation.DefaultValue, oldDefaultValue) || operation.DefaultValueSql != oldDefaultValueSql) { + var defaultConstraintName = operation[RelationalAnnotationNames.DefaultConstraintName] as string; + builder .Append("ALTER TABLE ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) .Append(" ADD"); - DefaultValue(operation.DefaultValue, operation.DefaultValueSql, operation.ColumnType, builder); + DefaultValue(operation.DefaultValue, operation.DefaultValueSql, operation.ColumnType, defaultConstraintName, builder); builder .Append(" FOR ") .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) @@ -1352,7 +1356,9 @@ protected override void Generate( MigrationCommandListBuilder builder, bool terminate = true) { - DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, builder); + var defaultConstraintName = operation[RelationalAnnotationNames.DefaultConstraintName] as string; + + DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, defaultConstraintName, builder); base.Generate(operation, model, builder, terminate: false); if (terminate) @@ -1545,6 +1551,32 @@ protected override void Generate(DeleteDataOperation operation, IModel? model, M protected override void Generate(UpdateDataOperation operation, IModel? model, MigrationCommandListBuilder builder) => GenerateExecWhenIdempotent(builder, b => base.Generate(operation, model, b)); + /// + /// Generates a SQL fragment for the named default constraint of a column. + /// + /// The default value for the column. + /// The SQL expression to use for the column's default constraint. + /// Store/database type of the column. + /// The command builder to use to add the SQL fragment. + /// The constraint name to use to add the SQL fragment. + protected virtual void DefaultValue( + object? defaultValue, + string? defaultValueSql, + string? columnType, + string? constraintName, + MigrationCommandListBuilder builder) + { + if (constraintName != null && (defaultValue != null || defaultValueSql != null)) + { + builder + .Append(" CONSTRAINT [") + .Append(constraintName) + .Append("]"); + } + + base.DefaultValue(defaultValue, defaultValueSql, columnType, builder); + } + /// protected override void SequenceOptions( string? schema, @@ -1637,11 +1669,13 @@ protected override void ColumnDefinition( builder.Append(operation.IsNullable ? " NULL" : " NOT NULL"); + var defaultConstraintName = operation[RelationalAnnotationNames.DefaultConstraintName] as string; + if (!string.Equals(columnType, "rowversion", StringComparison.OrdinalIgnoreCase) && !string.Equals(columnType, "timestamp", StringComparison.OrdinalIgnoreCase)) { // rowversion/timestamp columns cannot have default values, but also don't need them when adding a new column. - DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder); + DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, defaultConstraintName, builder); } var identity = operation[SqlServerAnnotationNames.Identity] as string; @@ -1921,13 +1955,28 @@ protected override void ForeignKeyAction(ReferentialAction referentialAction, Mi /// The schema that contains the table. /// The table that contains the column. /// The column. + /// The name of the default constraint. /// The command builder to use to add the SQL fragment. protected virtual void DropDefaultConstraint( string? schema, string tableName, string columnName, + string? defaultConstraintName, MigrationCommandListBuilder builder) { + if (defaultConstraintName != null) + { + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema)) + .Append(" DROP CONSTRAINT [") + .Append(defaultConstraintName) + .Append("]") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + return; + } + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); var variable = Uniquify("@var"); diff --git a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs index dc3572b09a7..4b6fde64fdf 100644 --- a/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs +++ b/src/EFCore.SqlServer/Scaffolding/Internal/SqlServerDatabaseModelFactory.cs @@ -733,6 +733,8 @@ private void GetColumns( [c].[is_nullable], [c].[is_identity], [dc].[definition] AS [default_sql], + [dc].[name] AS [default_constraint_name], + [dc].[is_system_named] AS [default_constraint_is_system_named], [cc].[definition] AS [computed_sql], [cc].[is_persisted] AS [computed_is_persisted], CAST([e].[value] AS nvarchar(MAX)) AS [comment], @@ -802,6 +804,8 @@ FROM [sys].[views] v var nullable = dataRecord.GetValueOrDefault("is_nullable"); var isIdentity = dataRecord.GetValueOrDefault("is_identity"); var defaultValueSql = dataRecord.GetValueOrDefault("default_sql"); + var defaultConstraintName = dataRecord.GetValueOrDefault("default_constraint_name"); + var defaultConstraintIsSystemNamed = dataRecord.GetValueOrDefault("default_constraint_is_system_named"); var computedValue = dataRecord.GetValueOrDefault("computed_sql"); var computedIsPersisted = dataRecord.GetValueOrDefault("computed_is_persisted"); var comment = dataRecord.GetValueOrDefault("comment"); @@ -875,6 +879,11 @@ FROM [sys].[views] v column[SqlServerAnnotationNames.Sparse] = true; } + if (defaultConstraintName != null && !defaultConstraintIsSystemNamed) + { + column[RelationalAnnotationNames.DefaultConstraintName] = defaultConstraintName; + } + table.Columns.Add(column); } } diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs index 501d53b2301..497d05d20c4 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.cs @@ -72,6 +72,7 @@ public void Test_new_annotations_handled_for_entity_types() RelationalAnnotationNames.DefaultValueSql, RelationalAnnotationNames.ComputedColumnSql, RelationalAnnotationNames.DefaultValue, + RelationalAnnotationNames.DefaultConstraintName, RelationalAnnotationNames.Name, #pragma warning disable CS0618 // Type or member is obsolete RelationalAnnotationNames.SequencePrefix, @@ -99,7 +100,8 @@ public void Test_new_annotations_handled_for_entity_types() #pragma warning disable CS0618 RelationalAnnotationNames.ContainerColumnTypeMapping, #pragma warning restore CS0618 - RelationalAnnotationNames.StoreType + RelationalAnnotationNames.StoreType, + RelationalAnnotationNames.UseNamedDefaultConstraints }; // Add a line here if the code generator is supposed to handle this annotation @@ -260,6 +262,7 @@ public void Test_new_annotations_handled_for_properties() #pragma warning restore CS0618 RelationalAnnotationNames.JsonPropertyName, RelationalAnnotationNames.StoreType, + RelationalAnnotationNames.UseNamedDefaultConstraints }; var columnMapping = $@"{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasColumnType)}(""default_int_mapping"")"; @@ -304,6 +307,10 @@ public void Test_new_annotations_handled_for_properties() RelationalAnnotationNames.DefaultValue, ("1", $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasDefaultValue)}(""1"")") }, + { + RelationalAnnotationNames.DefaultConstraintName, + ("some name", $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.HasDefaultValue)}(""1"", ""some name"")") + }, { RelationalAnnotationNames.IsFixedLength, (true, $@"{columnMapping}{_nl}.{nameof(RelationalPropertyBuilderExtensions.IsFixedLength)}()") @@ -389,12 +396,23 @@ private static void MissingAnnotationCheck( if (!invalidAnnotations.Contains(annotationName)) { var modelBuilder = FakeRelationalTestHelpers.Instance.CreateConventionBuilder(); + var metadataItem = createMetadataItem(modelBuilder); metadataItem.SetAnnotation( annotationName, validAnnotations.ContainsKey(annotationName) ? validAnnotations[annotationName].Value : null); + // code generator for default value with named constraint contains validation + // to check that constraint name must be accompanied by either DefaultValue + // or DefaultValueSql - so we need to add it here also + if (annotationName == RelationalAnnotationNames.DefaultConstraintName) + { + metadataItem.SetAnnotation( + RelationalAnnotationNames.DefaultValue, + validAnnotations[RelationalAnnotationNames.DefaultValue].Value); + } + modelBuilder.FinalizeModel(designTime: true, skipValidation: true); var sb = new IndentedStringBuilder(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs new file mode 100644 index 00000000000..12ac5b58c1c --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs @@ -0,0 +1,1048 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Migrations; + +public partial class MigrationsSqlServerTest : MigrationsTestBase +{ + #region basic operations with explicit name + + [ConditionalFact] + public virtual async Task Named_default_constraints_add_column_with_explicit_name() + { + await Test( + builder => builder.Entity("Entity").Property("Id"), + builder => { }, + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Entity] ADD [Number] int NOT NULL CONSTRAINT [MyConstraint] DEFAULT 7; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_drop_column_with_explicit_name() + { + await Test( + builder => builder.Entity("Entity").Property("Id"), + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + builder => { }, + model => + { + var table = Assert.Single(model.Tables); + var column = Assert.Single(table.Columns); + }); + + AssertSql( +""" +ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraintSql]; +ALTER TABLE [Entity] DROP COLUMN [Guid]; +""", + // + """ +ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraint]; +ALTER TABLE [Entity] DROP COLUMN [Number]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_create_table_with_column_with_explicit_name() + { + await Test( + builder => { }, + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]); + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +CREATE TABLE [Entity] ( + [Id] nvarchar(450) NOT NULL, + [Guid] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT (NEWID()), + [Number] int NOT NULL CONSTRAINT [MyConstraint] DEFAULT 7, + CONSTRAINT [PK_Entity] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_drop_table_with_column_with_explicit_name() + { + await Test( + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + builder => { }, + model => + { + Assert.Empty(model.Tables); + }); + + AssertSql( +""" +DROP TABLE [Entity]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_rename_constraint() + { + await Test( + builder => builder.Entity("Entity").Property("Id"), + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "RenamedConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "RenamedConstraintSql"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("RenamedConstraint", number[RelationalAnnotationNames.DefaultConstraintName]); + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("RenamedConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraint]; +ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [RenamedConstraint] DEFAULT 7 FOR [Number]; +""", + // + """ +ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraintSql]; +ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [RenamedConstraintSql] DEFAULT (NEWID()) FOR [Guid]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_add_explicit_constraint_name() + { + await Test( + builder => builder.Entity("Entity").Property("Id"), + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]); + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +DECLARE @var sysname; +SELECT @var = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Number'); +IF @var IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var + '];'); +ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [MyConstraint] DEFAULT 7 FOR [Number]; +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Guid'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [MyConstraintSql] DEFAULT (NEWID()) FOR [Guid]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_remove_explicit_constraint_name() + { + await Test( + builder => builder.Entity("Entity").Property("Id"), + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Null(number[RelationalAnnotationNames.DefaultConstraintName]); + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Null(guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraint]; +ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL; +ALTER TABLE [Entity] ADD DEFAULT 7 FOR [Number]; +""", + // + """ +ALTER TABLE [Entity] DROP CONSTRAINT [MyConstraintSql]; +ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL; +ALTER TABLE [Entity] ADD DEFAULT (NEWID()) FOR [Guid]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_add_column_with_implicit_name_on_nested_owned() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").OwnsOne("OwnedType", "MyOwned", b => + { + b.OwnsOne("NestedType", "MyNested", bb => + { + bb.Property("Foo"); + }); + }); + }, + builder => { }, + builder => + { + builder.Entity("Entity").OwnsOne("OwnedType", "MyOwned", b => + { + b.OwnsOne("NestedType", "MyNested", bb => + { + bb.Property("Number").HasDefaultValue(7); + bb.Property("Guid").HasDefaultValueSql("NEWID()"); + }); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "MyOwned_MyNested_Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_Entity_MyOwned_MyNested_Number", number[RelationalAnnotationNames.DefaultConstraintName]); + var guid = Assert.Single(table.Columns, c => c.Name == "MyOwned_MyNested_Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_Entity_MyOwned_MyNested_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] ADD [MyOwned_MyNested_Guid] uniqueidentifier NULL CONSTRAINT [DF_Entity_MyOwned_MyNested_Guid] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Entity] ADD [MyOwned_MyNested_Number] int NULL CONSTRAINT [DF_Entity_MyOwned_MyNested_Number] DEFAULT 7; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_add_column_with_explicit_name_and_null_value() + { + await Test( + builder => builder.Entity("Entity").Property("Id"), + builder => { }, + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(null); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql(null); + builder.Entity("Entity").Property("NumberNamed").HasDefaultValue(null, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("GuidNamed").HasDefaultValueSql(null, defaultConstraintName: "MyConstraintSql"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "NumberNamed"); + Assert.Null(number.DefaultValue); + Assert.Null(number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "GuidNamed"); + Assert.Equal("('00000000-0000-0000-0000-000000000000')", guid.DefaultValueSql); + Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] ADD [Guid] uniqueidentifier NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; +""", + // + """ +ALTER TABLE [Entity] ADD [GuidNamed] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT '00000000-0000-0000-0000-000000000000'; +""", + // + """ +ALTER TABLE [Entity] ADD [Number] int NULL; +""", + // + """ +ALTER TABLE [Entity] ADD [NumberNamed] int NULL; +"""); + } + + #endregion + + #region basic operations with implicit name + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_add_column_with_implicit_constraint_name() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("Entity").Property("Id"); + }, + builder => { }, + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_Guid] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Entity] ADD [Number] int NOT NULL CONSTRAINT [DF_Entity_Number] DEFAULT 7; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_drop_column_with_implicit_constraint_name() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("Entity").Property("Id"); + }, + builder => + { + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + builder => { }, + model => + { + var table = Assert.Single(model.Tables); + var column = Assert.Single(table.Columns); + }); + + AssertSql( +""" +ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Guid]; +ALTER TABLE [Entity] DROP COLUMN [Guid]; +""", + // + """ +ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Number]; +ALTER TABLE [Entity] DROP COLUMN [Number]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_create_table_with_column_with_implicit_constraint_name() + { + await Test( + builder => builder.UseNamedDefaultConstraints(), + builder => { }, + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +CREATE TABLE [Entity] ( + [Id] nvarchar(450) NOT NULL, + [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_Guid] DEFAULT (NEWID()), + [Number] int NOT NULL CONSTRAINT [DF_Entity_Number] DEFAULT 7, + CONSTRAINT [PK_Entity] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_drop_table_with_column_with_implicit_constraint_name() + { + await Test( + builder => builder.UseNamedDefaultConstraints(), + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + builder => { }, + model => + { + Assert.Empty(model.Tables); + }); + + AssertSql( +""" +DROP TABLE [Entity]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_rename_column_with_implicit_constraint_name() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("Entity").Property("Id"); + }, + builder => + { + builder.Entity("Entity").Property("Number").HasColumnName("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasColumnName("Guid").HasDefaultValueSql("NEWID()"); + }, + builder => + { + builder.Entity("Entity").Property("Number").HasColumnName("ModifiedNumber").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasColumnName("ModifiedGuid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "ModifiedNumber"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_Entity_ModifiedNumber", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "ModifiedGuid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_Entity_ModifiedGuid", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +EXEC sp_rename N'[Entity].[Number]', N'ModifiedNumber', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Entity].[Guid]', N'ModifiedGuid', 'COLUMN'; +""", + // + """ +ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Number]; +ALTER TABLE [Entity] ALTER COLUMN [ModifiedNumber] int NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_ModifiedNumber] DEFAULT 7 FOR [ModifiedNumber]; +""", + // + """ +ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Guid]; +ALTER TABLE [Entity] ALTER COLUMN [ModifiedGuid] uniqueidentifier NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_ModifiedGuid] DEFAULT (NEWID()) FOR [ModifiedGuid]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_rename_table_with_column_with_implicit_constraint_name() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("Entity").Property("Id"); + }, + builder => + { + builder.Entity("Entity").ToTable("Entities").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").ToTable("Entities").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + builder => + { + builder.Entity("Entity").ToTable("RenamedEntities").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").ToTable("RenamedEntities").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_RenamedEntities_Number", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_RenamedEntities_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entities] DROP CONSTRAINT [PK_Entities]; +""", + // + """ +EXEC sp_rename N'[Entities]', N'RenamedEntities', 'OBJECT'; +""", + // + """ +ALTER TABLE [RenamedEntities] DROP CONSTRAINT [DF_Entities_Number]; +ALTER TABLE [RenamedEntities] ALTER COLUMN [Number] int NOT NULL; +ALTER TABLE [RenamedEntities] ADD CONSTRAINT [DF_RenamedEntities_Number] DEFAULT 7 FOR [Number]; +""", + // + """ +ALTER TABLE [RenamedEntities] DROP CONSTRAINT [DF_Entities_Guid]; +ALTER TABLE [RenamedEntities] ALTER COLUMN [Guid] uniqueidentifier NOT NULL; +ALTER TABLE [RenamedEntities] ADD CONSTRAINT [DF_RenamedEntities_Guid] DEFAULT (NEWID()) FOR [Guid]; +""", + // + """ +ALTER TABLE [RenamedEntities] ADD CONSTRAINT [PK_RenamedEntities] PRIMARY KEY ([Id]); +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_add_opt_in_with_column_with_implicit_constraint_name() + { + await Test( + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + builder => { }, + builder => builder.UseNamedDefaultConstraints(), + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +DECLARE @var sysname; +SELECT @var = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Number'); +IF @var IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var + '];'); +ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_Number] DEFAULT 7 FOR [Number]; +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Entity]') AND [c].[name] = N'Guid'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Entity] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL; +ALTER TABLE [Entity] ADD CONSTRAINT [DF_Entity_Guid] DEFAULT (NEWID()) FOR [Guid]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_remove_opt_in_with_column_with_implicit_constraint_name() + { + await Test( + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + builder => builder.UseNamedDefaultConstraints(), + builder => { }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Null(number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Null(guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Number]; +ALTER TABLE [Entity] ALTER COLUMN [Number] int NOT NULL; +ALTER TABLE [Entity] ADD DEFAULT 7 FOR [Number]; +""", + // + """ +ALTER TABLE [Entity] DROP CONSTRAINT [DF_Entity_Guid]; +ALTER TABLE [Entity] ALTER COLUMN [Guid] uniqueidentifier NOT NULL; +ALTER TABLE [Entity] ADD DEFAULT (NEWID()) FOR [Guid]; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_add_opt_in_with_column_with_explicit_constraint_name() + { + await Test( + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + builder => { }, + builder => builder.UseNamedDefaultConstraints(), + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + // opt-in doesn't make a difference when constraint name is explicitly defined + AssertSql(); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_remove_opt_in_with_column_with_explicit_constraint_name() + { + await Test( + builder => + { + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + builder => builder.UseNamedDefaultConstraints(), + builder => { }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("MyConstraint", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("MyConstraintSql", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + // opt-in doesn't make a difference when constraint name is explicitly defined + AssertSql(); + } + + #endregion + + #region edge/advanced cases (e.g. table sharing, name clashes) + + [ConditionalFact] + public virtual async Task Named_default_constraints_TPT_inheritance_explicit_default_constraint_name() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("RootEntity").UseTptMappingStrategy(); + builder.Entity("RootEntity").ToTable("Roots"); + builder.Entity("RootEntity").Property("Id"); + builder.Entity("BranchEntity").HasBaseType("RootEntity"); + builder.Entity("BranchEntity").ToTable("Branches"); + builder.Entity("LeafEntity").HasBaseType("BranchEntity"); + builder.Entity("LeafEntity").ToTable("Leaves"); + }, + builder => { }, + builder => + { + builder.Entity("BranchEntity").Property("Number").HasDefaultValue(7, defaultConstraintName: "MyConstraint"); + builder.Entity("BranchEntity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraintSql"); + }, + model => + { + var roots = Assert.Single(model.Tables, x => x.Name == "Roots"); + var branches = Assert.Single(model.Tables, x => x.Name == "Branches"); + var leaves = Assert.Single(model.Tables, x => x.Name == "Leaves"); + + var branchGuid = Assert.Single(branches.Columns, x => x.Name == "Guid"); + Assert.Equal("MyConstraintSql", branchGuid[RelationalAnnotationNames.DefaultConstraintName]); + var branchNumber = Assert.Single(branches.Columns, x => x.Name == "Number"); + Assert.Equal("MyConstraint", branchNumber[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Branches] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [MyConstraintSql] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Branches] ADD [Number] int NOT NULL CONSTRAINT [MyConstraint] DEFAULT 7; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_TPT_inheritance_implicit_default_constraint_name() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("RootEntity").UseTptMappingStrategy(); + builder.Entity("RootEntity").ToTable("Roots"); + builder.Entity("RootEntity").Property("Id"); + builder.Entity("BranchEntity").HasBaseType("RootEntity"); + builder.Entity("BranchEntity").ToTable("Branches"); + builder.Entity("LeafEntity").HasBaseType("BranchEntity"); + builder.Entity("LeafEntity").ToTable("Leaves"); + }, + builder => { }, + builder => + { + builder.Entity("BranchEntity").Property("Number").HasDefaultValue(7); + builder.Entity("BranchEntity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var roots = Assert.Single(model.Tables, x => x.Name == "Roots"); + var branches = Assert.Single(model.Tables, x => x.Name == "Branches"); + var leaves = Assert.Single(model.Tables, x => x.Name == "Leaves"); + + var branchGuid = Assert.Single(branches.Columns, x => x.Name == "Guid"); + Assert.Equal("DF_Branches_Guid", branchGuid[RelationalAnnotationNames.DefaultConstraintName]); + var branchNumber = Assert.Single(branches.Columns, x => x.Name == "Number"); + Assert.Equal("DF_Branches_Number", branchNumber[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Branches] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Branches_Guid] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Branches] ADD [Number] int NOT NULL CONSTRAINT [DF_Branches_Number] DEFAULT 7; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_with_opt_in_TPC_inheritance_implicit_default_constraint_name() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("RootEntity").UseTpcMappingStrategy(); + builder.Entity("RootEntity").ToTable("Roots"); + builder.Entity("RootEntity").Property("Id"); + builder.Entity("BranchEntity").HasBaseType("RootEntity"); + builder.Entity("BranchEntity").ToTable("Branches"); + builder.Entity("LeafEntity").HasBaseType("BranchEntity"); + builder.Entity("LeafEntity").ToTable("Leaves"); + }, + builder => { }, + builder => + { + builder.Entity("BranchEntity").Property("Number").HasDefaultValue(7); + builder.Entity("BranchEntity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var roots = Assert.Single(model.Tables, x => x.Name == "Roots"); + var branches = Assert.Single(model.Tables, x => x.Name == "Branches"); + var leaves = Assert.Single(model.Tables, x => x.Name == "Leaves"); + + var branchGuid = Assert.Single(branches.Columns, x => x.Name == "Guid"); + Assert.Equal("DF_Branches_Guid", branchGuid[RelationalAnnotationNames.DefaultConstraintName]); + var branchNumber = Assert.Single(branches.Columns, x => x.Name == "Number"); + Assert.Equal("DF_Branches_Number", branchNumber[RelationalAnnotationNames.DefaultConstraintName]); + + var leafGuid = Assert.Single(leaves.Columns, x => x.Name == "Guid"); + Assert.Equal("DF_Leaves_Guid", leafGuid[RelationalAnnotationNames.DefaultConstraintName]); + var leafNumber = Assert.Single(leaves.Columns, x => x.Name == "Number"); + Assert.Equal("DF_Leaves_Number", leafNumber[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Leaves] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Leaves_Guid] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Leaves] ADD [Number] int NOT NULL CONSTRAINT [DF_Leaves_Number] DEFAULT 7; +""", + // + """ +ALTER TABLE [Branches] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_Branches_Guid] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Branches] ADD [Number] int NOT NULL CONSTRAINT [DF_Branches_Number] DEFAULT 7; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_name_clash_between_explicit_and_implicit_default_constraint_gets_deduplicated() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("Entity").Property("Id"); + builder.Entity("Entity").Property("Number").HasDefaultValue(7, defaultConstraintName: "DF_Entity_Another"); + builder.Entity("Entity").Property("Guid").HasDefaultValueSql("NEWID()", defaultConstraintName: "DF_Entity_YetAnother"); + }, + builder => { }, + builder => + { + builder.Entity("Entity").Property("Another").HasDefaultValue(7); + builder.Entity("Entity").Property("YetAnother").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal("DF_Entity_Another", number[RelationalAnnotationNames.DefaultConstraintName]); + Assert.Equal(7, number.DefaultValue); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("DF_Entity_YetAnother", guid[RelationalAnnotationNames.DefaultConstraintName]); + Assert.Equal("(newid())", guid.DefaultValueSql); + + var another = Assert.Single(table.Columns, c => c.Name == "Another"); + Assert.Equal("DF_Entity_Another1", another[RelationalAnnotationNames.DefaultConstraintName]); + Assert.Equal(7, another.DefaultValue); + + var yetAnother = Assert.Single(table.Columns, c => c.Name == "YetAnother"); + Assert.Equal("DF_Entity_YetAnother1", yetAnother[RelationalAnnotationNames.DefaultConstraintName]); + Assert.Equal("(newid())", yetAnother.DefaultValueSql); + }); + + AssertSql( +""" +ALTER TABLE [Entity] ADD [Another] int NOT NULL CONSTRAINT [DF_Entity_Another1] DEFAULT 7; +""", + // + """ +ALTER TABLE [Entity] ADD [YetAnother] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_YetAnother1] DEFAULT (NEWID()); +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_very_long_implicit_constraint_name_gets_trimmed_and_deduplicated() + { + await Test( + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity", b => + { + b.Property("Id"); + b.OwnsOne("Owned", "YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation", bb => + { + bb.Property("Name"); + }); + }); + }, + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity", b => + { + b.Property("Id"); + b.OwnsOne("Owned", "YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation", bb => + { + bb.Property("Name"); + bb.Property("Prop").HasDefaultValue(7); + bb.Property("AnotherProp").HasDefaultValueSql("NEWID()"); + bb.Property("YetAnotherProp").HasDefaultValue(27); + }); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + var columns = table.Columns.Where(x => x.Name.EndsWith("Prop")); + Assert.Equal(3, columns.Count()); + Assert.True(columns.All(x => x[RelationalAnnotationNames.DefaultConstraintName] != null)); + }); + + AssertSql( +""" +ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_AnotherProp] uniqueidentifier NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLooooooooooooo] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_Prop] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLooooooooooooo1] DEFAULT 7; +""", + // + """ +ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_YetAnotherProp] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLooooooooooooo2] DEFAULT 27; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_funky_table_name_with_implicit_constraint() + { + await Test( + builder => builder.Entity("My Entity").Property("Id"), + builder => { }, + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("My Entity").Property("Number").HasDefaultValue(7); + builder.Entity("My Entity").Property("Guid").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Number"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_My Entity_Number", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Guid"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_My Entity_Guid", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [My Entity] ADD [Guid] uniqueidentifier NOT NULL CONSTRAINT [DF_My Entity_Guid] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [My Entity] ADD [Number] int NOT NULL CONSTRAINT [DF_My Entity_Number] DEFAULT 7; +"""); + } + + [ConditionalFact] + public virtual async Task Named_default_constraints_funky_column_name_with_implicit_constraint() + { + await Test( + builder => builder.Entity("Entity").Property("Id"), + builder => { }, + builder => + { + builder.UseNamedDefaultConstraints(); + builder.Entity("Entity").Property("Num$be<>r").HasDefaultValue(7); + builder.Entity("Entity").Property("Gu!d").HasDefaultValueSql("NEWID()"); + }, + model => + { + var table = Assert.Single(model.Tables); + var number = Assert.Single(table.Columns, c => c.Name == "Num$be<>r"); + Assert.Equal(7, number.DefaultValue); + Assert.Equal("DF_Entity_Num$be<>r", number[RelationalAnnotationNames.DefaultConstraintName]); + + var guid = Assert.Single(table.Columns, c => c.Name == "Gu!d"); + Assert.Equal("(newid())", guid.DefaultValueSql); + Assert.Equal("DF_Entity_Gu!d", guid[RelationalAnnotationNames.DefaultConstraintName]); + }); + + AssertSql( +""" +ALTER TABLE [Entity] ADD [Gu!d] uniqueidentifier NOT NULL CONSTRAINT [DF_Entity_Gu!d] DEFAULT (NEWID()); +""", + // + """ +ALTER TABLE [Entity] ADD [Num$be<>r] int NOT NULL CONSTRAINT [DF_Entity_Num$be<>r] DEFAULT 7; +"""); + } + + #endregion +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs new file mode 100644 index 00000000000..a52a1c59c7b --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.TemporalTables.cs @@ -0,0 +1,8756 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.SqlServer.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Migrations; + +public partial class MigrationsSqlServerTest : MigrationsTestBase +{ + [ConditionalFact] + public virtual async Task Create_temporal_table_default_column_mappings_and_default_history_table() + { + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customer] ( + [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_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))'); +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_custom_column_mappings_and_default_history_table() + { + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart").HasColumnName("Start"); + ttb.HasPeriodEnd("SystemTimeEnd").HasColumnName("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customer] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [End] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [Start] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([Start], [End]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))'); +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_default_column_mappings_and_custom_history_table() + { + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customer] ( + [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_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[HistoryTable]))'); +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_explicitly_defined_schema() + { + 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", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); +""", + // + """ +CREATE TABLE [mySchema].[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 = [mySchema].[CustomersHistory])); +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_no_explicit_table_schema_provided() + { + 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("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_table_schema_provided() + { + 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", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema", table.Schema); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); +""", + // + """ +CREATE TABLE [mySchema].[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 = [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() + { + 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.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", 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].[HistoryTable])); +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_explicit_history_table_schema_provided() + { + 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.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("historySchema", 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'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); +""", + // + """ +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 = [historySchema].[HistoryTable])); +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_default_schema_for_table_and_explicit_history_table_schema_provided() + { + 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("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("historySchema", 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'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); +""", + // + """ +CREATE TABLE [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 = [historySchema].[HistoryTable])); +"""); + } + + [ConditionalFact] + public virtual async Task Drop_temporal_table_default_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"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("Start").HasColumnName("PeriodStart"); + ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd"); + })); + }), + builder => { }, + model => + { + Assert.Empty(model.Tables); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customer]; +""", + // + """ +DROP TABLE [CustomerHistory]; +"""); + } + + [ConditionalFact] + public virtual async Task Drop_temporal_table_custom_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"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start").HasColumnName("PeriodStart"); + ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd"); + })); + }), + builder => { }, + model => + { + Assert.Empty(model.Tables); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customer]; +""", + // + """ +DROP TABLE [HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Drop_temporal_table_custom_history_table_and_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( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("Start").HasColumnName("PeriodStart"); + ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd"); + })); + }), + builder => { }, + model => + { + Assert.Empty(model.Tables); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customer]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_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"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("RenamedCustomers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("RenamedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_rename_and_modify_column_in_same_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Discount"); + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("DoB"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Discount").HasComment("for VIP only"); + e.Property("DateOfBirth"); + e.ToTable("RenamedCustomers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("RenamedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Discount", c.Name), + c => Assert.Equal("DateOfBirth", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; +""", + // + """ +EXEC sp_rename N'[RenamedCustomers].[DoB]', N'DateOfBirth', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[HistoryTable].[DoB]', N'DateOfBirth', 'COLUMN'; +""", + // + """ +DECLARE @defaultSchema2 AS sysname; +SET @defaultSchema2 = SCHEMA_NAME(); +DECLARE @description2 AS sql_variant; +SET @description2 = N'for VIP only'; +EXEC sp_addextendedproperty 'MS_Description', @description2, 'SCHEMA', @defaultSchema2, 'TABLE', N'RenamedCustomers', 'COLUMN', N'Discount'; +""", + // + """ +DECLARE @defaultSchema3 AS sysname; +SET @defaultSchema3 = SCHEMA_NAME(); +DECLARE @description3 AS sql_variant; +SET @description3 = N'for VIP only'; +EXEC sp_addextendedproperty 'MS_Description', @description3, 'SCHEMA', @defaultSchema3, 'TABLE', N'HistoryTable', 'COLUMN', N'Discount'; +""", + // + """ +ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Rename_temporal_table_with_custom_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( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("RenamedCustomers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("RenamedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); +""", + // + """ +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', 'OBJECT'; +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', 'OBJECT'; +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', 'OBJECT'; +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', 'OBJECT'; +"""); + } + + [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', 'OBJECT'; +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', 'OBJECT'; +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 @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'Name'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Name]; +""", + // + """ +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'Name'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Name]; +""", + // + """ +DECLARE @var4 sysname; +SELECT @var4 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Number'); +IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var4 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number]; +""", + // + """ +DECLARE @var5 sysname; +SELECT @var5 = [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 @var5 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var5 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [Number]; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[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); + 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 @var sysname; +SELECT @var = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); +IF @var IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var + '];'); +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 Remove_columns_from_temporal_table_with_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", "mySchema", 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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @var sysname; +SELECT @var = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Name'); +IF @var IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var + '];'); +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 Remove_columns_from_temporal_table_with_default_schema() + { + await Test( + builder => + { + builder.HasDefaultSchema("myDefaultSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + 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.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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @var sysname; +SELECT @var = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Name'); +IF @var IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var + '];'); +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 Remove_columns_from_temporal_table_with_different_schemas_on_each_level() + { + await Test( + builder => + { + builder.HasDefaultSchema("myDefaultSchema"); + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + 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.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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @var sysname; +SELECT @var = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Name'); +IF @var IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var + '];'); +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'[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'[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'[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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable])) +"""); + } + + [ConditionalFact] + public virtual async Task Add_columns_to_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 => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] ADD [Name] nvarchar(max) NULL; +""", + // + """ +ALTER TABLE [Customers] ADD [Number] int NOT NULL DEFAULT 0; +"""); + } + + [ConditionalFact] + public virtual async Task + Convert_temporal_table_with_default_column_mappings_and_custom_history_table_to_normal_table_keep_period_columns() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart"); + e.Property("PeriodEnd"); + e.HasKey("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("PeriodEnd", c.Name), + c => Assert.Equal("PeriodStart", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DROP TABLE [HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_temporal_table_with_default_column_mappings_and_default_history_table_to_normal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodEnd'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customer] DROP COLUMN [PeriodEnd]; +""", + // + """ +DECLARE @var2 sysname; +SELECT @var2 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodStart'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customer] DROP COLUMN [PeriodStart]; +""", + // + """ +DROP TABLE [CustomerHistory]; +"""); + } + + [ConditionalFact] + public virtual async Task + Convert_temporal_table_with_default_column_mappings_and_custom_history_table_to_normal_table_remove_period_columns() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodEnd'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customer] DROP COLUMN [PeriodEnd]; +""", + // + """ +DECLARE @var2 sysname; +SELECT @var2 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodStart'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customer] DROP COLUMN [PeriodStart]; +""", + // + """ +DROP TABLE [HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_temporal_table_with_explicit_history_table_schema_to_normal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart"); + e.Property("PeriodEnd"); + e.HasKey("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("PeriodEnd", c.Name), + c => Assert.Equal("PeriodStart", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_temporal_table_with_explicit_schemas_same_schema_for_table_and_history_to_normal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customer", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "mySchema"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customer", "mySchema"); + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart"); + e.Property("PeriodEnd"); + e.HasKey("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("PeriodEnd", c.Name), + c => Assert.Equal("PeriodStart", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [mySchema].[Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [mySchema].[Customer] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DROP TABLE [mySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_temporal_table_using_custom_default_schema_to_normal_table() + { + await Test( + builder => builder.HasDefaultSchema("myDefaultSchema"), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customer", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customer"); + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart"); + e.Property("PeriodEnd"); + e.HasKey("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("PeriodEnd", c.Name), + c => Assert.Equal("PeriodStart", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [myDefaultSchema].[Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [myDefaultSchema].[Customer] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DROP TABLE [myDefaultSchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_temporal_table_using_custom_default_schema_and_explicit_history_schema_to_normal_table() + { + await Test( + builder => builder.HasDefaultSchema("myDefaultSchema"), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customer", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "mySchema"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customer"); + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart"); + e.Property("PeriodEnd"); + e.HasKey("Id"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("PeriodEnd", c.Name), + c => Assert.Equal("PeriodStart", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [myDefaultSchema].[Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [myDefaultSchema].[Customer] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DROP TABLE [mySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_normal_table_to_temporal_table_with_minimal_configuration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable(tb => tb.IsTemporal()); + + e.Metadata[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] = "PeriodStart"; + e.Metadata[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] = "PeriodEnd"; + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.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 [Customer] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customer] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [PeriodStart] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [PeriodEnd] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Convert_normal_table_to_temporal_generates_exec_when_idempotent() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable(tb => tb.IsTemporal()); + + e.Metadata[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] = "PeriodStart"; + e.Metadata[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] = "PeriodEnd"; + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.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)); + }, + migrationsSqlGenerationOptions: MigrationsSqlGenerationOptions.Idempotent); + + AssertSql( + """ +ALTER TABLE [Customer] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customer] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +EXEC(N'ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd])') +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [PeriodStart] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [PeriodEnd] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task + Convert_normal_table_with_period_columns_to_temporal_table_default_column_mappings_and_default_history_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("Start"); + e.Property("End"); + e.HasKey("Id"); + }), + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.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 [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task + Convert_normal_table_with_period_columns_to_temporal_table_default_column_mappings_and_specified_history_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("Start"); + e.Property("End"); + e.HasKey("Id"); + }), + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Convert_normal_table_to_temporal_table_default_column_mappings_and_default_history_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + }), + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.NotNull(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task + Convert_normal_table_without_period_columns_to_temporal_table_default_column_mappings_and_specified_history_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + }), + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Rename_period_properties_of_temporal_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"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("ModifiedStart").ValueGeneratedOnAddOrUpdate(); + e.Property("ModifiedEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("ModifiedStart"); + ttb.HasPeriodEnd("ModifiedEnd"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("ModifiedEnd", 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( + """ +EXEC sp_rename N'[Customer].[Start]', N'ModifiedStart', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Customer].[End]', N'ModifiedEnd', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Rename_period_columns_of_temporal_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( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); + ttb.HasPeriodEnd("End").HasColumnName("ModifiedEnd"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("ModifiedEnd", 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( + """ +EXEC sp_rename N'[Customer].[Start]', N'ModifiedStart', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Customer].[End]', N'ModifiedEnd', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Alter_period_column_of_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => { }, + builder => builder.Entity("Customer").Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @defaultSchema1 AS sysname; +SET @defaultSchema1 = SCHEMA_NAME(); +DECLARE @description1 AS sql_variant; +SET @description1 = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customers', 'COLUMN', N'End'; +"""); + } + + [ConditionalFact] + public virtual async Task Rename_regular_columns_of_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +EXEC sp_rename N'[Customer].[Name]', N'FullName', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_regular_column_of_temporal_table_from_nullable_to_non_nullable() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + + // adding data to make sure default for null value can be applied correctly + e.HasData( + new { Id = 1, IsVip = (bool?)true }, + new { Id = 2, IsVip = (bool?)false }, + new { Id = 3, IsVip = (bool?)null }); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("IsVip"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("IsVip"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("IsVip", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +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'[Customer]') AND [c].[name] = N'IsVip'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); +UPDATE [Customer] SET [IsVip] = CAST(0 AS bit) WHERE [IsVip] IS NULL; +ALTER TABLE [Customer] ALTER COLUMN [IsVip] bit NOT NULL; +ALTER TABLE [Customer] ADD DEFAULT CAST(0 AS bit) FOR [IsVip]; +""", + // + """ +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'IsVip'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +UPDATE [HistoryTable] SET [IsVip] = CAST(0 AS bit) WHERE [IsVip] IS NULL; +ALTER TABLE [HistoryTable] ALTER COLUMN [IsVip] bit NOT NULL; +ALTER TABLE [HistoryTable] ADD DEFAULT CAST(0 AS bit) FOR [IsVip]; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_computed_column() + { + await Test( + builder => { }, + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.Property("Number"); + e.Property("NumberPlusFive").HasComputedColumnSql("Number + 5 PERSISTED"); + e.HasKey("Id"); + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Number", c.Name), + c => Assert.Equal("NumberPlusFive", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customer] ( + [Id] int NOT NULL IDENTITY, + [End] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [Number] int NOT NULL, + [NumberPlusFive] AS Number + 5 PERSISTED, + [Start] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([Start], [End]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[HistoryTable]))'); +"""); + } + + [ConditionalFact] + public virtual async Task Add_nullable_computed_column_to_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("IdPlusFive", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] ADD [IdPlusFive] AS Id + 5 PERSISTED; +""", + // + """ +ALTER TABLE [HistoryTable] ADD [IdPlusFive] int NULL; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Add_non_nullable_computed_column_to_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Five").HasComputedColumnSql("5 PERSISTED"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Five", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customer] ADD [Five] AS 5 PERSISTED; +""", + // + """ +ALTER TABLE [HistoryTable] ADD [Five] int NOT NULL DEFAULT 0; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Remove_computed_column_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( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); + }), + builder => { }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +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'[Customer]') AND [c].[name] = N'IdPlusFive'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customer] DROP COLUMN [IdPlusFive]; +""", + // + """ +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'IdPlusFive'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [IdPlusFive]; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Alter_computed_column_sql_on_temporal_table() + { + var message = (await Assert.ThrowsAsync( + () => Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("IdPlusFive").HasComputedColumnSql("Id + 10 PERSISTED"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("IdPlusFive", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }))).Message; + + Assert.Equal( + SqlServerStrings.TemporalMigrationModifyingComputedColumnNotSupported("IdPlusFive", "Customer"), + message); + } + + [ConditionalFact] + public virtual async Task Add_column_on_temporal_table_with_computed_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Number"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("IdPlusFive", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] ADD [Number] int NOT NULL DEFAULT 0; +"""); + } + + [ConditionalFact] + public virtual async Task Remove_column_on_temporal_table_with_computed_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Number"); + }), + builder => builder.Entity( + "Customer", e => + { + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("IdPlusFive", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +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'[Customer]') AND [c].[name] = N'Number'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customer] 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 @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Rename_column_on_temporal_table_with_computed_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Number"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("RenamedNumber"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("IdPlusFive", c.Name), + c => Assert.Equal("RenamedNumber", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +EXEC sp_rename N'[Customer].[Number]', N'RenamedNumber', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Add_sparse_column_to_temporal_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"); + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("MyColumn").IsSparse(); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("MyColumn", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +IF EXISTS (SELECT 1 FROM [sys].[tables] [t] INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] WHERE [t].[name] = 'HistoryTable' AND data_compression <> 0) +EXEC(N'ALTER TABLE [HistoryTable] REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE);'); +""", + // + """ +ALTER TABLE [Customer] ADD [MyColumn] int SPARSE NULL; +""", + // + """ +ALTER TABLE [HistoryTable] ADD [MyColumn] int SPARSE NULL; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Add_sparse_column_to_temporal_table_with_custom_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"); + 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.Property("MyColumn").IsSparse(); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal("mySchema", table.Schema); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("myHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("MyColumn", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +IF EXISTS (SELECT 1 FROM [sys].[tables] [t] INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] WHERE [t].[name] = 'HistoryTable' AND [t].[schema_id] = schema_id('myHistorySchema') AND data_compression <> 0) +EXEC(N'ALTER TABLE [myHistorySchema].[HistoryTable] REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE);'); +""", + // + """ +ALTER TABLE [mySchema].[Customers] ADD [MyColumn] int SPARSE NULL; +""", + // + """ +ALTER TABLE [myHistorySchema].[HistoryTable] ADD [MyColumn] int SPARSE NULL; +""", + // + """ +ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable])) +"""); + } + + [ConditionalFact] + public virtual async Task Convert_regular_column_of_temporal_table_to_sparse() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + e.HasData( + new { MyColumn = 1 }, + new { MyColumn = 2 }, + new { MyColumn = (int?)null }, + new { MyColumn = (int?)null }); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("MyColumn"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("MyColumn").IsSparse(); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("MyColumn", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +IF EXISTS (SELECT 1 FROM [sys].[tables] [t] INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] WHERE [t].[name] = 'HistoryTable' AND data_compression <> 0) +EXEC(N'ALTER TABLE [HistoryTable] REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE);'); +""", + // + """ +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'[Customer]') AND [c].[name] = N'MyColumn'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customer] ALTER COLUMN [MyColumn] int SPARSE NULL; +""", + // + """ +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'MyColumn'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [HistoryTable] ALTER COLUMN [MyColumn] int SPARSE NULL; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Convert_sparse_column_of_temporal_table_to_regular() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + e.HasData( + new { MyColumn = 1 }, + new { MyColumn = 2 }, + new { MyColumn = (int?)null }, + new { MyColumn = (int?)null }); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("MyColumn").IsSparse(); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("MyColumn"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("MyColumn", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'MyColumn'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customer] ALTER COLUMN [MyColumn] int NULL; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_regular_table_with_sparse_column_to_temporal() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("MyColumn").IsSparse(); + e.HasData( + new { MyColumn = 1 }, + new { MyColumn = 2 }, + new { MyColumn = (int?)null }, + new { MyColumn = (int?)null }); + }), + builder => builder.Entity( + "Customer", e => + { + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.ToTable( + "Customers", + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("MyColumn", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Create_temporal_table_with_comments() + { + await Test( + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasComment("Column comment"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + }) + .HasComment("Table comment")); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.NotNull(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customer] ( + [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_Customer] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))'); +DECLARE @defaultSchema1 AS sysname; +SET @defaultSchema1 = SCHEMA_NAME(); +DECLARE @description1 AS sql_variant; +SET @description1 = N'Table comment'; +EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customer'; +SET @description1 = N'Column comment'; +EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customer', 'COLUMN', N'Name'; +"""); + } + + [ConditionalFact] + public virtual async Task Convert_normal_table_to_temporal_while_also_adding_comments_and_index() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name").HasComment("Column comment"); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.HasIndex("Name"); + + e.ToTable( + tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(450) NULL; +DECLARE @defaultSchema2 AS sysname; +SET @defaultSchema2 = SCHEMA_NAME(); +DECLARE @description2 AS sql_variant; +SET @description2 = N'Column comment'; +EXEC sp_addextendedproperty 'MS_Description', @description2, 'SCHEMA', @defaultSchema2, 'TABLE', N'Customer', 'COLUMN', N'Name'; +""", + // + """ +ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +CREATE INDEX [IX_Customer_Name] ON [Customer] ([Name]); +""", + // + """ +ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public async Task Alter_comments_for_temporal_table() + { + await Test( + 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( + tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasComment("Column comment"); + e.ToTable(tb => tb.HasComment("Table comment")); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasComment("Modified column comment"); + e.ToTable(tb => tb.HasComment("Modified table comment")); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customer", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.NotNull(table[SqlServerAnnotationNames.TemporalHistoryTableName]); + Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @defaultSchema2 AS sysname; +SET @defaultSchema2 = SCHEMA_NAME(); +DECLARE @description2 AS sql_variant; +EXEC sp_dropextendedproperty 'MS_Description', 'SCHEMA', @defaultSchema2, 'TABLE', N'Customer'; +SET @description2 = N'Modified table comment'; +EXEC sp_addextendedproperty 'MS_Description', @description2, 'SCHEMA', @defaultSchema2, 'TABLE', N'Customer'; +""", + // + """ +DECLARE @defaultSchema3 AS sysname; +SET @defaultSchema3 = SCHEMA_NAME(); +DECLARE @description3 AS sql_variant; +EXEC sp_dropextendedproperty 'MS_Description', 'SCHEMA', @defaultSchema3, 'TABLE', N'Customer', 'COLUMN', N'Name'; +SET @description3 = N'Modified column comment'; +EXEC sp_addextendedproperty 'MS_Description', @description3, 'SCHEMA', @defaultSchema3, 'TABLE', N'Customer', 'COLUMN', N'Name'; +"""); + } + + [ConditionalFact] + public virtual async Task Add_index_to_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("Number"); + 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 => builder.Entity( + "Customer", e => + { + e.HasIndex("Name"); + e.HasIndex("Number").IsUnique(); + }), + 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(2, table.Indexes.Count); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] ALTER COLUMN [Name] nvarchar(450) NULL; +""", + // + """ +CREATE INDEX [IX_Customers_Name] ON [Customers] ([Name]); +""", + // + """ +CREATE UNIQUE INDEX [IX_Customers_Number] ON [Customers] ([Number]); +"""); + } + + [ConditionalFact] + public virtual async Task Add_index_on_period_column_to_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("Number"); + 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 => builder.Entity( + "Customer", e => + { + e.HasIndex("Start"); + e.HasIndex("End", "Name"); + }), + 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]); + + // TODO: issue #26008 - we don't reverse engineer indexes on period columns since the columns are not added to the database model + //Assert.Equal(2, table.Indexes.Count); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] ALTER COLUMN [Name] nvarchar(450) NULL; +""", + // + """ +CREATE INDEX [IX_Customers_End_Name] ON [Customers] ([End], [Name]); +""", + // + """ +CREATE INDEX [IX_Customers_Start] ON [Customers] ([Start]); +"""); + } + + [ConditionalFact] + public virtual async Task History_table_schema_created_when_necessary() + { + 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", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + ttb.UseHistoryTable("MyHistoryTable", "mySchema2"); + })); + }); + }, + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("mySchema", table.Schema); + Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + }); + + AssertSql( + """ +IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); +""", + // + """ +IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];'); +""", + // + """ +CREATE TABLE [mySchema].[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 = [mySchema2].[MyHistoryTable])); +"""); + } + + [ConditionalFact] + public virtual async Task History_table_schema_not_created_if_we_know_it_already_exists1() + { + 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", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + + builder.Entity( + "Order", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Orders", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }); + }, + model => + { + Assert.Equal(2, model.Tables.Count); + Assert.True(model.Tables.All(x => x.Schema == "mySchema")); + Assert.True(model.Tables.All(x => x[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string == "mySchema")); + }); + + AssertSql( + """ +IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); +""", + // + """ +CREATE TABLE [mySchema].[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 = [mySchema].[CustomersHistory])); +""", + // + """ +CREATE TABLE [mySchema].[Orders] ( + [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_Orders] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[OrdersHistory])); +"""); + } + + [ConditionalFact] + public virtual async Task History_table_schema_not_created_if_we_know_it_already_exists2() + { + 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", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + ttb.UseHistoryTable("CustomersHistoryTable", "mySchema2"); + })); + }); + + builder.Entity( + "Order", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Orders", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + ttb.UseHistoryTable("OrdersHistoryTable", "mySchema2"); + })); + }); + }, + model => + { + Assert.Equal(2, model.Tables.Count); + Assert.True(model.Tables.All(x => x.Schema == "mySchema")); + Assert.True(model.Tables.All(x => x[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string == "mySchema2")); + }); + + AssertSql( + """ +IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); +""", + // + """ +IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];'); +""", + // + """ +CREATE TABLE [mySchema].[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 = [mySchema2].[CustomersHistoryTable])); +""", + // + """ +CREATE TABLE [mySchema].[Orders] ( + [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_Orders] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[OrdersHistoryTable])); +"""); + } + + [ConditionalFact] + public virtual async Task History_table_schema_renamed_to_one_exisiting_in_the_model() + { + await Test( + 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", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + ttb.UseHistoryTable("CustomersHistoryTable", "mySchema2"); + })); + }); + + builder.Entity( + "Order", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Orders", "mySchema2", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + ttb.UseHistoryTable("OrdersHistoryTable", "mySchema2"); + })); + }); + }, + 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", "mySchema", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + ttb.UseHistoryTable("CustomersHistoryTable", "mySchema2"); + })); + }); + + builder.Entity( + "Order", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Orders", "mySchema2", tb => tb.IsTemporal( + ttb => + { + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + ttb.UseHistoryTable("OrdersHistoryTable", "mySchema"); + })); + }); + }, + model => + { + Assert.Equal(2, model.Tables.Count); + var customers = model.Tables.First(t => t.Name == "Customers"); + Assert.Equal("mySchema", customers.Schema); + Assert.Equal("mySchema2", customers[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + + var orders = model.Tables.First(t => t.Name == "Orders"); + Assert.Equal("mySchema2", orders.Schema); + Assert.Equal("mySchema", orders[SqlServerAnnotationNames.TemporalHistoryTableSchema]); + }); + + // TODO: we could avoid creating the schema if we peek into the model + AssertSql( + """ +IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); +""", + // + """ +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])) +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_delete_columns_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + e.Property("Dob"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + e.Property("DateOfBirth"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("DateOfBirth", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @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]; +""", + // + """ +EXEC sp_rename N'[Customers].[Name]', N'FullName', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Customers].[Dob]', N'DateOfBirth', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[HistoryTable].[Dob]', N'DateOfBirth', 'COLUMN'; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_delete_columns_and_also_rename_table_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "ModifiedCustomers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("ModifiedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +DECLARE @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]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'ModifiedCustomers', 'OBJECT'; +""", + // + """ +EXEC sp_rename N'[ModifiedCustomers].[Name]', N'FullName', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', 'COLUMN'; +""", + // + """ +ALTER TABLE [ModifiedCustomers] ADD CONSTRAINT [PK_ModifiedCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [ModifiedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_delete_columns_and_also_rename_history_table_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("FullName"); + + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("ModifiedHistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("ModifiedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @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]; +""", + // + """ +EXEC sp_rename N'[Customers].[Name]', N'FullName', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[HistoryTable]', N'ModifiedHistoryTable', 'OBJECT'; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[ModifiedHistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_delete_column_and_add_another_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("DateOfBirth"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("DateOfBirth", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @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]; +""", + // + """ +ALTER TABLE [Customers] ADD [DateOfBirth] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [HistoryTable] ADD [DateOfBirth] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_delete_column_and_alter_another_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name"); + e.Property("Number"); + e.Property("DateOfBirth"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Name").HasComment("My comment"); + e.Property("DateOfBirth"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("DateOfBirth", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @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 @defaultSchema4 AS sysname; +SET @defaultSchema4 = SCHEMA_NAME(); +DECLARE @description4 AS sql_variant; +SET @description4 = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description4, 'SCHEMA', @defaultSchema4, 'TABLE', N'Customers', 'COLUMN', N'Name'; +""", + // + """ +DECLARE @defaultSchema5 AS sysname; +SET @defaultSchema5 = SCHEMA_NAME(); +DECLARE @description5 AS sql_variant; +SET @description5 = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description5, 'SCHEMA', @defaultSchema5, 'TABLE', N'HistoryTable', 'COLUMN', N'Name'; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_rename_and_alter_period_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +EXEC sp_rename N'[Customers].[Start]', N'ModifiedStart', 'COLUMN'; +""", + // + """ +DECLARE @defaultSchema1 AS sysname; +SET @defaultSchema1 = SCHEMA_NAME(); +DECLARE @description1 AS sql_variant; +SET @description1 = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customers', 'COLUMN', N'End'; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_table_delete_column_rename_and_alter_period_column_in_one_migration() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("DateOfBirth"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DECLARE @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'DateOfBirth'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [DateOfBirth]; +""", + // + """ +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'DateOfBirth'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [HistoryTable] DROP COLUMN [DateOfBirth]; +""", + // + """ +EXEC sp_rename N'[Customers].[Start]', N'ModifiedStart', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[HistoryTable].[Start]', N'ModifiedStart', 'COLUMN'; +""", + // + """ +DECLARE @defaultSchema4 AS sysname; +SET @defaultSchema4 = SCHEMA_NAME(); +DECLARE @description4 AS sql_variant; +SET @description4 = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description4, 'SCHEMA', @defaultSchema4, 'TABLE', N'Customers', 'COLUMN', N'End'; +""", + // + """ +DECLARE @defaultSchema5 AS sysname; +SET @defaultSchema5 = SCHEMA_NAME(); +DECLARE @description5 AS sql_variant; +SET @description5 = N'My comment'; +EXEC sp_addextendedproperty 'MS_Description', @description5, 'SCHEMA', @defaultSchema5, 'TABLE', N'HistoryTable', 'COLUMN', N'End'; +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Convert_from_temporal_table_with_minimal_configuration_to_explicit_one_noop() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable("Customers", tb => tb.IsTemporal()); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql(); + } + + [ConditionalFact] + public virtual async Task Convert_from_temporal_table_with_explicit_configuration_to_minimal_one_noop() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable("Customers", tb => tb.IsTemporal()); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql(); + } + + [ConditionalFact] + public virtual async Task Convert_from_temporal_table_with_minimal_configuration_to_explicit_one() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable("Customers", tb => tb.IsTemporal()); + }), + 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"); + 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.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +EXEC sp_rename N'[Customers].[PeriodStart]', N'Start', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Customers].[PeriodEnd]', N'End', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[CustomersHistory]', N'HistoryTable', 'OBJECT'; +"""); + } + + [ConditionalFact] + public virtual async Task Change_names_of_period_columns_in_temporal_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("ValidFrom").ValueGeneratedOnAddOrUpdate(); + e.Property("ValidTo").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("ValidFrom"); + ttb.HasPeriodEnd("ValidTo"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("ValidFrom", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("ValidTo", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +EXEC sp_rename N'[Customers].[PeriodStart]', N'ValidFrom', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Customers].[PeriodEnd]', N'ValidTo', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_to_temporal_and_add_new_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + 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.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [Number] int NOT NULL DEFAULT 0; +""", + // + """ +ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_to_temporal_and_remove_existing_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + 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.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Number'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number]; +""", + // + """ +ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_to_temporal_and_rename_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("NewNumber"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + 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.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("NewNumber", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +EXEC sp_rename N'[Customers].[Number]', N'NewNumber', 'COLUMN'; +""", + // + """ +ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_from_temporal_and_add_new_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [End]; +""", + // + """ +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'Start'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Start]; +""", + // + """ +DROP TABLE [HistoryTable]; +""", + // + """ +ALTER TABLE [Customers] ADD [Number] int NOT NULL DEFAULT 0; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_from_temporal_and_remove_existing_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.Property("Name"); + e.Property("Number"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable("Customers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [End]; +""", + // + """ +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 @var4 sysname; +SELECT @var4 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Start'); +IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var4 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Start]; +""", + // + """ +DROP TABLE [HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_from_temporal_and_rename_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("NewNumber"); + e.ToTable("Customers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("NewNumber", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [End]; +""", + // + """ +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'Start'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Start]; +""", + // + """ +EXEC sp_rename N'[Customers].[Number]', N'NewNumber', 'COLUMN'; +""", + // + """ +DROP TABLE [HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_to_temporal_rename_table_and_add_new_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable( + "NewCustomers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("NewCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [Number] int NOT NULL DEFAULT 0; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +ALTER TABLE [NewCustomers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [NewCustomers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [NewCustomers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_to_temporal_rename_table_and_remove_existing_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "NewCustomers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("NewCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Number'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Number]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +ALTER TABLE [NewCustomers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [NewCustomers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [NewCustomers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_to_temporal_rename_table_and_rename_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("NewNumber"); + e.ToTable( + "NewCustomers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("NewCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("NewNumber", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +EXEC sp_rename N'[NewCustomers].[Number]', N'NewNumber', 'COLUMN'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +ALTER TABLE [NewCustomers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [NewCustomers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [NewCustomers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_from_temporal_rename_table_and_add_new_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("NewCustomers"); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("NewCustomers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [End]; +""", + // + """ +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'Start'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Start]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +DROP TABLE [HistoryTable]; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [Number] int NOT NULL DEFAULT 0; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_rename_table_rename_history_table_and_add_new_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable( + "NewCustomers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("NewHistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables); + Assert.Equal("NewCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("NewHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +EXEC sp_rename N'[HistoryTable]', N'NewHistoryTable', 'OBJECT'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD [Number] int NOT NULL DEFAULT 0; +""", + // + """ +ALTER TABLE [NewHistoryTable] ADD [Number] int NOT NULL DEFAULT 0; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[NewHistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_convert_from_temporal_create_another_table_with_same_name_as_history_table() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + builder => + { + builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }); + + builder.Entity( + "History", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("HistoryTable"); + }); + }, + model => + { + var customersTable = Assert.Single(model.Tables, t => t.Name == "Customers"); + var historyTable = Assert.Single(model.Tables, t => t.Name == "HistoryTable"); + + Assert.Collection( + customersTable.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + customersTable.Columns.Single(c => c.Name == "Id"), + Assert.Single(customersTable.PrimaryKey!.Columns)); + + Assert.Collection( + historyTable.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + historyTable.Columns.Single(c => c.Name == "Id"), + Assert.Single(historyTable.PrimaryKey!.Columns)); + }); + + AssertSql( + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [End]; +""", + // + """ +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'Start'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [Start]; +""", + // + """ +DROP TABLE [HistoryTable]; +""", + // + """ +CREATE TABLE [HistoryTable] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + [Number] int NOT NULL, + CONSTRAINT [PK_HistoryTable] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_convert_regular_table_to_temporal_and_add_rowversion_column() + { + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Start").ValueGeneratedOnAddOrUpdate(); + e.Property("End").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.Property("MyRowVersion").IsRowVersion(); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable"); + ttb.HasPeriodStart("Start"); + ttb.HasPeriodEnd("End"); + })); + }), + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name), + c => Assert.Equal("MyRowVersion", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [MyRowVersion] rowversion NULL; +""", + // + """ +ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_create_temporal_table_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), + PeriodEnd = table.Column(type: "datetime2", nullable: false) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), + PeriodStart = table.Column(type: "datetime2", nullable: false) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => { }, + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, + [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), + PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd]) +) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomersHistory]))'); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_convert_regular_table_to_temporal_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.AlterTable( + name: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1"); + + migrationBuilder.AddColumn( + name: "PeriodEnd", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AddColumn( + name: "PeriodStart", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("Number"); + e.ToTable("Customers"); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("Number", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodStart] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodEnd] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomersHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_convert_regular_table_with_rowversion_to_temporal_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.AlterTable( + name: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "MyRowVersion", + table: "Customers", + type: "rowversion", + rowVersion: true, + nullable: false, + oldClrType: typeof(byte[]), + oldType: "rowversion", + oldRowVersion: true) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1"); + + migrationBuilder.AddColumn( + name: "PeriodEnd", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AddColumn( + name: "PeriodStart", + table: "Customers", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("MyRowVersion").IsRowVersion(); + e.ToTable("Customers"); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("MyRowVersion", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; +""", + // + """ +ALTER TABLE [Customers] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; +""", + // + """ +ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodStart] ADD HIDDEN +""", + // + """ +ALTER TABLE [Customers] ALTER COLUMN [PeriodEnd] ADD HIDDEN +""", + // + """ +DECLARE @historyTableSchema sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomersHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_rename_temporal_table_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Customers", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.RenameTable( + name: "Customers", + newName: "RenamedCustomers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null); + + migrationBuilder.AlterTable( + name: "RenamedCustomers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "PeriodStart", + table: "RenamedCustomers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "PeriodEnd", + table: "RenamedCustomers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "RenamedCustomers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "RenamedCustomers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AddPrimaryKey( + name: "PK_RenamedCustomers", + table: "RenamedCustomers", + column: "Id"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "RenamedCustomers"); + Assert.Equal("RenamedCustomers", table.Name); + Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); + Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); + Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); + Assert.Equal("RenamedCustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; +""", + // + """ +EXEC sp_rename N'[CustomersHistory]', N'RenamedCustomersHistory', 'OBJECT'; +""", + // + """ +ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() +EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[RenamedCustomersHistory]))') +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_convert_temporal_table_to_regular_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.DropColumn( + name: "PeriodEnd", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.DropColumn( + name: "PeriodStart", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterTable( + name: "Customers") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME +""", + // + """ +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'PeriodEnd'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [PeriodEnd]; +""", + // + """ +DECLARE @var2 sysname; +SELECT @var2 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'PeriodStart'); +IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); +ALTER TABLE [Customers] DROP COLUMN [PeriodStart]; +""", + // + """ +DROP TABLE [CustomersHistory]; +""", + // + """ +DECLARE @var3 sysname; +SELECT @var3 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); +IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var3 + '];'); +ALTER TABLE [Customers] ALTER COLUMN [Name] nvarchar(max) NOT NULL; +""", + // + """ +DECLARE @var4 sysname; +SELECT @var4 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Id'); +IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var4 + '];'); +ALTER TABLE [Customers] ALTER COLUMN [Id] int NOT NULL; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_add_column_to_temporal_table_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.AddColumn( + name: "MyRowVersion", + table: "Customers", + type: "rowversion", + rowVersion: true, + nullable: false, + defaultValue: new byte[0]) + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name), + c => Assert.Equal("MyRowVersion", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +ALTER TABLE [Customers] ADD [MyRowVersion] rowversion NOT NULL; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_remove_temporal_table_column_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.DropColumn( + name: "IsVip", + table: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.Property("IsVip"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +DECLARE @var1 sysname; +SELECT @var1 = [d].[name] +FROM [sys].[default_constraints] [d] +INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] +WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'IsVip'); +IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); +ALTER TABLE [Customers] DROP COLUMN [IsVip]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_rename_temporal_table_column_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.RenameColumn( + name: "Name", + table: "Customers", + newName: "FullName") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("FullName", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +EXEC sp_rename N'[Customers].[Name]', N'FullName', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_rename_temporal_table_period_columns_using_EF8_migration_code() + { + var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); + + migrationBuilder.RenameColumn( + name: "PeriodStart", + table: "Customers", + newName: "NewPeriodStart") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.RenameColumn( + name: "PeriodEnd", + table: "Customers", + newName: "NewPeriodEnd") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterTable( + name: "Customers") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "Customers", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "Id", + table: "Customers", + type: "int", + nullable: false, + oldClrType: typeof(int), + oldType: "int") + .Annotation("SqlServer:Identity", "1, 1") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:Identity", "1, 1") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "NewPeriodStart", + table: "Customers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + migrationBuilder.AlterColumn( + name: "NewPeriodEnd", + table: "Customers", + type: "datetime2", + nullable: false, + oldClrType: typeof(DateTime), + oldType: "datetime2") + .Annotation("SqlServer:IsTemporal", true) + .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .Annotation("SqlServer:TemporalHistoryTableSchema", null) + .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") + .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") + .OldAnnotation("SqlServer:IsTemporal", true) + .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") + .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) + .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") + .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); + + await Test( + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); + e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + e.Property("Name"); + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("CustomersHistory"); + ttb.HasPeriodStart("PeriodStart"); + ttb.HasPeriodEnd("PeriodEnd"); + })); + }), + migrationBuilder.Operations, + model => + { + var table = Assert.Single(model.Tables, t => t.Name == "Customers"); + Assert.Equal("Customers", table.Name); + + Assert.Collection( + table.Columns, + c => Assert.Equal("Id", c.Name), + c => Assert.Equal("Name", c.Name)); + Assert.Same( + table.Columns.Single(c => c.Name == "Id"), + Assert.Single(table.PrimaryKey!.Columns)); + }); + + AssertSql( +""" +EXEC sp_rename N'[Customers].[PeriodStart]', N'NewPeriodStart', 'COLUMN'; +""", + // + """ +EXEC sp_rename N'[Customers].[PeriodEnd]', N'NewPeriodEnd', 'COLUMN'; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_drop_temporal_table_and_add_the_same_table_in_one_migration() + { + await TestComposite( + [ + 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("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + 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("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }) + ]); + + AssertSql( +""" +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +""", + // + """ +IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); +""", + // + """ +CREATE TABLE [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 = [historySchema].[HistoryTable])); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_create_temporal_and_drop() + { + await TestComposite( + [ + 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("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + ]); + + AssertSql( +""" +IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); +""", + // + """ +CREATE TABLE [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 = [historySchema].[HistoryTable])); +""", + // + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_rename_temporal_and_drop() + { + await TestComposite( + [ + 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("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + 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( + "NewCustomers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + ]); + + AssertSql( +""" +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; +""", + // + """ +EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; +""", + // + """ +ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); +""", + // + """ +DROP TABLE [NewCustomers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_rename_period_drop_table_create_as_regular() + { + await TestComposite( + [ + 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("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.Property("NewSystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("NewSystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }), + ]); + + AssertSql( +""" +EXEC sp_rename N'[Customers].[SystemTimeStart]', N'NewSystemTimeStart', 'COLUMN'; +""", + // + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } + + [ConditionalFact] + public virtual async Task Temporal_multiop_rename_column_drop_table_create_as_regular() + { + await TestComposite( + [ + 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("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("NewName"); + e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); + e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); + e.HasKey("Id"); + + e.ToTable( + "Customers", tb => tb.IsTemporal( + ttb => + { + ttb.UseHistoryTable("HistoryTable", "historySchema"); + ttb.HasPeriodStart("SystemTimeStart"); + ttb.HasPeriodEnd("SystemTimeEnd"); + })); + }), + builder => { }, + builder => builder.Entity( + "Customer", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.Property("Name"); + e.HasKey("Id"); + + e.ToTable("Customers"); + }), + ]); + + AssertSql( +""" +EXEC sp_rename N'[Customers].[Name]', N'NewName', 'COLUMN'; +""", + // + """ +ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) +""", + // + """ +DROP TABLE [Customers]; +""", + // + """ +DROP TABLE [historySchema].[HistoryTable]; +""", + // + """ +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); +"""); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index 4d1587e2cc7..16a936a1fc6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -13,7 +13,7 @@ namespace Microsoft.EntityFrameworkCore.Migrations; -public class MigrationsSqlServerTest : MigrationsTestBase +public partial class MigrationsSqlServerTest : MigrationsTestBase { public MigrationsSqlServerTest(MigrationsSqlServerFixture fixture, ITestOutputHelper testOutputHelper) : base(fixture) @@ -3524,8752 +3524,6 @@ CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) """); } - [ConditionalFact] - public virtual async Task Create_temporal_table_default_column_mappings_and_default_history_table() - { - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'CREATE TABLE [Customer] ( - [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_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))'); -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_custom_column_mappings_and_default_history_table() - { - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart").HasColumnName("Start"); - ttb.HasPeriodEnd("SystemTimeEnd").HasColumnName("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'CREATE TABLE [Customer] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NULL, - [End] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, - [Start] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, - CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([Start], [End]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))'); -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_default_column_mappings_and_custom_history_table() - { - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'CREATE TABLE [Customer] ( - [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_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[HistoryTable]))'); -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_with_explicitly_defined_schema() - { - 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", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal("mySchema", table.Schema); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); -""", - // - """ -CREATE TABLE [mySchema].[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 = [mySchema].[CustomersHistory])); -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_no_explicit_table_schema_provided() - { - 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("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_table_schema_provided() - { - 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", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }); - }, - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal("mySchema", table.Schema); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); -""", - // - """ -CREATE TABLE [mySchema].[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 = [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() - { - 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.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }); - }, - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", 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].[HistoryTable])); -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_with_default_schema_for_model_changed_and_explicit_history_table_schema_provided() - { - 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.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }); - }, - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("historySchema", 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'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); -""", - // - """ -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 = [historySchema].[HistoryTable])); -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_with_default_schema_for_table_and_explicit_history_table_schema_provided() - { - 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("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }); - }, - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("historySchema", 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'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); -""", - // - """ -CREATE TABLE [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 = [historySchema].[HistoryTable])); -"""); - } - - [ConditionalFact] - public virtual async Task Drop_temporal_table_default_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"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("Start").HasColumnName("PeriodStart"); - ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd"); - })); - }), - builder => { }, - model => - { - Assert.Empty(model.Tables); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DROP TABLE [Customer]; -""", - // - """ -DROP TABLE [CustomerHistory]; -"""); - } - - [ConditionalFact] - public virtual async Task Drop_temporal_table_custom_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"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start").HasColumnName("PeriodStart"); - ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd"); - })); - }), - builder => { }, - model => - { - Assert.Empty(model.Tables); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DROP TABLE [Customer]; -""", - // - """ -DROP TABLE [HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Drop_temporal_table_custom_history_table_and_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( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("Start").HasColumnName("PeriodStart"); - ttb.HasPeriodEnd("End").HasColumnName("PeriodEnd"); - })); - }), - builder => { }, - model => - { - Assert.Empty(model.Tables); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DROP TABLE [Customer]; -""", - // - """ -DROP TABLE [historySchema].[HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Rename_temporal_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"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("RenamedCustomers"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("RenamedCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; -""", - // - """ -ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Rename_temporal_table_rename_and_modify_column_in_same_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Discount"); - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("DoB"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Discount").HasComment("for VIP only"); - e.Property("DateOfBirth"); - e.ToTable("RenamedCustomers"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("RenamedCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Discount", c.Name), - c => Assert.Equal("DateOfBirth", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; -""", - // - """ -EXEC sp_rename N'[RenamedCustomers].[DoB]', N'DateOfBirth', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[HistoryTable].[DoB]', N'DateOfBirth', 'COLUMN'; -""", - // - """ -DECLARE @defaultSchema2 AS sysname; -SET @defaultSchema2 = SCHEMA_NAME(); -DECLARE @description2 AS sql_variant; -SET @description2 = N'for VIP only'; -EXEC sp_addextendedproperty 'MS_Description', @description2, 'SCHEMA', @defaultSchema2, 'TABLE', N'RenamedCustomers', 'COLUMN', N'Discount'; -""", - // - """ -DECLARE @defaultSchema3 AS sysname; -SET @defaultSchema3 = SCHEMA_NAME(); -DECLARE @description3 AS sql_variant; -SET @description3 = N'for VIP only'; -EXEC sp_addextendedproperty 'MS_Description', @description3, 'SCHEMA', @defaultSchema3, 'TABLE', N'HistoryTable', 'COLUMN', N'Discount'; -""", - // - """ -ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Rename_temporal_table_with_custom_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( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("RenamedCustomers"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("RenamedCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; -""", - // - """ -ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); -""", - // - """ -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', 'OBJECT'; -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', 'OBJECT'; -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', 'OBJECT'; -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', 'OBJECT'; -"""); - } - - [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', 'OBJECT'; -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', 'OBJECT'; -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 @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'Name'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Name]; -""", - // - """ -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'Name'); -IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); -ALTER TABLE [HistoryTable] DROP COLUMN [Name]; -""", - // - """ -DECLARE @var4 sysname; -SELECT @var4 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Number'); -IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var4 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Number]; -""", - // - """ -DECLARE @var5 sysname; -SELECT @var5 = [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 @var5 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var5 + '];'); -ALTER TABLE [HistoryTable] DROP COLUMN [Number]; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[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); - 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 @var sysname; -SELECT @var = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); -IF @var IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var + '];'); -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 Remove_columns_from_temporal_table_with_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", "mySchema", 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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @var sysname; -SELECT @var = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Name'); -IF @var IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var + '];'); -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 Remove_columns_from_temporal_table_with_default_schema() - { - await Test( - builder => - { - builder.HasDefaultSchema("myDefaultSchema"); - builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - 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.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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @var sysname; -SELECT @var = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Name'); -IF @var IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var + '];'); -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 Remove_columns_from_temporal_table_with_different_schemas_on_each_level() - { - await Test( - builder => - { - builder.HasDefaultSchema("myDefaultSchema"); - builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - 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.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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @var sysname; -SELECT @var = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[mySchema].[Customers]') AND [c].[name] = N'Name'); -IF @var IS NOT NULL EXEC(N'ALTER TABLE [mySchema].[Customers] DROP CONSTRAINT [' + @var + '];'); -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'[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'[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'[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 [mySchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable])) -"""); - } - - [ConditionalFact] - public virtual async Task Add_columns_to_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 => builder.Entity( - "Customer", e => - { - e.Property("Name"); - e.Property("Number"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] ADD [Name] nvarchar(max) NULL; -""", - // - """ -ALTER TABLE [Customers] ADD [Number] int NOT NULL DEFAULT 0; -"""); - } - - [ConditionalFact] - public virtual async Task - Convert_temporal_table_with_default_column_mappings_and_custom_history_table_to_normal_table_keep_period_columns() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart"); - e.Property("PeriodEnd"); - e.HasKey("Id"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("PeriodEnd", c.Name), - c => Assert.Equal("PeriodStart", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DROP TABLE [HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_temporal_table_with_default_column_mappings_and_default_history_table_to_normal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodEnd'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customer] DROP COLUMN [PeriodEnd]; -""", - // - """ -DECLARE @var2 sysname; -SELECT @var2 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodStart'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customer] DROP COLUMN [PeriodStart]; -""", - // - """ -DROP TABLE [CustomerHistory]; -"""); - } - - [ConditionalFact] - public virtual async Task - Convert_temporal_table_with_default_column_mappings_and_custom_history_table_to_normal_table_remove_period_columns() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodEnd'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customer] DROP COLUMN [PeriodEnd]; -""", - // - """ -DECLARE @var2 sysname; -SELECT @var2 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'PeriodStart'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customer] DROP COLUMN [PeriodStart]; -""", - // - """ -DROP TABLE [HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_temporal_table_with_explicit_history_table_schema_to_normal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart"); - e.Property("PeriodEnd"); - e.HasKey("Id"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("PeriodEnd", c.Name), - c => Assert.Equal("PeriodStart", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customer] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DROP TABLE [historySchema].[HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_temporal_table_with_explicit_schemas_same_schema_for_table_and_history_to_normal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customer", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "mySchema"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("Customer", "mySchema"); - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart"); - e.Property("PeriodEnd"); - e.HasKey("Id"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("PeriodEnd", c.Name), - c => Assert.Equal("PeriodStart", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [mySchema].[Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [mySchema].[Customer] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DROP TABLE [mySchema].[HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_temporal_table_using_custom_default_schema_to_normal_table() - { - await Test( - builder => builder.HasDefaultSchema("myDefaultSchema"), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customer", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("Customer"); - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart"); - e.Property("PeriodEnd"); - e.HasKey("Id"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("PeriodEnd", c.Name), - c => Assert.Equal("PeriodStart", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [myDefaultSchema].[Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [myDefaultSchema].[Customer] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DROP TABLE [myDefaultSchema].[HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_temporal_table_using_custom_default_schema_and_explicit_history_schema_to_normal_table() - { - await Test( - builder => builder.HasDefaultSchema("myDefaultSchema"), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customer", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "mySchema"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("Customer"); - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart"); - e.Property("PeriodEnd"); - e.HasKey("Id"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Null(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Null(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("PeriodEnd", c.Name), - c => Assert.Equal("PeriodStart", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [myDefaultSchema].[Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [myDefaultSchema].[Customer] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DROP TABLE [mySchema].[HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_normal_table_to_temporal_table_with_minimal_configuration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable(tb => tb.IsTemporal()); - - e.Metadata[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] = "PeriodStart"; - e.Metadata[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] = "PeriodEnd"; - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.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 [Customer] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customer] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [PeriodStart] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [PeriodEnd] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') -"""); - } - - [ConditionalFact] - public virtual async Task Convert_normal_table_to_temporal_generates_exec_when_idempotent() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable(tb => tb.IsTemporal()); - - e.Metadata[SqlServerAnnotationNames.TemporalPeriodStartPropertyName] = "PeriodStart"; - e.Metadata[SqlServerAnnotationNames.TemporalPeriodEndPropertyName] = "PeriodEnd"; - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.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)); - }, - migrationsSqlGenerationOptions: MigrationsSqlGenerationOptions.Idempotent); - - AssertSql( - """ -ALTER TABLE [Customer] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customer] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -EXEC(N'ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd])') -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [PeriodStart] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [PeriodEnd] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') -"""); - } - - [ConditionalFact] - public virtual async Task - Convert_normal_table_with_period_columns_to_temporal_table_default_column_mappings_and_default_history_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("Start"); - e.Property("End"); - e.HasKey("Id"); - }), - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("CustomerHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.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 [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') -"""); - } - - [ConditionalFact] - public virtual async Task - Convert_normal_table_with_period_columns_to_temporal_table_default_column_mappings_and_specified_history_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("Start"); - e.Property("End"); - e.HasKey("Id"); - }), - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Convert_normal_table_to_temporal_table_default_column_mappings_and_default_history_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - }), - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.NotNull(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomerHistory]))') -"""); - } - - [ConditionalFact] - public virtual async Task - Convert_normal_table_without_period_columns_to_temporal_table_default_column_mappings_and_specified_history_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - }), - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Rename_period_properties_of_temporal_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"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("ModifiedStart").ValueGeneratedOnAddOrUpdate(); - e.Property("ModifiedEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("ModifiedStart"); - ttb.HasPeriodEnd("ModifiedEnd"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("ModifiedEnd", 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( - """ -EXEC sp_rename N'[Customer].[Start]', N'ModifiedStart', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[Customer].[End]', N'ModifiedEnd', 'COLUMN'; -"""); - } - - [ConditionalFact] - public virtual async Task Rename_period_columns_of_temporal_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( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); - ttb.HasPeriodEnd("End").HasColumnName("ModifiedEnd"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("ModifiedEnd", 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( - """ -EXEC sp_rename N'[Customer].[Start]', N'ModifiedStart', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[Customer].[End]', N'ModifiedEnd', 'COLUMN'; -"""); - } - - [ConditionalFact] - public virtual async Task Alter_period_column_of_temporal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => { }, - builder => builder.Entity("Customer").Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @defaultSchema1 AS sysname; -SET @defaultSchema1 = SCHEMA_NAME(); -DECLARE @description1 AS sql_variant; -SET @description1 = N'My comment'; -EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customers', 'COLUMN', N'End'; -"""); - } - - [ConditionalFact] - public virtual async Task Rename_regular_columns_of_temporal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("FullName"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("FullName", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -EXEC sp_rename N'[Customer].[Name]', N'FullName', 'COLUMN'; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_regular_column_of_temporal_table_from_nullable_to_non_nullable() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - - // adding data to make sure default for null value can be applied correctly - e.HasData( - new { Id = 1, IsVip = (bool?)true }, - new { Id = 2, IsVip = (bool?)false }, - new { Id = 3, IsVip = (bool?)null }); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("IsVip"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("IsVip"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("IsVip", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -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'[Customer]') AND [c].[name] = N'IsVip'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); -UPDATE [Customer] SET [IsVip] = CAST(0 AS bit) WHERE [IsVip] IS NULL; -ALTER TABLE [Customer] ALTER COLUMN [IsVip] bit NOT NULL; -ALTER TABLE [Customer] ADD DEFAULT CAST(0 AS bit) FOR [IsVip]; -""", - // - """ -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'IsVip'); -IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); -UPDATE [HistoryTable] SET [IsVip] = CAST(0 AS bit) WHERE [IsVip] IS NULL; -ALTER TABLE [HistoryTable] ALTER COLUMN [IsVip] bit NOT NULL; -ALTER TABLE [HistoryTable] ADD DEFAULT CAST(0 AS bit) FOR [IsVip]; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_with_computed_column() - { - await Test( - builder => { }, - builder => { }, - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.Property("Number"); - e.Property("NumberPlusFive").HasComputedColumnSql("Number + 5 PERSISTED"); - e.HasKey("Id"); - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Number", c.Name), - c => Assert.Equal("NumberPlusFive", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'CREATE TABLE [Customer] ( - [Id] int NOT NULL IDENTITY, - [End] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, - [Number] int NOT NULL, - [NumberPlusFive] AS Number + 5 PERSISTED, - [Start] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, - CONSTRAINT [PK_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([Start], [End]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[HistoryTable]))'); -"""); - } - - [ConditionalFact] - public virtual async Task Add_nullable_computed_column_to_temporal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => { }, - builder => builder.Entity( - "Customer", e => - { - e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("IdPlusFive", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customer] ADD [IdPlusFive] AS Id + 5 PERSISTED; -""", - // - """ -ALTER TABLE [HistoryTable] ADD [IdPlusFive] int NULL; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Add_non_nullable_computed_column_to_temporal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => { }, - builder => builder.Entity( - "Customer", e => - { - e.Property("Five").HasComputedColumnSql("5 PERSISTED"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Five", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customer] ADD [Five] AS 5 PERSISTED; -""", - // - """ -ALTER TABLE [HistoryTable] ADD [Five] int NOT NULL DEFAULT 0; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Remove_computed_column_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( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); - }), - builder => { }, - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -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'[Customer]') AND [c].[name] = N'IdPlusFive'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customer] DROP COLUMN [IdPlusFive]; -""", - // - """ -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'IdPlusFive'); -IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); -ALTER TABLE [HistoryTable] DROP COLUMN [IdPlusFive]; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Alter_computed_column_sql_on_temporal_table() - { - var message = (await Assert.ThrowsAsync( - () => Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("IdPlusFive").HasComputedColumnSql("Id + 10 PERSISTED"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("IdPlusFive", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }))).Message; - - Assert.Equal( - SqlServerStrings.TemporalMigrationModifyingComputedColumnNotSupported("IdPlusFive", "Customer"), - message); - } - - [ConditionalFact] - public virtual async Task Add_column_on_temporal_table_with_computed_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Number"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("IdPlusFive", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] ADD [Number] int NOT NULL DEFAULT 0; -"""); - } - - [ConditionalFact] - public virtual async Task Remove_column_on_temporal_table_with_computed_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Number"); - }), - builder => builder.Entity( - "Customer", e => - { - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("IdPlusFive", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -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'[Customer]') AND [c].[name] = N'Number'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customer] 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 @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Rename_column_on_temporal_table_with_computed_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("IdPlusFive").HasComputedColumnSql("Id + 5 PERSISTED"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Number"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("RenamedNumber"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("IdPlusFive", c.Name), - c => Assert.Equal("RenamedNumber", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -EXEC sp_rename N'[Customer].[Number]', N'RenamedNumber', 'COLUMN'; -"""); - } - - [ConditionalFact] - public virtual async Task Add_sparse_column_to_temporal_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"); - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => { }, - builder => builder.Entity( - "Customer", e => - { - e.Property("MyColumn").IsSparse(); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("MyColumn", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -IF EXISTS (SELECT 1 FROM [sys].[tables] [t] INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] WHERE [t].[name] = 'HistoryTable' AND data_compression <> 0) -EXEC(N'ALTER TABLE [HistoryTable] REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE);'); -""", - // - """ -ALTER TABLE [Customer] ADD [MyColumn] int SPARSE NULL; -""", - // - """ -ALTER TABLE [HistoryTable] ADD [MyColumn] int SPARSE NULL; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Add_sparse_column_to_temporal_table_with_custom_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"); - 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.Property("MyColumn").IsSparse(); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal("mySchema", table.Schema); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("myHistorySchema", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("MyColumn", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -IF EXISTS (SELECT 1 FROM [sys].[tables] [t] INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] WHERE [t].[name] = 'HistoryTable' AND [t].[schema_id] = schema_id('myHistorySchema') AND data_compression <> 0) -EXEC(N'ALTER TABLE [myHistorySchema].[HistoryTable] REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE);'); -""", - // - """ -ALTER TABLE [mySchema].[Customers] ADD [MyColumn] int SPARSE NULL; -""", - // - """ -ALTER TABLE [myHistorySchema].[HistoryTable] ADD [MyColumn] int SPARSE NULL; -""", - // - """ -ALTER TABLE [mySchema].[Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [myHistorySchema].[HistoryTable])) -"""); - } - - [ConditionalFact] - public virtual async Task Convert_regular_column_of_temporal_table_to_sparse() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - e.HasData( - new { MyColumn = 1 }, - new { MyColumn = 2 }, - new { MyColumn = (int?)null }, - new { MyColumn = (int?)null }); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("MyColumn"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("MyColumn").IsSparse(); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("MyColumn", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -IF EXISTS (SELECT 1 FROM [sys].[tables] [t] INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] WHERE [t].[name] = 'HistoryTable' AND data_compression <> 0) -EXEC(N'ALTER TABLE [HistoryTable] REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE);'); -""", - // - """ -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'[Customer]') AND [c].[name] = N'MyColumn'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customer] ALTER COLUMN [MyColumn] int SPARSE NULL; -""", - // - """ -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'MyColumn'); -IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); -ALTER TABLE [HistoryTable] ALTER COLUMN [MyColumn] int SPARSE NULL; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Convert_sparse_column_of_temporal_table_to_regular() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - e.HasData( - new { MyColumn = 1 }, - new { MyColumn = 2 }, - new { MyColumn = (int?)null }, - new { MyColumn = (int?)null }); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("MyColumn").IsSparse(); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("MyColumn"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("MyColumn", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'MyColumn'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customer] ALTER COLUMN [MyColumn] int NULL; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_regular_table_with_sparse_column_to_temporal() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("MyColumn").IsSparse(); - e.HasData( - new { MyColumn = 1 }, - new { MyColumn = 2 }, - new { MyColumn = (int?)null }, - new { MyColumn = (int?)null }); - }), - builder => builder.Entity( - "Customer", e => - { - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.ToTable( - "Customers", - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.NotNull(table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("MyColumn", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Create_temporal_table_with_comments() - { - await Test( - builder => { }, - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name").HasComment("Column comment"); - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - }) - .HasComment("Table comment")); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.NotNull(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'CREATE TABLE [Customer] ( - [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_Customer] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomerHistory]))'); -DECLARE @defaultSchema1 AS sysname; -SET @defaultSchema1 = SCHEMA_NAME(); -DECLARE @description1 AS sql_variant; -SET @description1 = N'Table comment'; -EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customer'; -SET @description1 = N'Column comment'; -EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customer', 'COLUMN', N'Name'; -"""); - } - - [ConditionalFact] - public virtual async Task Convert_normal_table_to_temporal_while_also_adding_comments_and_index() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name").HasComment("Column comment"); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.HasIndex("Name"); - - e.ToTable( - tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customer]') AND [c].[name] = N'Name'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customer] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customer] ALTER COLUMN [Name] nvarchar(450) NULL; -DECLARE @defaultSchema2 AS sysname; -SET @defaultSchema2 = SCHEMA_NAME(); -DECLARE @description2 AS sql_variant; -SET @description2 = N'Column comment'; -EXEC sp_addextendedproperty 'MS_Description', @description2, 'SCHEMA', @defaultSchema2, 'TABLE', N'Customer', 'COLUMN', N'Name'; -""", - // - """ -ALTER TABLE [Customer] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customer] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -CREATE INDEX [IX_Customer_Name] ON [Customer] ([Name]); -""", - // - """ -ALTER TABLE [Customer] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customer] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customer] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public async Task Alter_comments_for_temporal_table() - { - await Test( - 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( - tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name").HasComment("Column comment"); - e.ToTable(tb => tb.HasComment("Table comment")); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name").HasComment("Modified column comment"); - e.ToTable(tb => tb.HasComment("Modified table comment")); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customer", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.NotNull(table[SqlServerAnnotationNames.TemporalHistoryTableName]); - Assert.Equal("SystemTimeStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("SystemTimeEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @defaultSchema2 AS sysname; -SET @defaultSchema2 = SCHEMA_NAME(); -DECLARE @description2 AS sql_variant; -EXEC sp_dropextendedproperty 'MS_Description', 'SCHEMA', @defaultSchema2, 'TABLE', N'Customer'; -SET @description2 = N'Modified table comment'; -EXEC sp_addextendedproperty 'MS_Description', @description2, 'SCHEMA', @defaultSchema2, 'TABLE', N'Customer'; -""", - // - """ -DECLARE @defaultSchema3 AS sysname; -SET @defaultSchema3 = SCHEMA_NAME(); -DECLARE @description3 AS sql_variant; -EXEC sp_dropextendedproperty 'MS_Description', 'SCHEMA', @defaultSchema3, 'TABLE', N'Customer', 'COLUMN', N'Name'; -SET @description3 = N'Modified column comment'; -EXEC sp_addextendedproperty 'MS_Description', @description3, 'SCHEMA', @defaultSchema3, 'TABLE', N'Customer', 'COLUMN', N'Name'; -"""); - } - - [ConditionalFact] - public virtual async Task Add_index_to_temporal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("Number"); - 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 => builder.Entity( - "Customer", e => - { - e.HasIndex("Name"); - e.HasIndex("Number").IsUnique(); - }), - 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(2, table.Indexes.Count); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] ALTER COLUMN [Name] nvarchar(450) NULL; -""", - // - """ -CREATE INDEX [IX_Customers_Name] ON [Customers] ([Name]); -""", - // - """ -CREATE UNIQUE INDEX [IX_Customers_Number] ON [Customers] ([Number]); -"""); - } - - [ConditionalFact] - public virtual async Task Add_index_on_period_column_to_temporal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("Number"); - 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 => builder.Entity( - "Customer", e => - { - e.HasIndex("Start"); - e.HasIndex("End", "Name"); - }), - 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]); - - // TODO: issue #26008 - we don't reverse engineer indexes on period columns since the columns are not added to the database model - //Assert.Equal(2, table.Indexes.Count); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] ALTER COLUMN [Name] nvarchar(450) NULL; -""", - // - """ -CREATE INDEX [IX_Customers_End_Name] ON [Customers] ([End], [Name]); -""", - // - """ -CREATE INDEX [IX_Customers_Start] ON [Customers] ([Start]); -"""); - } - - [ConditionalFact] - public virtual async Task History_table_schema_created_when_necessary() - { - 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", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.UseHistoryTable("MyHistoryTable", "mySchema2"); - })); - }); - }, - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("mySchema", table.Schema); - Assert.Equal("mySchema2", table[SqlServerAnnotationNames.TemporalHistoryTableSchema]); - }); - - AssertSql( - """ -IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); -""", - // - """ -IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];'); -""", - // - """ -CREATE TABLE [mySchema].[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 = [mySchema2].[MyHistoryTable])); -"""); - } - - [ConditionalFact] - public virtual async Task History_table_schema_not_created_if_we_know_it_already_exists1() - { - 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", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }); - - builder.Entity( - "Order", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Orders", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }); - }, - model => - { - Assert.Equal(2, model.Tables.Count); - Assert.True(model.Tables.All(x => x.Schema == "mySchema")); - Assert.True(model.Tables.All(x => x[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string == "mySchema")); - }); - - AssertSql( - """ -IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); -""", - // - """ -CREATE TABLE [mySchema].[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 = [mySchema].[CustomersHistory])); -""", - // - """ -CREATE TABLE [mySchema].[Orders] ( - [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_Orders] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema].[OrdersHistory])); -"""); - } - - [ConditionalFact] - public virtual async Task History_table_schema_not_created_if_we_know_it_already_exists2() - { - 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", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.UseHistoryTable("CustomersHistoryTable", "mySchema2"); - })); - }); - - builder.Entity( - "Order", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Orders", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.UseHistoryTable("OrdersHistoryTable", "mySchema2"); - })); - }); - }, - model => - { - Assert.Equal(2, model.Tables.Count); - Assert.True(model.Tables.All(x => x.Schema == "mySchema")); - Assert.True(model.Tables.All(x => x[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string == "mySchema2")); - }); - - AssertSql( - """ -IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); -""", - // - """ -IF SCHEMA_ID(N'mySchema2') IS NULL EXEC(N'CREATE SCHEMA [mySchema2];'); -""", - // - """ -CREATE TABLE [mySchema].[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 = [mySchema2].[CustomersHistoryTable])); -""", - // - """ -CREATE TABLE [mySchema].[Orders] ( - [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_Orders] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([SystemTimeStart], [SystemTimeEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [mySchema2].[OrdersHistoryTable])); -"""); - } - - [ConditionalFact] - public virtual async Task History_table_schema_renamed_to_one_exisiting_in_the_model() - { - await Test( - 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", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.UseHistoryTable("CustomersHistoryTable", "mySchema2"); - })); - }); - - builder.Entity( - "Order", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Orders", "mySchema2", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.UseHistoryTable("OrdersHistoryTable", "mySchema2"); - })); - }); - }, - 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", "mySchema", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.UseHistoryTable("CustomersHistoryTable", "mySchema2"); - })); - }); - - builder.Entity( - "Order", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Orders", "mySchema2", tb => tb.IsTemporal( - ttb => - { - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - ttb.UseHistoryTable("OrdersHistoryTable", "mySchema"); - })); - }); - }, - model => - { - Assert.Equal(2, model.Tables.Count); - var customers = model.Tables.First(t => t.Name == "Customers"); - Assert.Equal("mySchema", customers.Schema); - Assert.Equal("mySchema2", customers[SqlServerAnnotationNames.TemporalHistoryTableSchema]); - - var orders = model.Tables.First(t => t.Name == "Orders"); - Assert.Equal("mySchema2", orders.Schema); - Assert.Equal("mySchema", orders[SqlServerAnnotationNames.TemporalHistoryTableSchema]); - }); - - // TODO: we could avoid creating the schema if we peek into the model - AssertSql( - """ -IF SCHEMA_ID(N'mySchema') IS NULL EXEC(N'CREATE SCHEMA [mySchema];'); -""", - // - """ -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])) -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_table_rename_and_delete_columns_in_one_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name"); - e.Property("Number"); - e.Property("Dob"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("FullName"); - e.Property("DateOfBirth"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("DateOfBirth", c.Name), - c => Assert.Equal("FullName", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @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]; -""", - // - """ -EXEC sp_rename N'[Customers].[Name]', N'FullName', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[Customers].[Dob]', N'DateOfBirth', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[HistoryTable].[Dob]', N'DateOfBirth', 'COLUMN'; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_table_rename_and_delete_columns_and_also_rename_table_in_one_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Name"); - e.Property("Number"); - - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("FullName"); - - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "ModifiedCustomers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("ModifiedCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("FullName", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -DECLARE @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]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'ModifiedCustomers', 'OBJECT'; -""", - // - """ -EXEC sp_rename N'[ModifiedCustomers].[Name]', N'FullName', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', 'COLUMN'; -""", - // - """ -ALTER TABLE [ModifiedCustomers] ADD CONSTRAINT [PK_ModifiedCustomers] PRIMARY KEY ([Id]); -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [ModifiedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_table_rename_and_delete_columns_and_also_rename_history_table_in_one_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Name"); - e.Property("Number"); - - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("FullName"); - - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("ModifiedHistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("ModifiedHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("FullName", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @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]; -""", - // - """ -EXEC sp_rename N'[Customers].[Name]', N'FullName', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[HistoryTable].[Name]', N'FullName', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[HistoryTable]', N'ModifiedHistoryTable', 'OBJECT'; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[ModifiedHistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_table_delete_column_and_add_another_column_in_one_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name"); - e.Property("Number"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name"); - e.Property("DateOfBirth"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("DateOfBirth", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @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]; -""", - // - """ -ALTER TABLE [Customers] ADD [DateOfBirth] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [HistoryTable] ADD [DateOfBirth] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_table_delete_column_and_alter_another_column_in_one_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name"); - e.Property("Number"); - e.Property("DateOfBirth"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Name").HasComment("My comment"); - e.Property("DateOfBirth"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("DateOfBirth", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @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 @defaultSchema4 AS sysname; -SET @defaultSchema4 = SCHEMA_NAME(); -DECLARE @description4 AS sql_variant; -SET @description4 = N'My comment'; -EXEC sp_addextendedproperty 'MS_Description', @description4, 'SCHEMA', @defaultSchema4, 'TABLE', N'Customers', 'COLUMN', N'Name'; -""", - // - """ -DECLARE @defaultSchema5 AS sysname; -SET @defaultSchema5 = SCHEMA_NAME(); -DECLARE @description5 AS sql_variant; -SET @description5 = N'My comment'; -EXEC sp_addextendedproperty 'MS_Description', @description5, 'SCHEMA', @defaultSchema5, 'TABLE', N'HistoryTable', 'COLUMN', N'Name'; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_table_rename_and_alter_period_column_in_one_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -EXEC sp_rename N'[Customers].[Start]', N'ModifiedStart', 'COLUMN'; -""", - // - """ -DECLARE @defaultSchema1 AS sysname; -SET @defaultSchema1 = SCHEMA_NAME(); -DECLARE @description1 AS sql_variant; -SET @description1 = N'My comment'; -EXEC sp_addextendedproperty 'MS_Description', @description1, 'SCHEMA', @defaultSchema1, 'TABLE', N'Customers', 'COLUMN', N'End'; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_table_delete_column_rename_and_alter_period_column_in_one_migration() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("DateOfBirth"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").HasComment("My comment").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start").HasColumnName("ModifiedStart"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("ModifiedStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DECLARE @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'DateOfBirth'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [DateOfBirth]; -""", - // - """ -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'DateOfBirth'); -IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [HistoryTable] DROP CONSTRAINT [' + @var3 + '];'); -ALTER TABLE [HistoryTable] DROP COLUMN [DateOfBirth]; -""", - // - """ -EXEC sp_rename N'[Customers].[Start]', N'ModifiedStart', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[HistoryTable].[Start]', N'ModifiedStart', 'COLUMN'; -""", - // - """ -DECLARE @defaultSchema4 AS sysname; -SET @defaultSchema4 = SCHEMA_NAME(); -DECLARE @description4 AS sql_variant; -SET @description4 = N'My comment'; -EXEC sp_addextendedproperty 'MS_Description', @description4, 'SCHEMA', @defaultSchema4, 'TABLE', N'Customers', 'COLUMN', N'End'; -""", - // - """ -DECLARE @defaultSchema5 AS sysname; -SET @defaultSchema5 = SCHEMA_NAME(); -DECLARE @description5 AS sql_variant; -SET @description5 = N'My comment'; -EXEC sp_addextendedproperty 'MS_Description', @description5, 'SCHEMA', @defaultSchema5, 'TABLE', N'HistoryTable', 'COLUMN', N'End'; -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Convert_from_temporal_table_with_minimal_configuration_to_explicit_one_noop() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable("Customers", tb => tb.IsTemporal()); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql(); - } - - [ConditionalFact] - public virtual async Task Convert_from_temporal_table_with_explicit_configuration_to_minimal_one_noop() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable("Customers", tb => tb.IsTemporal()); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql(); - } - - [ConditionalFact] - public virtual async Task Convert_from_temporal_table_with_minimal_configuration_to_explicit_one() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable("Customers", tb => tb.IsTemporal()); - }), - 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"); - 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.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -EXEC sp_rename N'[Customers].[PeriodStart]', N'Start', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[Customers].[PeriodEnd]', N'End', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[CustomersHistory]', N'HistoryTable', 'OBJECT'; -"""); - } - - [ConditionalFact] - public virtual async Task Change_names_of_period_columns_in_temporal_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("ValidFrom").ValueGeneratedOnAddOrUpdate(); - e.Property("ValidTo").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("ValidFrom"); - ttb.HasPeriodEnd("ValidTo"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("ValidFrom", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("ValidTo", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -EXEC sp_rename N'[Customers].[PeriodStart]', N'ValidFrom', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[Customers].[PeriodEnd]', N'ValidTo', 'COLUMN'; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_to_temporal_and_add_new_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - 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.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customers] ADD [Number] int NOT NULL DEFAULT 0; -""", - // - """ -ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_to_temporal_and_remove_existing_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - 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.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Number'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Number]; -""", - // - """ -ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_to_temporal_and_rename_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("NewNumber"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - 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.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("NewNumber", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -EXEC sp_rename N'[Customers].[Number]', N'NewNumber', 'COLUMN'; -""", - // - """ -ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_from_temporal_and_add_new_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [End]; -""", - // - """ -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'Start'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Start]; -""", - // - """ -DROP TABLE [HistoryTable]; -""", - // - """ -ALTER TABLE [Customers] ADD [Number] int NOT NULL DEFAULT 0; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_from_temporal_and_remove_existing_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.Property("Name"); - e.Property("Number"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable("Customers"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [End]; -""", - // - """ -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 @var4 sysname; -SELECT @var4 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Start'); -IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var4 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Start]; -""", - // - """ -DROP TABLE [HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_from_temporal_and_rename_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("NewNumber"); - e.ToTable("Customers"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("NewNumber", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [End]; -""", - // - """ -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'Start'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Start]; -""", - // - """ -EXEC sp_rename N'[Customers].[Number]', N'NewNumber', 'COLUMN'; -""", - // - """ -DROP TABLE [HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_to_temporal_rename_table_and_add_new_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable( - "NewCustomers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("NewCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [Number] int NOT NULL DEFAULT 0; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); -""", - // - """ -ALTER TABLE [NewCustomers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [NewCustomers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [NewCustomers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_to_temporal_rename_table_and_remove_existing_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "NewCustomers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("NewCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Number'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Number]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); -""", - // - """ -ALTER TABLE [NewCustomers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [NewCustomers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [NewCustomers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_to_temporal_rename_table_and_rename_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("NewNumber"); - e.ToTable( - "NewCustomers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("NewCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("NewNumber", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; -""", - // - """ -EXEC sp_rename N'[NewCustomers].[Number]', N'NewNumber', 'COLUMN'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); -""", - // - """ -ALTER TABLE [NewCustomers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [NewCustomers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [NewCustomers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_from_temporal_rename_table_and_add_new_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("NewCustomers"); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("NewCustomers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [End]; -""", - // - """ -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'Start'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Start]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; -""", - // - """ -DROP TABLE [HistoryTable]; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [Number] int NOT NULL DEFAULT 0; -""", - // - """ -ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_rename_table_rename_history_table_and_add_new_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable( - "NewCustomers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("NewHistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables); - Assert.Equal("NewCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("NewHistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; -""", - // - """ -EXEC sp_rename N'[HistoryTable]', N'NewHistoryTable', 'OBJECT'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD [Number] int NOT NULL DEFAULT 0; -""", - // - """ -ALTER TABLE [NewHistoryTable] ADD [Number] int NOT NULL DEFAULT 0; -""", - // - """ -ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [NewCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[NewHistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_convert_from_temporal_create_another_table_with_same_name_as_history_table() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - builder => - { - builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }); - - builder.Entity( - "History", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("HistoryTable"); - }); - }, - model => - { - var customersTable = Assert.Single(model.Tables, t => t.Name == "Customers"); - var historyTable = Assert.Single(model.Tables, t => t.Name == "HistoryTable"); - - Assert.Collection( - customersTable.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - customersTable.Columns.Single(c => c.Name == "Id"), - Assert.Single(customersTable.PrimaryKey!.Columns)); - - Assert.Collection( - historyTable.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - historyTable.Columns.Single(c => c.Name == "Id"), - Assert.Single(historyTable.PrimaryKey!.Columns)); - }); - - AssertSql( - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'End'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [End]; -""", - // - """ -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'Start'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [Start]; -""", - // - """ -DROP TABLE [HistoryTable]; -""", - // - """ -CREATE TABLE [HistoryTable] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NULL, - [Number] int NOT NULL, - CONSTRAINT [PK_HistoryTable] PRIMARY KEY ([Id]) -); -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_convert_regular_table_to_temporal_and_add_rowversion_column() - { - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Start").ValueGeneratedOnAddOrUpdate(); - e.Property("End").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.Property("MyRowVersion").IsRowVersion(); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable"); - ttb.HasPeriodStart("Start"); - ttb.HasPeriodEnd("End"); - })); - }), - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("Start", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("End", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("HistoryTable", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name), - c => Assert.Equal("MyRowVersion", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -ALTER TABLE [Customers] ADD [End] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customers] ADD [MyRowVersion] rowversion NULL; -""", - // - """ -ALTER TABLE [Customers] ADD [Start] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([Start], [End]) -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [Start] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [End] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[HistoryTable]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_create_temporal_table_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.CreateTable( - name: "Customers", - columns: table => new - { - Id = table.Column(type: "int", nullable: false) - .Annotation("SqlServer:Identity", "1, 1") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), - Name = table.Column(type: "nvarchar(max)", nullable: false) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), - PeriodEnd = table.Column(type: "datetime2", nullable: false) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"), - PeriodStart = table.Column(type: "datetime2", nullable: false) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - }, - constraints: table => - { - table.PrimaryKey("PK_Customers", x => x.Id); - }) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => { }, - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'CREATE TABLE [Customers] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NOT NULL, - [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL, - [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL, - CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]), - PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd]) -) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[CustomersHistory]))'); -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_convert_regular_table_to_temporal_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.AlterTable( - name: "Customers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "Customers", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "Customers", - type: "int", - nullable: false, - oldClrType: typeof(int), - oldType: "int") - .Annotation("SqlServer:Identity", "1, 1") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - .OldAnnotation("SqlServer:Identity", "1, 1"); - - migrationBuilder.AddColumn( - name: "PeriodEnd", - table: "Customers", - type: "datetime2", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AddColumn( - name: "PeriodStart", - table: "Customers", - type: "datetime2", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("Number"); - e.ToTable("Customers"); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("Number", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -ALTER TABLE [Customers] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customers] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [PeriodStart] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [PeriodEnd] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomersHistory]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_convert_regular_table_with_rowversion_to_temporal_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.AlterTable( - name: "Customers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "Customers", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "MyRowVersion", - table: "Customers", - type: "rowversion", - rowVersion: true, - nullable: false, - oldClrType: typeof(byte[]), - oldType: "rowversion", - oldRowVersion: true) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "Customers", - type: "int", - nullable: false, - oldClrType: typeof(int), - oldType: "int") - .Annotation("SqlServer:Identity", "1, 1") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - .OldAnnotation("SqlServer:Identity", "1, 1"); - - migrationBuilder.AddColumn( - name: "PeriodEnd", - table: "Customers", - type: "datetime2", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AddColumn( - name: "PeriodStart", - table: "Customers", - type: "datetime2", - nullable: false, - defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("MyRowVersion").IsRowVersion(); - e.ToTable("Customers"); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("CustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("MyRowVersion", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -ALTER TABLE [Customers] ADD [PeriodEnd] datetime2 NOT NULL DEFAULT '9999-12-31T23:59:59.9999999'; -""", - // - """ -ALTER TABLE [Customers] ADD [PeriodStart] datetime2 NOT NULL DEFAULT '0001-01-01T00:00:00.0000000'; -""", - // - """ -ALTER TABLE [Customers] ADD PERIOD FOR SYSTEM_TIME ([PeriodStart], [PeriodEnd]) -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [PeriodStart] ADD HIDDEN -""", - // - """ -ALTER TABLE [Customers] ALTER COLUMN [PeriodEnd] ADD HIDDEN -""", - // - """ -DECLARE @historyTableSchema sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + '].[CustomersHistory]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_rename_temporal_table_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.DropPrimaryKey( - name: "PK_Customers", - table: "Customers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.RenameTable( - name: "Customers", - newName: "RenamedCustomers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null); - - migrationBuilder.AlterTable( - name: "RenamedCustomers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "PeriodStart", - table: "RenamedCustomers", - type: "datetime2", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "datetime2") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "PeriodEnd", - table: "RenamedCustomers", - type: "datetime2", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "datetime2") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "RenamedCustomers", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "RenamedCustomers", - type: "int", - nullable: false, - oldClrType: typeof(int), - oldType: "int") - .Annotation("SqlServer:Identity", "1, 1") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "RenamedCustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart") - .OldAnnotation("SqlServer:Identity", "1, 1") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AddPrimaryKey( - name: "PK_RenamedCustomers", - table: "RenamedCustomers", - column: "Id"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "RenamedCustomers"); - Assert.Equal("RenamedCustomers", table.Name); - Assert.Equal(true, table[SqlServerAnnotationNames.IsTemporal]); - Assert.Equal("PeriodStart", table[SqlServerAnnotationNames.TemporalPeriodStartPropertyName]); - Assert.Equal("PeriodEnd", table[SqlServerAnnotationNames.TemporalPeriodEndPropertyName]); - Assert.Equal("RenamedCustomersHistory", table[SqlServerAnnotationNames.TemporalHistoryTableName]); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'RenamedCustomers', 'OBJECT'; -""", - // - """ -EXEC sp_rename N'[CustomersHistory]', N'RenamedCustomersHistory', 'OBJECT'; -""", - // - """ -ALTER TABLE [RenamedCustomers] ADD CONSTRAINT [PK_RenamedCustomers] PRIMARY KEY ([Id]); -""", - // - """ -DECLARE @historyTableSchema1 sysname = SCHEMA_NAME() -EXEC(N'ALTER TABLE [RenamedCustomers] SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema1 + '].[RenamedCustomersHistory]))') -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_convert_temporal_table_to_regular_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.DropColumn( - name: "PeriodEnd", - table: "Customers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.DropColumn( - name: "PeriodStart", - table: "Customers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterTable( - name: "Customers") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "Customers", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "Customers", - type: "int", - nullable: false, - oldClrType: typeof(int), - oldType: "int") - .Annotation("SqlServer:Identity", "1, 1") - .OldAnnotation("SqlServer:Identity", "1, 1") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP PERIOD FOR SYSTEM_TIME -""", - // - """ -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'PeriodEnd'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [PeriodEnd]; -""", - // - """ -DECLARE @var2 sysname; -SELECT @var2 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'PeriodStart'); -IF @var2 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var2 + '];'); -ALTER TABLE [Customers] DROP COLUMN [PeriodStart]; -""", - // - """ -DROP TABLE [CustomersHistory]; -""", - // - """ -DECLARE @var3 sysname; -SELECT @var3 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Name'); -IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var3 + '];'); -ALTER TABLE [Customers] ALTER COLUMN [Name] nvarchar(max) NOT NULL; -""", - // - """ -DECLARE @var4 sysname; -SELECT @var4 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'Id'); -IF @var4 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var4 + '];'); -ALTER TABLE [Customers] ALTER COLUMN [Id] int NOT NULL; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_add_column_to_temporal_table_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.AddColumn( - name: "MyRowVersion", - table: "Customers", - type: "rowversion", - rowVersion: true, - nullable: false, - defaultValue: new byte[0]) - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name), - c => Assert.Equal("MyRowVersion", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -ALTER TABLE [Customers] ADD [MyRowVersion] rowversion NOT NULL; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_remove_temporal_table_column_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.DropColumn( - name: "IsVip", - table: "Customers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.Property("IsVip"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -DECLARE @var1 sysname; -SELECT @var1 = [d].[name] -FROM [sys].[default_constraints] [d] -INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id] -WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Customers]') AND [c].[name] = N'IsVip'); -IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [Customers] DROP CONSTRAINT [' + @var1 + '];'); -ALTER TABLE [Customers] DROP COLUMN [IsVip]; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_rename_temporal_table_column_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.RenameColumn( - name: "Name", - table: "Customers", - newName: "FullName") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("FullName", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -EXEC sp_rename N'[Customers].[Name]', N'FullName', 'COLUMN'; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_rename_temporal_table_period_columns_using_EF8_migration_code() - { - var migrationBuilder = new MigrationBuilder("Microsoft.EntityFrameworkCore.SqlServer"); - - migrationBuilder.RenameColumn( - name: "PeriodStart", - table: "Customers", - newName: "NewPeriodStart") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.RenameColumn( - name: "PeriodEnd", - table: "Customers", - newName: "NewPeriodEnd") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterTable( - name: "Customers") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Name", - table: "Customers", - type: "nvarchar(max)", - nullable: false, - oldClrType: typeof(string), - oldType: "nvarchar(max)") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "Id", - table: "Customers", - type: "int", - nullable: false, - oldClrType: typeof(int), - oldType: "int") - .Annotation("SqlServer:Identity", "1, 1") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") - .OldAnnotation("SqlServer:Identity", "1, 1") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "NewPeriodStart", - table: "Customers", - type: "datetime2", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "datetime2") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - migrationBuilder.AlterColumn( - name: "NewPeriodEnd", - table: "Customers", - type: "datetime2", - nullable: false, - oldClrType: typeof(DateTime), - oldType: "datetime2") - .Annotation("SqlServer:IsTemporal", true) - .Annotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .Annotation("SqlServer:TemporalHistoryTableSchema", null) - .Annotation("SqlServer:TemporalPeriodEndColumnName", "NewPeriodEnd") - .Annotation("SqlServer:TemporalPeriodStartColumnName", "NewPeriodStart") - .OldAnnotation("SqlServer:IsTemporal", true) - .OldAnnotation("SqlServer:TemporalHistoryTableName", "CustomersHistory") - .OldAnnotation("SqlServer:TemporalHistoryTableSchema", null) - .OldAnnotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd") - .OldAnnotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart"); - - await Test( - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("PeriodStart").ValueGeneratedOnAddOrUpdate(); - e.Property("PeriodEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - e.Property("Name"); - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("CustomersHistory"); - ttb.HasPeriodStart("PeriodStart"); - ttb.HasPeriodEnd("PeriodEnd"); - })); - }), - migrationBuilder.Operations, - model => - { - var table = Assert.Single(model.Tables, t => t.Name == "Customers"); - Assert.Equal("Customers", table.Name); - - Assert.Collection( - table.Columns, - c => Assert.Equal("Id", c.Name), - c => Assert.Equal("Name", c.Name)); - Assert.Same( - table.Columns.Single(c => c.Name == "Id"), - Assert.Single(table.PrimaryKey!.Columns)); - }); - - AssertSql( -""" -EXEC sp_rename N'[Customers].[PeriodStart]', N'NewPeriodStart', 'COLUMN'; -""", - // - """ -EXEC sp_rename N'[Customers].[PeriodEnd]', N'NewPeriodEnd', 'COLUMN'; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_drop_temporal_table_and_add_the_same_table_in_one_migration() - { - await TestComposite( - [ - 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("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - 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("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }) - ]); - - AssertSql( -""" -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DROP TABLE [Customers]; -""", - // - """ -DROP TABLE [historySchema].[HistoryTable]; -""", - // - """ -IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); -""", - // - """ -CREATE TABLE [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 = [historySchema].[HistoryTable])); -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_create_temporal_and_drop() - { - await TestComposite( - [ - 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("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - builder => { }, - ]); - - AssertSql( -""" -IF SCHEMA_ID(N'historySchema') IS NULL EXEC(N'CREATE SCHEMA [historySchema];'); -""", - // - """ -CREATE TABLE [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 = [historySchema].[HistoryTable])); -""", - // - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DROP TABLE [Customers]; -""", - // - """ -DROP TABLE [historySchema].[HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_rename_temporal_and_drop() - { - await TestComposite( - [ - 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("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - 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( - "NewCustomers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - builder => { }, - ]); - - AssertSql( -""" -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -ALTER TABLE [Customers] DROP CONSTRAINT [PK_Customers]; -""", - // - """ -EXEC sp_rename N'[Customers]', N'NewCustomers', 'OBJECT'; -""", - // - """ -ALTER TABLE [NewCustomers] ADD CONSTRAINT [PK_NewCustomers] PRIMARY KEY ([Id]); -""", - // - """ -DROP TABLE [NewCustomers]; -""", - // - """ -DROP TABLE [historySchema].[HistoryTable]; -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_rename_period_drop_table_create_as_regular() - { - await TestComposite( - [ - 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("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.Property("NewSystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("NewSystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - builder => { }, - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - - e.ToTable("Customers"); - }), - ]); - - AssertSql( -""" -EXEC sp_rename N'[Customers].[SystemTimeStart]', N'NewSystemTimeStart', 'COLUMN'; -""", - // - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DROP TABLE [Customers]; -""", - // - """ -DROP TABLE [historySchema].[HistoryTable]; -""", - // - """ -CREATE TABLE [Customers] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NULL, - CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) -); -"""); - } - - [ConditionalFact] - public virtual async Task Temporal_multiop_rename_column_drop_table_create_as_regular() - { - await TestComposite( - [ - 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("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("NewName"); - e.Property("SystemTimeStart").ValueGeneratedOnAddOrUpdate(); - e.Property("SystemTimeEnd").ValueGeneratedOnAddOrUpdate(); - e.HasKey("Id"); - - e.ToTable( - "Customers", tb => tb.IsTemporal( - ttb => - { - ttb.UseHistoryTable("HistoryTable", "historySchema"); - ttb.HasPeriodStart("SystemTimeStart"); - ttb.HasPeriodEnd("SystemTimeEnd"); - })); - }), - builder => { }, - builder => builder.Entity( - "Customer", e => - { - e.Property("Id").ValueGeneratedOnAdd(); - e.Property("Name"); - e.HasKey("Id"); - - e.ToTable("Customers"); - }), - ]); - - AssertSql( -""" -EXEC sp_rename N'[Customers].[Name]', N'NewName', 'COLUMN'; -""", - // - """ -ALTER TABLE [Customers] SET (SYSTEM_VERSIONING = OFF) -""", - // - """ -DROP TABLE [Customers]; -""", - // - """ -DROP TABLE [historySchema].[HistoryTable]; -""", - // - """ -CREATE TABLE [Customers] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NULL, - CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) -); -"""); - } - [ConditionalFact] public override async Task Add_required_primitive_collection_to_existing_table() { diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 225dc8a64f8..6660e3f221a 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -899,6 +899,72 @@ protected virtual void ConfigureProperty(IMutableProperty property, string confi } } + [ConditionalFact] + public void DefaultValue_with_explicit_constraint_name_throws_for_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().Property(x => x.Name).HasDefaultValue("Miauo", defaultConstraintName: "MyConstraint"); + modelBuilder.Entity().HasBaseType(); + modelBuilder.Entity().HasBaseType(); + + VerifyError( + RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc("MyConstraint"), + modelBuilder); + } + + [ConditionalFact] + public void DefaultValueSql_with_explicit_constraint_name_throws_for_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().Property(x => x.Name).HasDefaultValueSql("NEWID()", defaultConstraintName: "MyConstraint"); + modelBuilder.Entity().HasBaseType(); + modelBuilder.Entity().HasBaseType(); + + VerifyError( + RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc("MyConstraint"), + modelBuilder); + } + + [ConditionalFact] + public void DefaultValue_with_empty_explicit_constraint_name_throws() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.UseNamedDefaultConstraints(); + modelBuilder.Entity().UseTpcMappingStrategy(); + + Assert.Throws( + () => modelBuilder.Entity().Property(x => x.Name).HasDefaultValue("Miauo", defaultConstraintName: "")); + } + + [ConditionalFact] + public void DefaultValueSql_with_empty_explicit_constraint_name_throws() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.UseNamedDefaultConstraints(); + modelBuilder.Entity().UseTpcMappingStrategy(); + + Assert.Throws( + () => modelBuilder.Entity().Property(x => x.Name).HasDefaultValueSql("Miauo", defaultConstraintName: "")); + } + + [ConditionalFact] + public void DefaultValue_with_implicit_constraint_name_throws_for_TPC() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.UseNamedDefaultConstraints(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().Property(x => x.Name).HasDefaultValue("Miauo"); + modelBuilder.Entity().HasBaseType(); + modelBuilder.Entity().Property(x => x.Breed).HasDefaultValue("Ragdoll", defaultConstraintName: "DF_Cat_Name"); + modelBuilder.Entity().HasBaseType(); + + VerifyError( + RelationalStrings.ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash("DF_Cat_Name"), + modelBuilder); + } + [ConditionalFact] public void Temporal_can_only_be_specified_on_root_entities() { From ee500899c5df60d18066726a033307633690316b Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Mon, 19 May 2025 21:39:00 -0700 Subject: [PATCH 2/2] =?UTF-8?q?Default=20constraint=20name=20PR=20feedback?= =?UTF-8?q?:=20=C2=95=09Add=20model=20builder=20extention=20methods=20for?= =?UTF-8?q?=20other=20types=20of=20properties=20=C2=95=09Uniquify=20names?= =?UTF-8?q?=20using=20the=20established=20pattern=20in=20SharedTableConven?= =?UTF-8?q?tion=20=C2=95=09Make=20GetDefaultConstraintName=20return=20the?= =?UTF-8?q?=20generated=20default=20constraint=20name=20if=20UseNamedDefau?= =?UTF-8?q?ltConstraints=20was=20called?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/RelationalModelExtensions.cs | 6 +- .../RelationalPropertyExtensions.cs | 30 +++- .../RelationalConventionSetBuilder.cs | 6 - .../RelationalDefaultConstraintConvention.cs | 150 ------------------ .../Conventions/SharedTableConvention.cs | 117 +++++++++++++- .../SqlServerAnnotationCodeGenerator.cs | 3 +- ...ypePrimitiveCollectionBuilderExtensions.cs | 82 ++++++++++ ...verComplexTypePropertyBuilderExtensions.cs | 80 ++++++++++ ...verPrimitiveCollectionBuilderExtensions.cs | 80 ++++++++++ .../SqlServerPropertyBuilderExtensions.cs | 30 ++-- .../SqlServerSharedTableConvention.cs | 4 + .../Internal/SqlServerAnnotationProvider.cs | 2 +- ...nsSqlServerTest.NamedDefaultConstraints.cs | 6 +- 13 files changed, 404 insertions(+), 192 deletions(-) delete mode 100644 src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs diff --git a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs index efc3a4f26ed..b23372623b8 100644 --- a/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalModelExtensions.cs @@ -537,10 +537,10 @@ public static void SetCollation(this IMutableModel model, string? value) /// Returns the value indicating whether named default constraints should be used. /// /// The model to get the value for. - public static bool? AreNamedDefaultConstraintsUsed(this IReadOnlyModel model) + public static bool AreNamedDefaultConstraintsUsed(this IReadOnlyModel model) => (model is RuntimeModel) ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) - : (bool?)model[RelationalAnnotationNames.UseNamedDefaultConstraints]; + : (bool?)model[RelationalAnnotationNames.UseNamedDefaultConstraints] ?? false; /// /// Sets the value indicating whether named default constraints should be used. @@ -558,7 +558,7 @@ public static void UseNamedDefaultConstraints(this IMutableModel model, bool val /// Indicates whether the configuration was specified using a data annotation. public static bool? UseNamedDefaultConstraints( this IConventionModel model, - bool value, + bool? value, bool fromDataAnnotation = false) => (bool?)model.SetOrRemoveAnnotation( RelationalAnnotationNames.UseNamedDefaultConstraints, diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index d6421eb70f2..6c703b725bd 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -2092,20 +2092,42 @@ public static void SetJsonPropertyName(this IMutableProperty property, string? n /// /// The property. public static string? GetDefaultConstraintName(this IReadOnlyProperty property) - => (property is RuntimeProperty) + => property is RuntimeProperty + ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) + : (string?)property[RelationalAnnotationNames.DefaultConstraintName] + ?? (ShouldHaveDefaultConstraintName(property) + && StoreObjectIdentifier.Create(property.DeclaringType, StoreObjectType.Table) is StoreObjectIdentifier table + ? property.GenerateDefaultConstraintName(table) + : null); + + /// + /// Gets the default constraint name. + /// + /// The property. + /// The store object identifier to generate the name for. + public static string? GetDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + => property is RuntimeProperty ? throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData) - : (string?)property[RelationalAnnotationNames.DefaultConstraintName]; + : (string?)property[RelationalAnnotationNames.DefaultConstraintName] + ?? (ShouldHaveDefaultConstraintName(property) + ? property.GenerateDefaultConstraintName(storeObject) + : null); + + private static bool ShouldHaveDefaultConstraintName(IReadOnlyProperty property) + => property.DeclaringType.Model.AreNamedDefaultConstraintsUsed() + && (property[RelationalAnnotationNames.DefaultValue] is not null + || property[RelationalAnnotationNames.DefaultValueSql] is not null); /// /// Generates the default constraint name based on the table and column name. /// /// The property. - /// The store object identifier to generate the name from. + /// The store object identifier to generate the name for. public static string GenerateDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) { var candidate = $"DF_{storeObject.Name}_{property.GetColumnName(storeObject)}"; - return candidate.Length > 120 ? candidate[..120] : candidate; + return Uniquifier.Truncate(candidate, property.DeclaringType.Model.GetMaxIdentifierLength()); } /// diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 0531a79b407..b8e49bb22c2 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -89,12 +89,6 @@ public override ConventionSet CreateConventionSet() conventionSet.Replace( new RelationalRuntimeModelConvention(Dependencies, RelationalDependencies)); - var defaultConstraintConvention = new RelationalDefaultConstraintConvention(Dependencies, RelationalDependencies); - ConventionSet.AddAfter( - conventionSet.ModelFinalizingConventions, - defaultConstraintConvention, - typeof(SharedTableConvention)); - return conventionSet; } } diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs deleted file mode 100644 index 29e5f09f897..00000000000 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalDefaultConstraintConvention.cs +++ /dev/null @@ -1,150 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; - -/// -/// A convention that manipulates names of default constraints to avoid clashes. -/// -/// -/// See Model building conventions for more information and examples. -/// -public class RelationalDefaultConstraintConvention : IModelFinalizingConvention -{ - /// - /// Creates a new instance of . - /// - /// Parameter object containing dependencies for this convention. - /// Parameter object containing relational dependencies for this convention. - public RelationalDefaultConstraintConvention( - ProviderConventionSetBuilderDependencies dependencies, - RelationalConventionSetBuilderDependencies relationalDependencies) - { - Dependencies = dependencies; - RelationalDependencies = relationalDependencies; - } - - /// - /// Dependencies for this service. - /// - protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } - - /// - /// Relational provider-specific dependencies for this service. - /// - protected virtual RelationalConventionSetBuilderDependencies RelationalDependencies { get; } - - /// - public void ProcessModelFinalizing( - IConventionModelBuilder modelBuilder, - IConventionContext context) - { - var explicitDefaultConstraintNames = new List(); - - // store all explicit names first - we don't want to change those in case they conflict with implicit names - foreach (var entity in modelBuilder.Metadata.GetEntityTypes()) - { - foreach (var property in entity.GetDeclaredProperties()) - { - if (property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName) is IConventionAnnotation annotation - && annotation.Value is string explicitDefaultConstraintName - && annotation.GetConfigurationSource() == ConfigurationSource.Explicit) - { - if (property.GetMappedStoreObjects(StoreObjectType.Table).Count() > 1) - { - // for TPC and some entity splitting scenarios (specifically composite key) we end up with multiple tables - // having to define the constraint. Since constraint has to be unique, we can't keep the same name for all - // Disabling this scenario until we have better place to configure the constraint name - // see issue #27970 - throw new InvalidOperationException( - RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc(explicitDefaultConstraintName)); - } - - explicitDefaultConstraintNames.Add(explicitDefaultConstraintName); - } - } - } - - var existingDefaultConstraintNames = new List(explicitDefaultConstraintNames); - var useNamedDefaultConstraints = modelBuilder.Metadata.AreNamedDefaultConstraintsUsed() == true; - - var suffixCounter = 1; - foreach (var entity in modelBuilder.Metadata.GetEntityTypes()) - { - foreach (var property in entity.GetDeclaredProperties()) - { - if (property.FindAnnotation(RelationalAnnotationNames.DefaultValue) is IConventionAnnotation defaultValueAnnotation - || property.FindAnnotation(RelationalAnnotationNames.DefaultValueSql) is IConventionAnnotation defaultValueSqlAnnotation) - { - var defaultConstraintNameAnnotation = property.FindAnnotation(RelationalAnnotationNames.DefaultConstraintName); - if (defaultConstraintNameAnnotation != null && defaultConstraintNameAnnotation.GetConfigurationSource() != ConfigurationSource.Convention) - { - // explicit constraint name - we already stored those so nothing to do here - continue; - } - - if (useNamedDefaultConstraints) - { - var mappedTables = property.GetMappedStoreObjects(StoreObjectType.Table); - var mappedTablesCount = mappedTables.Count(); - - if (mappedTablesCount == 0) - { - continue; - } - - if (mappedTablesCount == 1) - { - var constraintNameCandidate = property.GenerateDefaultConstraintName(mappedTables.First()); - if (!existingDefaultConstraintNames.Contains(constraintNameCandidate)) - { - // name that we generate is unique - add it to the list of names but we don't need to store it as annotation - existingDefaultConstraintNames.Add(constraintNameCandidate); - } - else - { - // conflict - generate name that is unique and store is as annotation - while (existingDefaultConstraintNames.Contains(constraintNameCandidate + suffixCounter)) - { - suffixCounter++; - } - - existingDefaultConstraintNames.Add(constraintNameCandidate + suffixCounter); - property.SetDefaultConstraintName(constraintNameCandidate + suffixCounter); - } - - continue; - } - - // TPC or entity splitting - when column is mapped to multiple tables, we can deal with them - // as long as there are no name clashes with some other constraints - // by the time we actually need to generate the constraint name (to put it in the annotation for the migration op) - // we will know which store object the property we are processing is mapped to, so can pick the right name based on that - // here though, where we want to uniquefy the name duplicates, we work on the model level so can't pick the right de-duped name - // so in case of conflict, we have to throw - // see issue #27970 - var constraintNameCandidates = new List(); - foreach (var mappedTable in mappedTables) - { - var constraintNameCandidate = property.GenerateDefaultConstraintName(mappedTable); - if (constraintNameCandidate != null) - { - if (!existingDefaultConstraintNames.Contains(constraintNameCandidate)) - { - constraintNameCandidates.Add(constraintNameCandidate); - } - else - { - throw new InvalidOperationException( - RelationalStrings.ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(constraintNameCandidate)); - } - } - } - - existingDefaultConstraintNames.AddRange(constraintNameCandidates); - } - } - } - } - } -} diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index be7355e1a53..85663dd18d4 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -51,6 +51,7 @@ public virtual void ProcessModelFinalizing( var foreignKeys = new Dictionary(); var indexes = new Dictionary(); var checkConstraints = new Dictionary<(string, string?), (IConventionCheckConstraint, StoreObjectIdentifier)>(); + var defaultConstraints = new Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)>(); var triggers = new Dictionary(); foreach (var ((tableName, schema), conventionEntityTypes) in tables) { @@ -76,6 +77,11 @@ public virtual void ProcessModelFinalizing( checkConstraints.Clear(); } + if (!DefaultConstraintsUniqueAcrossTables) + { + defaultConstraints.Clear(); + } + if (!TriggersUniqueAcrossTables) { triggers.Clear(); @@ -89,6 +95,7 @@ public virtual void ProcessModelFinalizing( UniquifyForeignKeyNames(entityType, foreignKeys, storeObject, maxLength); UniquifyIndexNames(entityType, indexes, storeObject, maxLength); UniquifyCheckConstraintNames(entityType, checkConstraints, storeObject, maxLength); + UniquifyDefaultConstraintNames(entityType, defaultConstraints, storeObject, maxLength); UniquifyTriggerNames(entityType, triggers, storeObject, maxLength); } } @@ -124,14 +131,19 @@ protected virtual bool CheckConstraintsUniqueAcrossTables protected virtual bool TriggersUniqueAcrossTables => true; + /// + /// Gets a value indicating whether default constraint names should be unique across tables. + /// + protected virtual bool DefaultConstraintsUniqueAcrossTables + => false; + private static void TryUniquifyTableNames( IConventionModel model, Dictionary<(string Name, string? Schema), List> tables, int maxLength) { Dictionary<(string Name, string? Schema), Dictionary<(string Name, string? Schema), List>>? - clashingTables - = null; + clashingTables = null; foreach (var entityType in model.GetEntityTypes()) { var tableName = entityType.GetTableName(); @@ -646,6 +658,107 @@ protected virtual bool AreCompatible( return null; } + private void UniquifyDefaultConstraintNames( + IConventionEntityType entityType, + Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)> defaultConstraints, + in StoreObjectIdentifier storeObject, + int maxLength) + { + foreach (var property in entityType.GetProperties()) + { + var constraintName = property.GetDefaultConstraintName(storeObject); + if (constraintName == null) + { + continue; + } + + var columnName = property.GetColumnName(storeObject); + if (columnName == null) + { + continue; + } + + if (!defaultConstraints.TryGetValue((constraintName, storeObject.Schema), out var otherPropertyPair)) + { + defaultConstraints[(constraintName, storeObject.Schema)] = (property, storeObject); + continue; + } + + var (otherProperty, otherStoreObject) = otherPropertyPair; + if (storeObject == otherStoreObject + && columnName == otherProperty.GetColumnName(storeObject) + && AreCompatibleDefaultConstraints(property, otherProperty, storeObject)) + { + continue; + } + + var newConstraintName = TryUniquifyDefaultConstraint(property, constraintName, storeObject.Schema, defaultConstraints, storeObject, maxLength); + if (newConstraintName != null) + { + defaultConstraints[(newConstraintName, storeObject.Schema)] = (property, storeObject); + continue; + } + + var newOtherConstraintName = TryUniquifyDefaultConstraint(otherProperty, constraintName, storeObject.Schema, defaultConstraints, otherStoreObject, maxLength); + if (newOtherConstraintName != null) + { + defaultConstraints[(constraintName, storeObject.Schema)] = (property, storeObject); + defaultConstraints[(newOtherConstraintName, otherStoreObject.Schema)] = otherPropertyPair; + } + } + } + + /// + /// Gets a value indicating whether two default constraints with the same name are compatible. + /// + /// A property with a default constraint. + /// Another property with a default constraint. + /// The identifier of the store object. + /// if compatible + protected virtual bool AreCompatibleDefaultConstraints( + IReadOnlyProperty property, + IReadOnlyProperty duplicateProperty, + in StoreObjectIdentifier storeObject) + => property.GetDefaultValue(storeObject) == duplicateProperty.GetDefaultValue(storeObject) + && property.GetDefaultValueSql(storeObject) == duplicateProperty.GetDefaultValueSql(storeObject); + + private static string? TryUniquifyDefaultConstraint( + IConventionProperty property, + string constraintName, + string? schema, + Dictionary<(string, string?), (IConventionProperty, StoreObjectIdentifier)> defaultConstraints, + in StoreObjectIdentifier storeObject, + int maxLength) + { + var mappedTables = property.GetMappedStoreObjects(StoreObjectType.Table); + if (mappedTables.Count() > 1) + { + // For TPC and some entity splitting scenarios we end up with multiple tables having to define the constraint. + // Since constraint name has to be unique, we can't keep the same name for all + // Disabling this scenario until we have better way to configure the constraint name + // see issue #27970 + if (property.GetDefaultConstraintNameConfigurationSource() == null) + { + throw new InvalidOperationException( + RelationalStrings.ImplicitDefaultNamesNotSupportedForTpcWhenNamesClash(constraintName)); + } + else + { + throw new InvalidOperationException( + RelationalStrings.ExplicitDefaultConstraintNamesNotSupportedForTpc(constraintName)); + } + } + + if (property.Builder.CanSetAnnotation(RelationalAnnotationNames.DefaultConstraintName, null)) + { + constraintName = Uniquifier.Uniquify(constraintName, defaultConstraints, n => (n, schema), maxLength); + property.Builder.HasAnnotation(RelationalAnnotationNames.DefaultConstraintName, constraintName); + return constraintName; + } + + return null; + } + private void UniquifyTriggerNames( IConventionEntityType entityType, Dictionary triggers, diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 7ce2c2e1bb2..eb29b0c3b03 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -218,7 +218,6 @@ public override IReadOnlyList GenerateFluentApiCalls( else { var defaultValueSqlAnnotationExists = annotations.TryGetValue(RelationalAnnotationNames.DefaultValueSql, out defaultValueSqlAnnotation); - Check.DebugAssert(defaultValueSqlAnnotationExists, "If default constaint name was set, one of DefaultValue or DefaultValueSql must also be set."); annotations.Remove(RelationalAnnotationNames.DefaultValueSql); } } @@ -240,7 +239,7 @@ public override IReadOnlyList GenerateFluentApiCalls( } else { - Check.NotNull(defaultValueSqlAnnotation, "Both DefaultValue and DefaultValueSql annotations are null."); + Check.DebugAssert(defaultValueSqlAnnotation != null, $"Default constraint name was set for {property.Name}, but DefaultValue and DefaultValueSql are both null."); fragments.Add( new MethodCallCodeFragment( nameof(SqlServerPropertyBuilderExtensions.HasDefaultValueSql), diff --git a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs index 07675cb2d59..03efbf87538 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePrimitiveCollectionBuilderExtensions.cs @@ -53,4 +53,86 @@ public static ComplexTypePrimitiveCollectionBuilder IsSparse (ComplexTypePrimitiveCollectionBuilder)IsSparse( (ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, sparse); + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value, + string defaultConstraintName) + { + primitiveCollectionBuilder.Metadata.SetDefaultValue(value); + primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValue( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value, + string defaultConstraintName) + => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValue( + (ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, value, defaultConstraintName); + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + string defaultConstraintName) + { + Check.NullButNotEmpty(sql, nameof(sql)); + + primitiveCollectionBuilder.Metadata.SetDefaultValueSql(sql); + primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePrimitiveCollectionBuilder HasDefaultValueSql( + this ComplexTypePrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + string defaultConstraintName) + => (ComplexTypePrimitiveCollectionBuilder)HasDefaultValueSql( + (ComplexTypePrimitiveCollectionBuilder)primitiveCollectionBuilder, sql, defaultConstraintName); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs index 2893f858f05..6aaa54826e4 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerComplexTypePropertyBuilderExtensions.cs @@ -256,4 +256,84 @@ public static ComplexTypePropertyBuilder IsSparse( this ComplexTypePropertyBuilder propertyBuilder, bool sparse = true) => (ComplexTypePropertyBuilder)IsSparse((ComplexTypePropertyBuilder)propertyBuilder, sparse); + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePropertyBuilder HasDefaultValue( + this ComplexTypePropertyBuilder propertyBuilder, + object? value, + string defaultConstraintName) + { + propertyBuilder.Metadata.SetDefaultValue(value); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return propertyBuilder; + } + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePropertyBuilder HasDefaultValue( + this ComplexTypePropertyBuilder propertyBuilder, + object? value, + string defaultConstraintName) + => (ComplexTypePropertyBuilder)HasDefaultValue((ComplexTypePropertyBuilder)propertyBuilder, value, defaultConstraintName); + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePropertyBuilder HasDefaultValueSql( + this ComplexTypePropertyBuilder propertyBuilder, + string? sql, + string defaultConstraintName) + { + Check.NullButNotEmpty(sql, nameof(sql)); + + propertyBuilder.Metadata.SetDefaultValueSql(sql); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return propertyBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static ComplexTypePropertyBuilder HasDefaultValueSql( + this ComplexTypePropertyBuilder propertyBuilder, + string? sql, + string defaultConstraintName) + => (ComplexTypePropertyBuilder)HasDefaultValueSql((ComplexTypePropertyBuilder)propertyBuilder, sql, defaultConstraintName); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs index 87fa60c5132..cfeeadf2309 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPrimitiveCollectionBuilderExtensions.cs @@ -50,4 +50,84 @@ public static PrimitiveCollectionBuilder IsSparse( this PrimitiveCollectionBuilder primitiveCollectionBuilder, bool sparse = true) => (PrimitiveCollectionBuilder)IsSparse((PrimitiveCollectionBuilder)primitiveCollectionBuilder, sparse); + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValue( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value, + string defaultConstraintName) + { + primitiveCollectionBuilder.Metadata.SetDefaultValue(value); + primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value for the column that the property maps + /// to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValue( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + object? value, + string defaultConstraintName) + => (PrimitiveCollectionBuilder)HasDefaultValue((PrimitiveCollectionBuilder)primitiveCollectionBuilder, value, defaultConstraintName); + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValueSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + string defaultConstraintName) + { + Check.NullButNotEmpty(sql, nameof(sql)); + + primitiveCollectionBuilder.Metadata.SetDefaultValueSql(sql); + primitiveCollectionBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName); + + return primitiveCollectionBuilder; + } + + /// + /// Configures the default value expression for the column that the property maps to when targeting a relational database. + /// + /// + /// See Database default values for more information and examples. + /// + /// The type of the property being configured. + /// The builder for the property being configured. + /// The SQL expression for the default value of the column. + /// The default constraint name. + /// The same builder instance so that multiple calls can be chained. + public static PrimitiveCollectionBuilder HasDefaultValueSql( + this PrimitiveCollectionBuilder primitiveCollectionBuilder, + string? sql, + string defaultConstraintName) + => (PrimitiveCollectionBuilder)HasDefaultValueSql((PrimitiveCollectionBuilder)primitiveCollectionBuilder, sql, defaultConstraintName); } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs index 9a7a17c1736..478966c319a 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs @@ -819,9 +819,7 @@ public static bool CanSetIsSparse( /// /// See Modeling entity types and relationships, and /// Accessing SQL Server and Azure SQL databases with EF Core - /// for more information and examples. Also see - /// Sparse columns for - /// general information on SQL Server sparse columns. + /// for more information and examples. /// /// The builder for the property being configured. /// The default value of the column. @@ -846,9 +844,7 @@ public static PropertyBuilder HasDefaultValue( /// /// See Modeling entity types and relationships, and /// Accessing SQL Server and Azure SQL databases with EF Core - /// for more information and examples. Also see - /// Sparse columns for - /// general information on SQL Server sparse columns. + /// for more information and examples. /// /// The builder for the property being configured. /// The default value of the column. @@ -866,9 +862,7 @@ public static PropertyBuilder HasDefaultValue( /// /// See Modeling entity types and relationships, and /// Accessing SQL Server and Azure SQL databases with EF Core - /// for more information and examples. Also see - /// Sparse columns for - /// general information on SQL Server sparse columns. + /// for more information and examples. /// /// The builder for the property being configured. /// The default value of the column. @@ -890,6 +884,7 @@ public static PropertyBuilder HasDefaultValue( } propertyBuilder.Metadata.SetDefaultValue(value, fromDataAnnotation); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName, fromDataAnnotation); return propertyBuilder; } @@ -899,9 +894,7 @@ public static PropertyBuilder HasDefaultValue( /// /// See Modeling entity types and relationships, and /// Accessing SQL Server and Azure SQL databases with EF Core - /// for more information and examples. Also see - /// Sparse columns for - /// general information on SQL Server sparse columns. + /// for more information and examples. /// /// The builder for the property being configured. /// The default value of the column. @@ -928,9 +921,7 @@ public static bool CanSetDefaultValue( /// /// See Modeling entity types and relationships, and /// Accessing SQL Server and Azure SQL databases with EF Core - /// for more information and examples. Also see - /// Sparse columns for - /// general information on SQL Server sparse columns. + /// for more information and examples. /// /// The builder for the property being configured. /// The SQL expression for the default value of the column. @@ -955,9 +946,7 @@ public static PropertyBuilder HasDefaultValueSql( /// /// See Modeling entity types and relationships, and /// Accessing SQL Server and Azure SQL databases with EF Core - /// for more information and examples. Also see - /// Sparse columns for - /// general information on SQL Server sparse columns. + /// for more information and examples. /// /// The type of the property being configured. /// The builder for the property being configured. @@ -976,9 +965,7 @@ public static PropertyBuilder HasDefaultValueSql( /// /// See Modeling entity types and relationships, and /// Accessing SQL Server and Azure SQL databases with EF Core - /// for more information and examples. Also see - /// Sparse columns for - /// general information on SQL Server sparse columns. + /// for more information and examples. /// /// The builder for the property being configured. /// The SQL expression for the default value of the column. @@ -1000,6 +987,7 @@ public static PropertyBuilder HasDefaultValueSql( } propertyBuilder.Metadata.SetDefaultValueSql(sql, fromDataAnnotation); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName, fromDataAnnotation); return propertyBuilder; } diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs index d40b142c7e2..59b753466aa 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerSharedTableConvention.cs @@ -32,6 +32,10 @@ public SqlServerSharedTableConvention( protected override bool IndexesUniqueAcrossTables => false; + /// + protected override bool DefaultConstraintsUniqueAcrossTables + => true; + /// protected override bool AreCompatible(IReadOnlyKey key, IReadOnlyKey duplicateKey, in StoreObjectIdentifier storeObject) => base.AreCompatible(key, duplicateKey, storeObject) diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index 66478a046d8..24fbda9033a 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -261,7 +261,7 @@ public override IEnumerable For(IColumn column, bool designTime) var mappedProperty = column.PropertyMappings.FirstOrDefault()?.Property; if (mappedProperty != null) { - if (mappedProperty.GetDefaultConstraintName() is string defaultConstraintName) + if (mappedProperty.GetDefaultConstraintName(table) is string defaultConstraintName) { // named constraint stored as annotation are either explicitly configured by user // or generated by EF because of naming duplicates (SqlServerDefaultValueConvention) diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs index 12ac5b58c1c..e3b1b580a15 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.NamedDefaultConstraints.cs @@ -964,15 +964,15 @@ await Test( AssertSql( """ -ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_AnotherProp] uniqueidentifier NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLooooooooooooo] DEFAULT (NEWID()); +ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_AnotherProp] uniqueidentifier NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnn~] DEFAULT (NEWID()); """, // """ -ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_Prop] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLooooooooooooo1] DEFAULT 7; +ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_Prop] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnn~1] DEFAULT 7; """, // """ -ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_YetAnotherProp] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLooooooooooooo2] DEFAULT 27; +ALTER TABLE [VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity] ADD [YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnnnnnnnnnnnnnnnnggggggggggggggggggggOwnedNavigation_YetAnotherProp] int NULL CONSTRAINT [DF_VeryVeryVeryVeryVeryVeryVeryVeryLoooooooooooooooooooooooooooooooonEntity_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnn~2] DEFAULT 27; """); }