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..b23372623b8 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] ?? false; + + /// + /// 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..6c703b725bd 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -2086,4 +2086,82 @@ 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] + ?? (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] + ?? (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 for. + public static string GenerateDefaultConstraintName(this IReadOnlyProperty property, in StoreObjectIdentifier storeObject) + { + var candidate = $"DF_{storeObject.Name}_{property.GetColumnName(storeObject)}"; + + return Uniquifier.Truncate(candidate, property.DeclaringType.Model.GetMaxIdentifierLength()); + } + + /// + /// 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/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.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..eb29b0c3b03 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -201,8 +201,53 @@ 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); + 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.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), + 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/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/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/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 2f92f859692..478966c319a 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerPropertyBuilderExtensions.cs @@ -812,4 +812,207 @@ 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. + /// + /// 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. + /// + /// 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. + /// + /// 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); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName, 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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); + propertyBuilder.Metadata.SetDefaultConstraintName(defaultConstraintName, 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/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 a6fecde4a4f..24fbda9033a 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(table) 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..e3b1b580a15 --- /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_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnnn~] DEFAULT (NEWID()); +""", + // + """ +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_YetAnotherVeryVeryVeryVeryVeryLoooooooooooooonnnnn~2] 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() {