diff --git a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs index 067fb5d9fa4..8d8eec50bee 100644 --- a/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs +++ b/src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs @@ -1,4024 +1,4026 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.Globalization; -using System.Text; -using Microsoft.EntityFrameworkCore.SqlServer.Internal; -using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; -using Microsoft.EntityFrameworkCore.SqlServer.Update.Internal; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore.Migrations; - -/// -/// SQL Server-specific implementation of . -/// -/// -/// -/// The service lifetime is . This means that each -/// instance will use its own instance of this service. -/// The implementation may depend on other services registered with any lifetime. -/// The implementation does not need to be thread-safe. -/// -/// -/// See Database migrations, and -/// Accessing SQL Server and Azure SQL databases with EF Core -/// for more information and examples. -/// -/// -public class SqlServerMigrationsSqlGenerator : MigrationsSqlGenerator -{ - private IReadOnlyList _operations = null!; - private int _variableCounter = -1; - - private readonly ICommandBatchPreparer _commandBatchPreparer; - - /// - /// Creates a new instance. - /// - /// Parameter object containing dependencies for this service. - /// The command batch preparer. - public SqlServerMigrationsSqlGenerator( - MigrationsSqlGeneratorDependencies dependencies, - ICommandBatchPreparer commandBatchPreparer) - : base(dependencies) - => _commandBatchPreparer = commandBatchPreparer; - - /// - /// Generates commands from a list of operations. - /// - /// The operations. - /// The target model which may be if the operations exist without a model. - /// The options to use when generating commands. - /// The list of commands to be executed or scripted. - public override IReadOnlyList Generate( - IReadOnlyList operations, - IModel? model = null, - MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) - { - _operations = operations; - try - { - return base.Generate(RewriteOperations(operations, model, options), model, options); - } - finally - { - _operations = null!; - } - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// - /// This method uses a double-dispatch mechanism to call the method - /// that is specific to a certain subtype of . Typically database providers - /// will override these specific methods rather than this method. However, providers can override - /// this methods to handle provider-specific operations. - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - switch (operation) - { - case SqlServerCreateDatabaseOperation createDatabaseOperation: - Generate(createDatabaseOperation, model, builder); - break; - case SqlServerDropDatabaseOperation dropDatabaseOperation: - Generate(dropDatabaseOperation, model, builder); - break; - default: - base.Generate(operation, model, builder); - break; - } - } - - /// - protected override void Generate(AddCheckConstraintOperation operation, IModel? model, MigrationCommandListBuilder builder) - => GenerateExecWhenIdempotent(builder, b => base.Generate(operation, model, b)); - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - AddColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate) - { - if (!terminate - && operation.Comment != null) - { - throw new ArgumentException(SqlServerStrings.CannotProduceUnterminatedSQLWithComments(nameof(AddColumnOperation))); - } - - if (IsIdentity(operation)) - { - // NB: This gets added to all added non-nullable columns by MigrationsModelDiffer. We need to suppress - // it, here because SQL Server can't have both IDENTITY and a DEFAULT constraint on the same column. - operation.DefaultValue = null; - } - - var needsExec = Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) - && operation.ComputedColumnSql != null; - if (needsExec) - { - var subBuilder = new MigrationCommandListBuilder(Dependencies); - base.Generate(operation, model, subBuilder, terminate: false); - subBuilder.EndCommand(); - - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - var command = subBuilder.GetCommandList().Single(); - - builder - .Append("EXEC(") - .Append(stringTypeMapping.GenerateSqlLiteral(command.CommandText)) - .Append(")"); - } - else - { - base.Generate(operation, model, builder, terminate: false); - } - - if (terminate) - { - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - if (operation.Comment != null) - { - AddDescription( - builder, operation.Comment, - operation.Schema, - operation.Table, - operation.Name); - } - - builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - AddForeignKeyOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - base.Generate(operation, model, builder, terminate: false); - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - AddPrimaryKeyOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - base.Generate(operation, model, builder, terminate: false); - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate( - AlterColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - if (operation[RelationalAnnotationNames.ColumnOrder] != operation.OldColumn[RelationalAnnotationNames.ColumnOrder]) - { - Dependencies.MigrationsLogger.ColumnOrderIgnoredWarning(operation); - } - - IEnumerable? indexesToRebuild = null; - var column = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) - ?.Columns.FirstOrDefault(c => c.Name == operation.Name); - - if (operation.ComputedColumnSql != operation.OldColumn.ComputedColumnSql - || operation.IsStored != operation.OldColumn.IsStored) - { - var dropColumnOperation = new DropColumnOperation - { - Schema = operation.Schema, - Table = operation.Table, - Name = operation.Name - }; - if (column != null) - { - dropColumnOperation.AddAnnotations(column.GetAnnotations()); - } - - var addColumnOperation = new AddColumnOperation - { - Schema = operation.Schema, - Table = operation.Table, - Name = operation.Name, - ClrType = operation.ClrType, - ColumnType = operation.ColumnType, - IsUnicode = operation.IsUnicode, - IsFixedLength = operation.IsFixedLength, - MaxLength = operation.MaxLength, - Precision = operation.Precision, - Scale = operation.Scale, - IsRowVersion = operation.IsRowVersion, - IsNullable = operation.IsNullable, - DefaultValue = operation.DefaultValue, - DefaultValueSql = operation.DefaultValueSql, - ComputedColumnSql = operation.ComputedColumnSql, - IsStored = operation.IsStored, - Comment = operation.Comment, - Collation = operation.Collation - }; - addColumnOperation.AddAnnotations(operation.GetAnnotations()); - - // TODO: Use a column rebuild instead - indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); - DropIndexes(indexesToRebuild, builder); - Generate(dropColumnOperation, model, builder, terminate: false); - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - Generate(addColumnOperation, model, builder); - CreateIndexes(indexesToRebuild, builder); - builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - - return; - } - - var columnType = operation.ColumnType - ?? GetColumnType( - operation.Schema, - operation.Table, - operation.Name, - operation, - model); - - var narrowed = false; - var oldColumnSupported = IsOldColumnSupported(model); - string? oldType = null; - - // SQL Server can't ALTER COLUMN on a computed column when the expression is unchanged; see #33425. - var computedColumnIsNoOp = operation.ComputedColumnSql != null - && operation.OldColumn.ComputedColumnSql != null - && operation.ComputedColumnSql == operation.OldColumn.ComputedColumnSql - && operation.IsStored == operation.OldColumn.IsStored; - - if (oldColumnSupported && !computedColumnIsNoOp) - { - if (IsIdentity(operation) != IsIdentity(operation.OldColumn)) - { - throw new InvalidOperationException(SqlServerStrings.AlterIdentityColumn); - } - - oldType = operation.OldColumn.ColumnType - ?? GetColumnType( - operation.Schema, - operation.Table, - operation.Name, - operation.OldColumn, - model); - narrowed = columnType != oldType - || operation.Collation != operation.OldColumn.Collation - || operation is { IsNullable: false, OldColumn.IsNullable: true }; - } - - if (narrowed) - { - indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); - DropIndexes(indexesToRebuild, builder); - } - - // Handle change of identity seed value - if (IsIdentity(operation) && oldColumnSupported) - { - Check.DebugAssert(IsIdentity(operation.OldColumn), "Unsupported column change to identity"); - - var oldSeed = 1; - if (TryParseIdentitySeedIncrement(operation, out var newSeed, out _) - && (operation.OldColumn[SqlServerAnnotationNames.Identity] is null - || TryParseIdentitySeedIncrement(operation.OldColumn, out oldSeed, out _)) - && newSeed != oldSeed) - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - var table = stringTypeMapping.GenerateSqlLiteral( - Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)); - - builder - .Append($"DBCC CHECKIDENT({table}, RESEED, {newSeed})") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - } - - var newAnnotations = operation.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.Identity); - var oldAnnotations = operation.OldColumn.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.Identity); - - var alterStatementNeeded = narrowed - || !oldColumnSupported - || operation.ClrType != operation.OldColumn.ClrType - || columnType != operation.OldColumn.ColumnType - || operation.IsUnicode != operation.OldColumn.IsUnicode - || operation.IsFixedLength != operation.OldColumn.IsFixedLength - || operation.MaxLength != operation.OldColumn.MaxLength - || operation.Precision != operation.OldColumn.Precision - || operation.Scale != operation.OldColumn.Scale - || operation.IsRowVersion != operation.OldColumn.IsRowVersion - || operation.IsNullable != operation.OldColumn.IsNullable - || operation.Collation != operation.OldColumn.Collation - || HasDifferences(newAnnotations, oldAnnotations); - - if (computedColumnIsNoOp) - { - alterStatementNeeded = false; - } - - var (oldDefaultValue, oldDefaultValueSql) = (operation.OldColumn.DefaultValue, operation.OldColumn.DefaultValueSql); - - if (alterStatementNeeded - || !Equals(operation.DefaultValue, oldDefaultValue) - || operation.DefaultValueSql != oldDefaultValueSql) - { - var oldDefaultConstraintName = operation.OldColumn[RelationalAnnotationNames.DefaultConstraintName] as string; - - DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, oldDefaultConstraintName, builder); - (oldDefaultValue, oldDefaultValueSql) = (null, null); - } - - // The column is being made non-nullable. Generate an update statement before doing that, to convert any existing null values to - // the default value (otherwise SQL Server fails). - if (operation is { IsNullable: false, OldColumn.IsNullable: true } - && (operation.DefaultValueSql is not null || operation.DefaultValue is not null)) - { - string defaultValueSql; - if (operation.DefaultValueSql is not null) - { - defaultValueSql = operation.DefaultValueSql; - } - else - { - Check.DebugAssert(operation.DefaultValue is not null); - - var typeMapping = Dependencies.TypeMappingSource.FindMapping(operation.DefaultValue.GetType(), columnType) - ?? Dependencies.TypeMappingSource.GetMappingForValue(operation.DefaultValue); - - defaultValueSql = typeMapping.GenerateSqlLiteral(operation.DefaultValue); - } - - var updateBuilder = new StringBuilder() - .Append("UPDATE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" SET ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" = ") - .Append(defaultValueSql) - .Append(" WHERE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" IS NULL"); - - if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) - { - builder - .Append("EXEC(N'") - .Append(updateBuilder.ToString().TrimEnd('\n', '\r', ';').Replace("'", "''")) - .Append("')"); - } - else - { - builder.Append(updateBuilder.ToString()); - } - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - if (alterStatementNeeded) - { - // SQL Server can't ALTER COLUMN from json to a non JSON type; use rename-add-copy-drop instead. See #38364. - if ((oldType ?? operation.OldColumn.ColumnType) - ?.Equals("json", StringComparison.OrdinalIgnoreCase) - == true - && !columnType.Equals("json", StringComparison.OrdinalIgnoreCase)) - { - AlterColumnFromJson(operation, columnType, model, builder); - } - else - { - AppendAlterColumnDefinition(operation, operation.IsNullable, model, builder); - } - } - - 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, defaultConstraintName, builder); - builder - .Append(" FOR ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - if (operation.OldColumn.Comment != operation.Comment) - { - var dropDescription = operation.OldColumn.Comment != null; - if (dropDescription) - { - DropDescription( - builder, - operation.Schema, - operation.Table, - operation.Name); - } - - if (operation.Comment != null) - { - AddDescription( - builder, operation.Comment, - operation.Schema, - operation.Table, - operation.Name, - omitVariableDeclarations: dropDescription); - } - } - - if (narrowed) - { - CreateIndexes(indexesToRebuild!, builder); - } - - builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - - private void AlterColumnFromJson( - AlterColumnOperation operation, - string columnType, - IModel? model, - MigrationCommandListBuilder builder) - { - var tempColumnName = "ef_temp_" + operation.Name; - - Rename( - Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema) - + "." - + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name), - tempColumnName, - "COLUMN", - builder); - - builder - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" ADD "); - - var addColumnOperation = new AddColumnOperation - { - Schema = operation.Schema, - Table = operation.Table, - Name = operation.Name, - ClrType = operation.ClrType, - ColumnType = operation.ColumnType, - IsUnicode = operation.IsUnicode, - IsFixedLength = operation.IsFixedLength, - MaxLength = operation.MaxLength, - Precision = operation.Precision, - Scale = operation.Scale, - IsRowVersion = operation.IsRowVersion, - IsNullable = true, - Collation = operation.Collation, - Comment = operation.Comment - }; - addColumnOperation.AddAnnotations( - operation.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.Identity)); - - ColumnDefinition( - operation.Schema, - operation.Table, - operation.Name, - addColumnOperation, - model, - builder); - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - var updateSql = new StringBuilder() - .Append("UPDATE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" SET ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" = CONVERT(") - .Append(columnType) - .Append(", ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tempColumnName)) - .Append(")") - .ToString(); - - builder - .Append("EXEC(N'") - .Append(updateSql.Replace("'", "''")) - .Append("')"); - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - builder - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" DROP COLUMN ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tempColumnName)) - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - if (!operation.IsNullable) - { - AppendAlterColumnDefinition(operation, false, model, builder); - } - } - - private void AppendAlterColumnDefinition( - AlterColumnOperation operation, - bool isNullable, - IModel? model, - MigrationCommandListBuilder builder) - { - builder - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" ALTER COLUMN "); - - // NB: ComputedColumnSql, IsStored, DefaultValue, DefaultValueSql, Comment, ValueGenerationStrategy, and Identity are - // handled elsewhere. Don't copy them here. - var definitionOperation = new AlterColumnOperation - { - Schema = operation.Schema, - Table = operation.Table, - Name = operation.Name, - ClrType = operation.ClrType, - ColumnType = operation.ColumnType, - IsUnicode = operation.IsUnicode, - IsFixedLength = operation.IsFixedLength, - MaxLength = operation.MaxLength, - Precision = operation.Precision, - Scale = operation.Scale, - IsRowVersion = operation.IsRowVersion, - IsNullable = isNullable, - Collation = operation.Collation, - OldColumn = operation.OldColumn - }; - definitionOperation.AddAnnotations( - operation.GetAnnotations().Where(a => a.Name is not SqlServerAnnotationNames.ValueGenerationStrategy - and not SqlServerAnnotationNames.Identity)); - - ColumnDefinition( - operation.Schema, - operation.Table, - operation.Name, - definitionOperation, - model, - builder); - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate( - RenameIndexOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - if (string.IsNullOrEmpty(operation.Table)) - { - throw new InvalidOperationException(SqlServerStrings.IndexTableRequired); - } - - Rename( - Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema) - + "." - + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name), - operation.NewName, - "INDEX", - builder); - builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate(RenameSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - var name = operation.Name; - if (operation.NewName != null - && operation.NewName != name) - { - Rename( - Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema), - operation.NewName, - "OBJECT", - builder); - - name = operation.NewName; - } - - if (operation.NewSchema != operation.Schema - && (operation.NewSchema != null - || !HasLegacyRenameOperations(model))) - { - Transfer(operation.NewSchema, operation.Schema, name, builder); - } - - builder.EndCommand(); - } - - /// - /// Builds commands for the given by making calls on the given - /// , and then terminates the final command. - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate( - RestartSequenceOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - builder - .Append("ALTER SEQUENCE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) - .Append(" RESTART"); - - if (operation.StartValue.HasValue) - { - builder - .Append(" WITH ") - .Append(IntegerConstant(operation.StartValue.Value)); - } - - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - EndStatement(builder); - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - CreateTableOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - var hasComments = operation.Comment != null || operation.Columns.Any(c => c.Comment != null); - - if (!terminate && hasComments) - { - throw new ArgumentException(SqlServerStrings.CannotProduceUnterminatedSQLWithComments(nameof(CreateTableOperation))); - } - - var needsExec = false; - - var tableCreationOptions = new List(); - - if ((operation[SqlServerAnnotationNames.IsTemporal] as bool?) == true) - { - var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string - ?? model?.GetDefaultSchema(); - - needsExec = historyTableSchema == null; - var subBuilder = needsExec - ? new MigrationCommandListBuilder(Dependencies) - : builder; - - subBuilder - .Append("CREATE TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) - .AppendLine(" ("); - - using (subBuilder.Indent()) - { - CreateTableColumns(operation, model, subBuilder); - CreateTableConstraints(operation, model, subBuilder); - subBuilder.AppendLine(","); - var startColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; - var endColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; - var start = Dependencies.SqlGenerationHelper.DelimitIdentifier(startColumnName!); - var end = Dependencies.SqlGenerationHelper.DelimitIdentifier(endColumnName!); - subBuilder.AppendLine($"PERIOD FOR SYSTEM_TIME({start}, {end})"); - } - - subBuilder.Append(")"); - - var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; - string historyTable; - if (needsExec) - { - subBuilder - .EndCommand(); - - var execBody = subBuilder.GetCommandList().Single().CommandText.Replace("'", "''"); - - var schemaVariable = Uniquify("@historyTableSchema"); - builder - .AppendLine($"DECLARE {schemaVariable} nvarchar(max) = QUOTENAME(SCHEMA_NAME())") - .Append("EXEC(N'") - .Append(execBody); - - historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!); - tableCreationOptions.Add($"SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + {schemaVariable} + N'.{historyTable})"); - } - else - { - historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!, historyTableSchema); - tableCreationOptions.Add($"SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable})"); - } - } - else - { - base.Generate(operation, model, builder, terminate: false); - } - - var memoryOptimized = IsMemoryOptimized(operation); - if (memoryOptimized) - { - tableCreationOptions.Add("MEMORY_OPTIMIZED = ON"); - } - - if (tableCreationOptions.Count > 0) - { - builder.Append(" WITH ("); - if (tableCreationOptions.Count == 1) - { - builder - .Append(tableCreationOptions[0]) - .Append(")"); - } - else - { - builder.AppendLine(); - - using (builder.Indent()) - { - for (var i = 0; i < tableCreationOptions.Count; i++) - { - builder.Append(tableCreationOptions[i]); - - if (i < tableCreationOptions.Count - 1) - { - builder.Append(","); - } - - builder.AppendLine(); - } - } - - builder.Append(")"); - } - } - - if (needsExec) - { - builder.Append("')"); - } - - if (hasComments) - { - Check.DebugAssert(terminate, "terminate is false but there are comments"); - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - var firstDescription = true; - if (operation.Comment != null) - { - AddDescription(builder, operation.Comment, operation.Schema, operation.Name); - - firstDescription = false; - } - - foreach (var column in operation.Columns) - { - if (column.Comment == null) - { - continue; - } - - AddDescription( - builder, column.Comment, - operation.Schema, - operation.Name, - column.Name, - omitVariableDeclarations: !firstDescription); - - firstDescription = false; - } - - builder.EndCommand(suppressTransaction: memoryOptimized); - } - else if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: memoryOptimized); - } - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate( - RenameTableOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - var name = operation.Name; - if (operation.NewName != null - && operation.NewName != name) - { - Rename( - Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema), - operation.NewName, - "OBJECT", - builder); - - name = operation.NewName; - } - - if (operation.NewSchema != operation.Schema - && (operation.NewSchema != null - || !HasLegacyRenameOperations(model))) - { - Transfer(operation.NewSchema, operation.Schema, name, builder); - } - - builder.EndCommand(); - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - DropTableOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - base.Generate(operation, model, builder, terminate: false); - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Name)); - } - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - CreateIndexOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - if (operation[SqlServerAnnotationNames.FullTextIndex] is string keyIndex) - { - GenerateFullTextIndex(keyIndex); - return; - } - - if (operation[SqlServerAnnotationNames.VectorIndexMetric] is string) - { - GenerateVectorIndex(); - return; - } - - if (operation[RelationalAnnotationNames.JsonIndex] is RelationalJsonIndex jsonIndex) - { - GenerateJsonIndex(jsonIndex); - return; - } - - var table = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema); - var hasNullableColumns = operation.Columns.Any(c => table?.FindColumn(c)?.IsNullable != false); - - var memoryOptimized = IsMemoryOptimized(operation, model, operation.Schema, operation.Table); - if (memoryOptimized) - { - builder.Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append(" ADD INDEX ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" "); - - if (operation.IsUnique && !hasNullableColumns) - { - builder.Append("UNIQUE "); - } - - IndexTraits(operation, model, builder); - - builder.Append("("); - GenerateIndexColumnList(operation, model, builder); - builder.Append(")"); - } - else - { - var needsLegacyFilter = UseLegacyIndexFilters(operation, model); - var needsExec = Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) - && (operation.Filter != null - || needsLegacyFilter); - var subBuilder = needsExec - ? new MigrationCommandListBuilder(Dependencies) - : builder; - - base.Generate(operation, model, subBuilder, terminate: false); - - if (needsExec) - { - subBuilder - .EndCommand(); - - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - var command = subBuilder.GetCommandList().Single(); - - builder - .Append("EXEC(") - .Append(stringTypeMapping.GenerateSqlLiteral(command.CommandText)) - .Append(")"); - } - } - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: memoryOptimized); - } - - void GenerateFullTextIndex(string keyIndex) - { - builder.Append("CREATE FULLTEXT INDEX ON ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append("("); - - var languages = (Dictionary?)operation.FindAnnotation(SqlServerAnnotationNames.FullTextLanguages)?.Value; - - for (var i = 0; i < operation.Columns.Length; i++) - { - if (i > 0) - { - builder.Append(", "); - } - - builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Columns[i])); - - if (languages is not null && languages.TryGetValue(operation.Columns[i], out var language)) - { - builder.Append(" LANGUAGE ").Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(language)); - } - } - - builder.Append(") KEY INDEX ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(keyIndex)); - - if (operation[SqlServerAnnotationNames.FullTextCatalog] is string catalog) - { - builder.Append(" ON ").Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(catalog)); - } - - if (operation[SqlServerAnnotationNames.FullTextChangeTracking] is FullTextChangeTracking changeTracking) - { - builder.Append(" WITH CHANGE_TRACKING = "); - builder.Append( - changeTracking switch - { - FullTextChangeTracking.Auto => "AUTO", - FullTextChangeTracking.Manual => "MANUAL", - FullTextChangeTracking.Off => "OFF", - FullTextChangeTracking.OffNoPopulation => "OFF, NO POPULATION", - - _ => throw new UnreachableException(), - }); - } - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true); - } - } - - void GenerateVectorIndex() - { - builder.Append("CREATE VECTOR INDEX ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" ON ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append("("); - GenerateIndexColumnList(operation, model, builder); - builder.Append(")"); - - IndexOptions(operation, model, builder); - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true); - } - } - - void GenerateJsonIndex(RelationalJsonIndex jsonIndex) - { - var jsonColumn = jsonIndex.Elements[0].ContainingColumn.Name; - builder.Append("CREATE JSON INDEX ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" ON ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) - .Append("(") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(jsonColumn)) - .Append(") FOR ("); - - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - for (var i = 0; i < jsonIndex.Elements.Count; i++) - { - if (i > 0) - { - builder.Append(", "); - } - - var element = jsonIndex.Elements[i]; - // Add a trailing wildcard for the leaf JSON array - var segments = element is IRelationalJsonArray - ? [.. element.Path, StructuredJsonPathSegment.Array] - : element.Path; - builder.Append( - stringTypeMapping.GenerateSqlLiteral( - new StructuredJsonPath(segments, jsonIndex.CollectionIndices?[i]) - .ToString(wildcardForNullIndex: '*'))); - } - - builder.Append(")"); - - IndexOptions(operation, model, builder); - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true); - } - } - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - DropPrimaryKeyOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - base.Generate(operation, model, builder, terminate: false); - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate(EnsureSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - if (string.Equals(operation.Name, "dbo", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - - builder - .Append("IF SCHEMA_ID(") - .Append(stringTypeMapping.GenerateSqlLiteral(operation.Name)) - .Append(") IS NULL EXEC(") - .Append( - stringTypeMapping.GenerateSqlLiteral( - "CREATE SCHEMA " - + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name) - + Dependencies.SqlGenerationHelper.StatementTerminator)) - .Append(")") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(); - } - - /// - /// Builds commands for the given by making calls on the given - /// , and then terminates the final command. - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate( - CreateSequenceOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - builder - .Append("CREATE SEQUENCE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)); - - if (operation.ClrType != typeof(long)) - { - var typeMapping = Dependencies.TypeMappingSource.GetMapping(operation.ClrType); - - builder - .Append(" AS ") - .Append(typeMapping.StoreTypeNameBase); - } - - builder - .Append(" START WITH ") - .Append(IntegerConstant(operation.StartValue)); - - SequenceOptions(operation, model, builder); - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - EndStatement(builder); - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected virtual void Generate( - SqlServerCreateDatabaseOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - builder - .Append("CREATE DATABASE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)); - - if (!string.IsNullOrEmpty(operation.FileName)) - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - - var fileName = ExpandFileName(operation.FileName); - var name = Path.GetFileNameWithoutExtension(fileName); - - var logFileName = Path.ChangeExtension(fileName, ".ldf"); - var logName = name + "_log"; - - // Match default naming behavior of SQL Server - logFileName = logFileName.Insert(logFileName.Length - ".ldf".Length, "_log"); - - builder - .AppendLine() - .Append("ON (NAME = ") - .Append(stringTypeMapping.GenerateSqlLiteral(name)) - .Append(", FILENAME = ") - .Append(stringTypeMapping.GenerateSqlLiteral(fileName)) - .Append(")") - .AppendLine() - .Append("LOG ON (NAME = ") - .Append(stringTypeMapping.GenerateSqlLiteral(logName)) - .Append(", FILENAME = ") - .Append(stringTypeMapping.GenerateSqlLiteral(logFileName)) - .Append(")"); - } - - if (!string.IsNullOrEmpty(operation.Collation)) - { - builder - .AppendLine() - .Append("COLLATE ") - .Append(operation.Collation); - } - - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true) - .AppendLine("IF SERVERPROPERTY('EngineEdition') <> 5") - .AppendLine("BEGIN"); - - using (builder.Indent()) - { - builder - .Append("ALTER DATABASE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" SET READ_COMMITTED_SNAPSHOT ON") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - builder - .Append("END") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true); - } - - private static string ExpandFileName(string fileName) - { - if (fileName.StartsWith("|DataDirectory|", StringComparison.OrdinalIgnoreCase)) - { - var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory") as string; - if (string.IsNullOrEmpty(dataDirectory)) - { - dataDirectory = AppDomain.CurrentDomain.BaseDirectory; - } - - fileName = Path.Combine(dataDirectory, fileName["|DataDirectory|".Length..]); - } - - return Path.GetFullPath(fileName); - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected virtual void Generate( - SqlServerDropDatabaseOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - builder - .AppendLine("IF SERVERPROPERTY('EngineEdition') <> 5") - .AppendLine("BEGIN"); - - using (builder.Indent()) - { - builder - .Append("ALTER DATABASE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" SET SINGLE_USER WITH ROLLBACK IMMEDIATE") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - builder - .Append("END") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true) - .Append("DROP DATABASE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true); - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate( - AlterDatabaseOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - if (operation[SqlServerAnnotationNames.EditionOptions] is string editionOptions) - { - var dbVariable = Uniquify("@db_name"); - builder - .AppendLine("BEGIN") - .AppendLine($"DECLARE {dbVariable} nvarchar(max) = QUOTENAME(DB_NAME());") - .AppendLine($"EXEC(N'ALTER DATABASE ' + {dbVariable} + ' MODIFY ( ") - .Append(editionOptions.Replace("'", "''")) - .AppendLine(" );');") - .AppendLine("END") - .AppendLine(); - } - - if (operation.Collation != operation.OldDatabase.Collation) - { - var dbVariable = Uniquify("@db_name"); - builder - .AppendLine("BEGIN") - .AppendLine($"DECLARE {dbVariable} nvarchar(max) = QUOTENAME(DB_NAME());"); - - var collation = operation.Collation; - if (operation.Collation == null) - { - var collationVariable = Uniquify("@defaultCollation"); - builder.AppendLine($"DECLARE {collationVariable} nvarchar(max) = CAST(SERVERPROPERTY('Collation') AS nvarchar(max));"); - collation = "' + " + collationVariable + " + N'"; - } - - builder - .AppendLine($"EXEC(N'ALTER DATABASE ' + {dbVariable} + ' COLLATE {collation};');") - .AppendLine("END") - .AppendLine(); - } - - GenerateFullTextCatalogStatements(operation, builder); - - if (!IsMemoryOptimized(operation)) - { - builder.EndCommand(suppressTransaction: true); - return; - } - - builder.AppendLine("IF SERVERPROPERTY('IsXTPSupported') = 1 AND SERVERPROPERTY('EngineEdition') <> 5"); - using (builder.Indent()) - { - builder - .AppendLine("BEGIN") - .AppendLine("IF NOT EXISTS ("); - using (builder.Indent()) - { - builder - .Append("SELECT 1 FROM [sys].[filegroups] [FG] ") - .Append("JOIN [sys].[database_files] [F] ON [FG].[data_space_id] = [F].[data_space_id] ") - .AppendLine("WHERE [FG].[type] = N'FX' AND [F].[type] = 2)"); - } - - using (builder.Indent()) - { - var dbVariable = Uniquify("@db_name"); - builder - .AppendLine("BEGIN") - .AppendLine("ALTER DATABASE CURRENT SET AUTO_CLOSE OFF;") - .AppendLine($"DECLARE {dbVariable} nvarchar(max) = DB_NAME();") - .AppendLine("DECLARE @fg_name nvarchar(max);") - .AppendLine("SELECT TOP(1) @fg_name = [name] FROM [sys].[filegroups] WHERE [type] = N'FX';") - .AppendLine() - .AppendLine("IF @fg_name IS NULL"); - - using (builder.Indent()) - { - builder - .AppendLine("BEGIN") - .AppendLine($"SET @fg_name = QUOTENAME({dbVariable} + N'_MODFG');") - .AppendLine("EXEC(N'ALTER DATABASE CURRENT ADD FILEGROUP ' + @fg_name + ' CONTAINS MEMORY_OPTIMIZED_DATA;');") - .AppendLine("END"); - } - - var pathVariable = Uniquify("@path"); - builder - .AppendLine() - .AppendLine($"DECLARE {pathVariable} nvarchar(max);") - .Append($"SELECT TOP(1) {pathVariable} = [physical_name] FROM [sys].[database_files] ") - .AppendLine("WHERE charindex('\\', [physical_name]) > 0 ORDER BY [file_id];") - .AppendLine($"IF ({pathVariable} IS NULL)") - .IncrementIndent().AppendLine($"SET {pathVariable} = '\\' + {dbVariable};").DecrementIndent() - .AppendLine() - .AppendLine($"DECLARE @filename nvarchar(max) = right({pathVariable}, charindex('\\', reverse({pathVariable})) - 1);") - .AppendLine( - "SET @filename = REPLACE(left(@filename, len(@filename) - charindex('.', reverse(@filename))), '''', '''''') + N'_MOD';") - .AppendLine( - "DECLARE @new_path nvarchar(max) = REPLACE(CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(max)), '''', '''''') + @filename;") - .AppendLine() - .AppendLine("EXEC(N'"); - - using (builder.Indent()) - { - builder - .AppendLine("ALTER DATABASE CURRENT") - .AppendLine("ADD FILE (NAME=''' + @filename + ''', filename=''' + @new_path + ''')") - .AppendLine("TO FILEGROUP ' + @fg_name + ';')"); - } - - builder.AppendLine("END"); - } - - builder.AppendLine("END"); - } - - builder.AppendLine() - .AppendLine("IF SERVERPROPERTY('IsXTPSupported') = 1") - .AppendLine("EXEC(N'"); - using (builder.Indent()) - { - builder - .AppendLine("ALTER DATABASE CURRENT") - .AppendLine("SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT ON;')"); - } - - builder.EndCommand(suppressTransaction: true); - } - - private void GenerateFullTextCatalogStatements( - AlterDatabaseOperation operation, - MigrationCommandListBuilder builder) - { - var oldCatalogs = SqlServerFullTextCatalog.GetFullTextCatalogs(operation.OldDatabase).ToDictionary(c => c.Name, c => c); - var newCatalogs = SqlServerFullTextCatalog.GetFullTextCatalogs(operation).ToDictionary(c => c.Name, c => c); - - // Drop removed catalogs - foreach (var (name, _) in oldCatalogs) - { - if (!newCatalogs.ContainsKey(name)) - { - builder - .Append("DROP FULLTEXT CATALOG ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .AppendLine(); - } - } - - // Create added catalogs - foreach (var (name, catalog) in newCatalogs) - { - if (!oldCatalogs.ContainsKey(name)) - { - builder.Append("CREATE FULLTEXT CATALOG ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)); - - if (!catalog.IsAccentSensitive) - { - builder.Append(" WITH ACCENT_SENSITIVITY = OFF"); - } - - if (catalog.IsDefault) - { - builder.Append(" AS DEFAULT"); - } - - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .AppendLine(); - } - } - - // Alter changed catalogs - foreach (var (name, catalog) in newCatalogs) - { - if (oldCatalogs.TryGetValue(name, out var oldProps)) - { - if (oldProps.IsAccentSensitive != catalog.IsAccentSensitive) - { - builder - .Append("ALTER FULLTEXT CATALOG ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) - .Append(" REBUILD WITH ACCENT_SENSITIVITY = ") - .Append(catalog.IsAccentSensitive ? "ON" : "OFF") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .AppendLine(); - } - - if (!oldProps.IsDefault && catalog.IsDefault) - { - builder - .Append("ALTER FULLTEXT CATALOG ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) - .Append(" AS DEFAULT") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .AppendLine(); - } - } - } - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate(AlterTableOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - if (IsMemoryOptimized(operation) - ^ IsMemoryOptimized(operation.OldTable)) - { - throw new InvalidOperationException(SqlServerStrings.AlterMemoryOptimizedTable); - } - - if (operation.OldTable.Comment != operation.Comment) - { - var dropDescription = operation.OldTable.Comment != null; - if (dropDescription) - { - DropDescription(builder, operation.Schema, operation.Name); - } - - if (operation.Comment != null) - { - AddDescription( - builder, - operation.Comment, - operation.Schema, - operation.Name, - omitVariableDeclarations: dropDescription); - } - } - - builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Name)); - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - DropForeignKeyOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - base.Generate(operation, model, builder, terminate: false); - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - DropIndexOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate) - { - if (string.IsNullOrEmpty(operation.Table)) - { - throw new InvalidOperationException(SqlServerStrings.IndexTableRequired); - } - - if (operation[SqlServerAnnotationNames.FullTextIndex] is string) - { - builder - .Append("DROP FULLTEXT INDEX ON ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table!, operation.Schema)); - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: true); - } - - return; - } - - var memoryOptimized = IsMemoryOptimized(operation, model, operation.Schema, operation.Table); - if (memoryOptimized) - { - builder - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table!, operation.Schema)) - .Append(" DROP INDEX ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)); - } - else - { - builder - .Append("DROP INDEX ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) - .Append(" ON ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)); - } - - if (terminate) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: memoryOptimized); - } - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - DropColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - 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) - { - builder - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); - } - } - - /// - /// Builds commands for the given - /// by making calls on the given . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate( - RenameColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - Rename( - Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema) - + "." - + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name), - operation.NewName, - "COLUMN", - builder); - builder.EndCommand(); - } - - private enum ParsingState - { - Normal, - InBlockComment, - InSquareBrackets, - InDoubleQuotes, - InQuotes - } - - /// - /// Builds commands for the given by making calls on the given - /// , and then terminates the final command. - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - protected override void Generate(SqlOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - if (Options.HasFlag(MigrationsSqlGenerationOptions.Script)) - { - builder.Append(operation.Sql); - if (!operation.Sql.EndsWith('\n')) - { - builder.AppendLine(); - } - - EndStatement(builder, operation.SuppressTransaction); - return; - } - - var preBatched = operation.Sql - .Replace("\\\n", "") - .Replace("\\\r\n", "") - .Split(["\r\n", "\n"], StringSplitOptions.None); - - var state = ParsingState.Normal; - var batchBuilder = new StringBuilder(); - foreach (var line in preBatched) - { - var trimmed = line.TrimStart(); - - if (state == ParsingState.Normal - && trimmed.StartsWith("GO", StringComparison.OrdinalIgnoreCase) - && (trimmed.Length == 2 - || char.IsWhiteSpace(trimmed[2]))) - { - var batch = batchBuilder.ToString(); - batchBuilder.Clear(); - - var count = trimmed.Length >= 4 - && int.TryParse(trimmed.AsSpan(3), out var specifiedCount) - ? specifiedCount - : 1; - - for (var j = 0; j < count; j++) - { - AppendBatch(batch); - } - } - else - { - for (var i = 0; i < trimmed.Length; i++) - { - var c = trimmed[i]; - var next = i + 1 < trimmed.Length ? trimmed[i + 1] : '\0'; - - if (state == ParsingState.Normal && c == '-' && next == '-') - { - goto LineEnd; - } - - state = state switch - { - ParsingState.Normal when c == '\'' => ParsingState.InQuotes, - ParsingState.Normal when c == '[' => ParsingState.InSquareBrackets, - ParsingState.Normal when c == '"' => ParsingState.InDoubleQuotes, - ParsingState.Normal when c == '/' && next == '*' => ConsumeAndReturn(ref i, ParsingState.InBlockComment), - - ParsingState.InQuotes when c == '\'' => ParsingState.Normal, - - ParsingState.InSquareBrackets when c == ']' && next == ']' => ConsumeAndReturn( - ref i, ParsingState.InSquareBrackets), - ParsingState.InSquareBrackets when c == ']' => ParsingState.Normal, - - ParsingState.InDoubleQuotes when c == '"' => ParsingState.Normal, - - ParsingState.InBlockComment when c == '*' && next == '/' => ConsumeAndReturn(ref i, ParsingState.Normal), - - _ => state - }; - } - - LineEnd: - batchBuilder.AppendLine(line); - } - } - - AppendBatch(batchBuilder.ToString()); - - ParsingState ConsumeAndReturn(ref int index, ParsingState newState) - { - index++; - return newState; - } - - void AppendBatch(string batch) - { - if (!string.IsNullOrWhiteSpace(batch)) - { - builder.Append(batch); - EndStatement(builder, operation.SuppressTransaction); - } - } - } - - /// - /// Builds commands for the given by making calls on the given - /// . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to build the commands. - /// Indicates whether or not to terminate the command after generating SQL for the operation. - protected override void Generate( - InsertDataOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool terminate = true) - { - GenerateIdentityInsert(builder, operation, on: true, model); - - var sqlBuilder = new StringBuilder(); - - var modificationCommands = GenerateModificationCommands(operation, model).ToList(); - var updateSqlGenerator = (ISqlServerUpdateSqlGenerator)Dependencies.UpdateSqlGenerator; - - foreach (var batch in _commandBatchPreparer.CreateCommandBatches(modificationCommands, moreCommandSets: true)) - { - updateSqlGenerator.AppendBulkInsertOperation(sqlBuilder, batch.ModificationCommands, commandPosition: 0); - } - - if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) - { - builder - .Append("EXEC(N'") - .Append(sqlBuilder.ToString().TrimEnd('\n', '\r', ';').Replace("'", "''")) - .Append("')") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - else - { - builder.Append(sqlBuilder.ToString()); - } - - GenerateIdentityInsert(builder, operation, on: false, model); - - if (terminate) - { - builder.EndCommand(); - } - } - - private void GenerateIdentityInsert(MigrationCommandListBuilder builder, InsertDataOperation operation, bool on, IModel? model) - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - - builder - .Append("IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE") - .Append(" [name] IN (") - .Append(string.Join(", ", operation.Columns.Select(stringTypeMapping.GenerateSqlLiteral))) - .Append(") AND [object_id] = OBJECT_ID(") - .Append( - stringTypeMapping.GenerateSqlLiteral( - Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema ?? model?.GetDefaultSchema()))) - .AppendLine("))"); - - using (builder.Indent()) - { - builder - .Append("SET IDENTITY_INSERT ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema ?? model?.GetDefaultSchema())) - .Append(on ? " ON" : " OFF") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - } - - /// - protected override void Generate(DeleteDataOperation operation, IModel? model, MigrationCommandListBuilder builder) - => GenerateExecWhenIdempotent(builder, b => base.Generate(operation, model, b)); - - /// - 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, - string name, - SequenceOperation operation, - IModel? model, - MigrationCommandListBuilder builder, - bool forAlter) - { - builder - .Append(" INCREMENT BY ") - .Append(IntegerConstant(operation.IncrementBy)); - - if (operation.MinValue.HasValue) - { - builder - .Append(" MINVALUE ") - .Append(IntegerConstant(operation.MinValue.Value)); - } - else if (forAlter) - { - builder.Append(" NO MINVALUE"); - } - - if (operation.MaxValue.HasValue) - { - builder - .Append(" MAXVALUE ") - .Append(IntegerConstant(operation.MaxValue.Value)); - } - else if (forAlter) - { - builder.Append(" NO MAXVALUE"); - } - - builder.Append(operation.IsCyclic ? " CYCLE" : " NO CYCLE"); - } - - /// - /// Generates a SQL fragment for a column definition for the given column metadata. - /// - /// The schema that contains the table, or to use the default schema. - /// The table that contains the column. - /// The column name. - /// The column metadata. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to add the SQL fragment. - protected override void ColumnDefinition( - string? schema, - string table, - string name, - ColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - if (operation.ComputedColumnSql != null) - { - ComputedColumnDefinition(schema, table, name, operation, model, builder); - - return; - } - - var columnType = operation.ColumnType ?? GetColumnType(schema, table, name, operation, model); - builder - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) - .Append(" ") - .Append(columnType); - - if (operation.Collation != null) - { - // SQL Server collation docs: https://learn.microsoft.com/sql/relational-databases/collations/collation-and-unicode-support - - // The default behavior in MigrationsSqlGenerator is to quote collation names, but SQL Server does not support that. - // Instead, make sure the collation name only contains a restricted set of characters. - foreach (var c in operation.Collation) - { - if (!char.IsLetterOrDigit(c) && c != '_') - { - throw new InvalidOperationException(SqlServerStrings.InvalidCollationName(operation.Collation)); - } - } - - builder - .Append(" COLLATE ") - .Append(operation.Collation); - } - - if (operation[SqlServerAnnotationNames.Sparse] is bool isSparse && isSparse) - { - builder.Append(" SPARSE"); - } - - var isPeriodStartColumn = (operation[SqlServerAnnotationNames.TemporalIsPeriodStartColumn] as bool?) == true; - var isPeriodEndColumn = (operation[SqlServerAnnotationNames.TemporalIsPeriodEndColumn] as bool?) == true; - - if (isPeriodStartColumn || isPeriodEndColumn) - { - builder.Append(" GENERATED ALWAYS AS ROW "); - builder.Append(isPeriodStartColumn ? "START" : "END"); - - // Defaults to true to preserve backward compatibility - the period columns have always been hidden. - // Set to false via TemporalPeriodPropertyBuilder.IsHidden(false). - var hidden = operation[SqlServerAnnotationNames.IsHidden] as bool? ?? true; - if (hidden) - { - builder.Append(" HIDDEN"); - } - } - - 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, defaultConstraintName, builder); - } - - var identity = operation[SqlServerAnnotationNames.Identity] as string; - if (identity != null - || (operation[SqlServerAnnotationNames.ValueGenerationStrategy] as SqlServerValueGenerationStrategy?) - == SqlServerValueGenerationStrategy.IdentityColumn) - { - builder.Append(" IDENTITY"); - - if (!string.IsNullOrEmpty(identity) - && identity != "1, 1") - { - builder - .Append("(") - .Append(identity) - .Append(")"); - } - } - } - - /// - /// Generates a SQL fragment for a computed column definition for the given column metadata. - /// - /// The schema that contains the table, or to use the default schema. - /// The table that contains the column. - /// The column name. - /// The column metadata. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to add the SQL fragment. - protected override void ComputedColumnDefinition( - string? schema, - string table, - string name, - ColumnOperation operation, - IModel? model, - MigrationCommandListBuilder builder) - { - builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)); - - builder - .Append(" AS ") - .Append(operation.ComputedColumnSql!); - - if (operation.Collation != null) - { - builder - .Append(" COLLATE ") - .Append(operation.Collation); - } - - if (operation.IsStored == true) - { - builder.Append(" PERSISTED"); - } - } - - /// - /// Generates a rename. - /// - /// The old name. - /// The new name. - /// The command builder to use to build the commands. - protected virtual void Rename( - string name, - string newName, - MigrationCommandListBuilder builder) - => Rename(name, newName, /*type:*/ null, builder); - - /// - /// Generates a rename. - /// - /// The old name. - /// The new name. - /// If not , then appends literal for type of object being renamed (e.g. column or index.) - /// The command builder to use to build the commands. - protected virtual void Rename( - string name, - string newName, - string? type, - MigrationCommandListBuilder builder) - { - // Types come from https://learn.microsoft.com/sql/relational-databases/system-stored-procedures/sp-rename-transact-sql - var typeMappingSource = Dependencies.TypeMappingSource; - var nameTypeMapping = typeMappingSource.FindMapping(typeof(string), "nvarchar(776)")!; - - builder - .Append("EXEC sp_rename ") - .Append(nameTypeMapping.GenerateSqlLiteral(name)) - .Append(", ") - .Append(nameTypeMapping.GenerateSqlLiteral(newName)); - - if (type != null) - { - builder - .Append(", ") - .Append(typeMappingSource.FindMapping(typeof(string), "varchar(13)")!.GenerateSqlLiteral(type)); - } - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - /// - /// Generates a transfer from one schema to another. - /// - /// The schema to transfer to. - /// The schema to transfer from. - /// The name of the item to transfer. - /// The command builder to use to build the commands. - protected virtual void Transfer( - string? newSchema, - string? schema, - string name, - MigrationCommandListBuilder builder) - { - if (newSchema == null) - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - - var schemaVariable = Uniquify("@defaultSchema"); - builder - .AppendLine($"DECLARE {schemaVariable} nvarchar(max) = QUOTENAME(SCHEMA_NAME());") - .Append("EXEC(") - .Append($"N'ALTER SCHEMA ' + {schemaVariable} + ") - .Append( - stringTypeMapping.GenerateSqlLiteral( - " TRANSFER " + Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema) + ";")) - .AppendLine(");"); - } - else - { - builder - .Append("ALTER SCHEMA ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(newSchema)) - .Append(" TRANSFER ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema)) - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - } - - /// - /// Generates a SQL fragment for traits of an index from a , - /// , or . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to add the SQL fragment. - protected override void IndexTraits(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - if (operation[SqlServerAnnotationNames.Clustered] is bool clustered) - { - builder.Append(clustered ? "CLUSTERED " : "NONCLUSTERED "); - } - } - - /// - /// Generates a SQL fragment for extras (filter, included columns, options) of an index from a . - /// - /// The operation. - /// The target model which may be if the operations exist without a model. - /// The command builder to use to add the SQL fragment. - protected override void IndexOptions(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) - { - if (operation[SqlServerAnnotationNames.Include] is IReadOnlyList includeColumns - && includeColumns.Count > 0) - { - builder.Append(" INCLUDE ("); - for (var i = 0; i < includeColumns.Count; i++) - { - builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(includeColumns[i])); - - if (i != includeColumns.Count - 1) - { - builder.Append(", "); - } - } - - builder.Append(")"); - } - - if (operation is CreateIndexOperation createIndexOperation) - { - if (!string.IsNullOrEmpty(createIndexOperation.Filter)) - { - builder - .Append(" WHERE ") - .Append(createIndexOperation.Filter); - } - else if (UseLegacyIndexFilters(createIndexOperation, model)) - { - var table = model?.GetRelationalModel().FindTable(createIndexOperation.Table, createIndexOperation.Schema); - var nullableColumns = createIndexOperation.Columns - .Where(c => table?.FindColumn(c)?.IsNullable != false) - .ToList(); - - builder.Append(" WHERE "); - for (var i = 0; i < nullableColumns.Count; i++) - { - if (i != 0) - { - builder.Append(" AND "); - } - - builder - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(nullableColumns[i])) - .Append(" IS NOT NULL"); - } - } - } - - var options = new List(); - - if (operation[SqlServerAnnotationNames.FillFactor] is int fillFactor) - { - options.Add("FILLFACTOR = " + fillFactor); - } - - if (operation[SqlServerAnnotationNames.CreatedOnline] is bool isOnline && isOnline) - { - options.Add("ONLINE = ON"); - } - - if (operation[SqlServerAnnotationNames.SortInTempDb] is bool sortInTempDb && sortInTempDb) - { - options.Add("SORT_IN_TEMPDB = ON"); - } - - if (operation[SqlServerAnnotationNames.DataCompression] is DataCompressionType dataCompressionType) - { - options.Add( - "DATA_COMPRESSION = " - + dataCompressionType switch - { - DataCompressionType.None => "NONE", - DataCompressionType.Row => "ROW", - DataCompressionType.Page => "PAGE", - - _ => throw new UnreachableException(), - }); - } - - // When this CreateIndexOperation was rewritten from a Drop+Create pair (an index facet - // changed and the index needs to be recreated), emit DROP_EXISTING = ON so SQL Server - // atomically replaces the index without leaving the table un-indexed during the rebuild. - // See #35067. - if (operation[SqlServerAnnotationNames.UseDropExisting] is true) - { - options.Add("DROP_EXISTING = ON"); - } - - // Vector index options. - // Note that the metric facet is mandatory, and used to determine if the index is a vector index. - if (operation[SqlServerAnnotationNames.VectorIndexMetric] is string vectorMetric) - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping("varchar(max)"); - - options.Add("METRIC = " + stringTypeMapping.GenerateSqlLiteral(vectorMetric)); - - if (operation[SqlServerAnnotationNames.VectorIndexType] is string vectorType) - { - options.Add("TYPE = " + stringTypeMapping.GenerateSqlLiteral(vectorType)); - } - } - - if (options.Count > 0) - { - builder - .Append(" WITH (") - .Append(string.Join(", ", options)) - .Append(")"); - } - } - - /// - /// Generates a SQL fragment for the given referential action. - /// - /// The referential action. - /// The command builder to use to add the SQL fragment. - protected override void ForeignKeyAction(ReferentialAction referentialAction, MigrationCommandListBuilder builder) - { - if (referentialAction == ReferentialAction.Restrict) - { - builder.Append("NO ACTION"); - } - else - { - base.ForeignKeyAction(referentialAction, builder); - } - } - - /// - /// Generates a SQL fragment to drop default constraints for a column. - /// - /// 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"); - - builder - .Append("DECLARE ") - .Append(variable) - .AppendLine(" nvarchar(max);") - .Append("SELECT ") - .Append(variable) - .AppendLine(" = QUOTENAME(OBJECT_NAME([c].[default_object_id]))") - .AppendLine("FROM [sys].[columns] [c]") - .Append("WHERE [c].[object_id] = OBJECT_ID(") - .Append( - stringTypeMapping.GenerateSqlLiteral( - Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema))) - .Append(") AND [c].[name] = ") - .Append(stringTypeMapping.GenerateSqlLiteral(columnName)) - .AppendLine(";") - .Append("IF ") - .Append(variable) - .Append(" IS NOT NULL EXEC(") - .Append( - stringTypeMapping.GenerateSqlLiteral( - "ALTER TABLE " + Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema) + " DROP CONSTRAINT ")) - .Append(" + ") - .Append(variable) - .Append(" + '") - .Append(Dependencies.SqlGenerationHelper.StatementTerminator) - .Append("')") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - /// - /// Gets the list of indexes that need to be rebuilt when the given column is changing. - /// - /// The column. - /// The operation which may require a rebuild. - /// The list of indexes affected. - protected virtual IEnumerable GetIndexesToRebuild( - IColumn? column, - MigrationOperation currentOperation) - { - if (column == null) - { - yield break; - } - - var table = column.Table; - var createIndexOperations = _operations.SkipWhile(o => o != currentOperation).Skip(1) - .OfType().Where(o => o.Table == table.Name && o.Schema == table.Schema).ToList(); - foreach (var index in table.Indexes) - { - var indexName = index.Name; - if (createIndexOperations.Any(o => o.Name == indexName)) - { - continue; - } - - if (index.Columns.Any(c => c == column)) - { - yield return index; - } - else if (index[SqlServerAnnotationNames.Include] is IReadOnlyList includeColumns - && includeColumns.Contains(column.Name)) - { - yield return index; - } - } - } - - /// - /// Generates SQL to drop the given indexes. - /// - /// The indexes to drop. - /// The command builder to use to build the commands. - protected virtual void DropIndexes( - IEnumerable indexes, - MigrationCommandListBuilder builder) - { - foreach (var index in indexes) - { - var table = index.Table; - var operation = new DropIndexOperation - { - Schema = table.Schema, - Table = table.Name, - Name = index.Name - }; - operation.AddAnnotations(index.GetAnnotations()); - - Generate(operation, table.Model.Model, builder, terminate: false); - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - } - - /// - /// Generates SQL to create the given indexes. - /// - /// The indexes to create. - /// The command builder to use to build the commands. - protected virtual void CreateIndexes( - IEnumerable indexes, - MigrationCommandListBuilder builder) - { - foreach (var index in indexes) - { - Generate(CreateIndexOperation.CreateFrom(index), index.Table.Model.Model, builder, terminate: false); - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - } - - /// - /// Generates add commands for descriptions on tables and columns. - /// - /// The command builder to use to build the commands. - /// The new description to be applied. - /// The schema of the table. - /// The name of the table. - /// The name of the column. - /// - /// Indicates whether the variable declarations should be omitted. - /// - protected virtual void AddDescription( - MigrationCommandListBuilder builder, - string description, - string? schema, - string table, - string? column = null, - bool omitVariableDeclarations = false) - { - var schemaLiteral = Uniquify("@defaultSchema", increase: !omitVariableDeclarations); - var descriptionVariable = Uniquify("@description", increase: false); - - if (schema == null) - { - if (!omitVariableDeclarations) - { - builder.Append($"DECLARE {schemaLiteral} AS sysname") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - builder.Append($"SET {schemaLiteral} = SCHEMA_NAME()") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - } - else - { - schemaLiteral = Literal(schema); - } - - if (!omitVariableDeclarations) - { - builder.Append($"DECLARE {descriptionVariable} AS sql_variant") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - builder.Append($"SET {descriptionVariable} = {Literal(description)}") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - builder - .Append("EXEC sp_addextendedproperty 'MS_Description', ") - .Append(descriptionVariable) - .Append(", 'SCHEMA', ") - .Append(schemaLiteral) - .Append(", 'TABLE', ") - .Append(Literal(table)); - - if (column != null) - { - builder - .Append(", 'COLUMN', ") - .Append(Literal(column)); - } - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - string Literal(string s) - => SqlLiteral(s); - - static string SqlLiteral(string value) - { - var builder = new StringBuilder(); - - var start = 0; - int i; - int length; - var openApostrophe = false; - var lastConcatStartPoint = 0; - var concatCount = 1; - var concatStartList = new List(); - for (i = 0; i < value.Length; i++) - { - var lineFeed = value[i] == '\n'; - var carriageReturn = value[i] == '\r'; - var apostrophe = value[i] == '\''; - if (lineFeed || carriageReturn || apostrophe) - { - length = i - start; - if (length != 0) - { - if (!openApostrophe) - { - AddConcatOperatorIfNeeded(); - builder.Append("N\'"); - openApostrophe = true; - } - - builder.Append(value.AsSpan().Slice(start, length)); - } - - if (lineFeed || carriageReturn) - { - if (openApostrophe) - { - builder.Append('\''); - openApostrophe = false; - } - - AddConcatOperatorIfNeeded(); - builder - .Append("NCHAR(") - .Append(lineFeed ? "10" : "13") - .Append(')'); - } - else if (apostrophe) - { - if (!openApostrophe) - { - AddConcatOperatorIfNeeded(); - builder.Append("N'"); - openApostrophe = true; - } - - builder.Append("''"); - } - - start = i + 1; - } - } - - length = i - start; - if (length != 0) - { - if (!openApostrophe) - { - AddConcatOperatorIfNeeded(); - builder.Append("N\'"); - openApostrophe = true; - } - - builder.Append(value.AsSpan().Slice(start, length)); - } - - if (openApostrophe) - { - builder.Append('\''); - } - - for (var j = concatStartList.Count - 1; j >= 0; j--) - { - builder.Insert(concatStartList[j], "CONCAT("); - builder.Append(')'); - } - - if (builder.Length == 0) - { - builder.Append("N''"); - } - - var result = builder.ToString(); - - return result; - - void AddConcatOperatorIfNeeded() - { - if (builder.Length != 0) - { - builder.Append(", "); - concatCount++; - - if (concatCount == 2) - { - concatStartList.Add(lastConcatStartPoint); - } - - if (concatCount == 254) - { - lastConcatStartPoint = builder.Length; - concatCount = 1; - } - } - } - } - } - - /// - /// Generates drop commands for descriptions on tables and columns. - /// - /// The command builder to use to build the commands. - /// The schema of the table. - /// The name of the table. - /// The name of the column. - /// - /// Indicates whether the variable declarations should be omitted. - /// - protected virtual void DropDescription( - MigrationCommandListBuilder builder, - string? schema, - string table, - string? column = null, - bool omitVariableDeclarations = false) - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - - var schemaLiteral = Uniquify("@defaultSchema", increase: !omitVariableDeclarations); - var descriptionVariable = Uniquify("@description", increase: false); - if (schema == null) - { - if (!omitVariableDeclarations) - { - builder.Append($"DECLARE {schemaLiteral} AS sysname") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - builder.Append($"SET {schemaLiteral} = SCHEMA_NAME()") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - } - else - { - schemaLiteral = Literal(schema); - } - - if (!omitVariableDeclarations) - { - builder.Append($"DECLARE {descriptionVariable} AS sql_variant") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - } - - builder - .Append("EXEC sp_dropextendedproperty 'MS_Description', 'SCHEMA', ") - .Append(schemaLiteral) - .Append(", 'TABLE', ") - .Append(Literal(table)); - - if (column != null) - { - builder - .Append(", 'COLUMN', ") - .Append(Literal(column)); - } - - builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - string Literal(string s) - => stringTypeMapping.GenerateSqlLiteral(s); - } - - /// - /// Checks whether or not should have a filter generated for it by - /// Migrations. - /// - /// The index creation operation. - /// The target model. - /// if a filter should be generated. - protected virtual bool UseLegacyIndexFilters(CreateIndexOperation operation, IModel? model) - => (!TryGetVersion(model, out var version) || VersionComparer.Compare(version, "2.0.0") < 0) - && operation.Filter is null - && operation.IsUnique - && operation[SqlServerAnnotationNames.Clustered] is null or false - && model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) is var table - && operation.Columns.Any(c => table?.FindColumn(c)?.IsNullable != false); - - private static string IntegerConstant(long value) - => string.Format(CultureInfo.InvariantCulture, "{0}", value); - - private static bool IsMemoryOptimized(Annotatable annotatable, IModel? model, string? schema, string tableName) - => annotatable[SqlServerAnnotationNames.MemoryOptimized] as bool? - ?? ((model?.GetRelationalModel().FindTable(tableName, schema)?[SqlServerAnnotationNames.MemoryOptimized] as bool?) == true); - - private static bool IsMemoryOptimized(Annotatable annotatable) - => (annotatable[SqlServerAnnotationNames.MemoryOptimized] as bool?) == true; - - private static bool IsIdentity(ColumnOperation operation) - => operation[SqlServerAnnotationNames.Identity] != null - || (operation[SqlServerAnnotationNames.ValueGenerationStrategy] as SqlServerValueGenerationStrategy?) - == SqlServerValueGenerationStrategy.IdentityColumn; - - private static void RemoveIdentityAnnotations(ColumnOperation operation) - { - operation.RemoveAnnotation(SqlServerAnnotationNames.Identity); - - if ((operation[SqlServerAnnotationNames.ValueGenerationStrategy] as SqlServerValueGenerationStrategy?) - == SqlServerValueGenerationStrategy.IdentityColumn) - { - operation.RemoveAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy); - } - } - - private static bool TryParseIdentitySeedIncrement(ColumnOperation operation, out int seed, out int increment) - { - if (operation[SqlServerAnnotationNames.Identity] is string seedIncrement - && seedIncrement.Split(",") is [var seedString, var incrementString] - && int.TryParse(seedString, out var seedParsed) - && int.TryParse(incrementString, out var incrementParsed)) - { - (seed, increment) = (seedParsed, incrementParsed); - return true; - } - - (seed, increment) = (0, 0); - return false; - } - - private void GenerateExecWhenIdempotent( - MigrationCommandListBuilder builder, - Action generate) - { - if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) - { - var subBuilder = new MigrationCommandListBuilder(Dependencies); - generate(subBuilder); - - var command = subBuilder.GetCommandList().Single(); - builder - .Append("EXEC(N'") - .Append(command.CommandText.TrimEnd('\n', '\r', ';').Replace("'", "''")) - .Append("')") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) - .EndCommand(command.TransactionSuppressed); - - return; - } - - generate(builder); - } - - private static bool HasDifferences(IEnumerable source, IEnumerable target) - { - var targetAnnotations = target.ToDictionary(a => a.Name); - - var count = 0; - foreach (var sourceAnnotation in source) - { - if (!targetAnnotations.TryGetValue(sourceAnnotation.Name, out var targetAnnotation) - || !Equals(sourceAnnotation.Value, targetAnnotation.Value)) - { - return true; - } - - count++; - } - - return count != targetAnnotations.Count; - } - - private string Uniquify(string variableName, bool increase = true) - { - if (increase) - { - _variableCounter++; - } - - return _variableCounter == 0 ? variableName : variableName + _variableCounter; - } - - private IReadOnlyList RewriteDropAndCreateIndexAsDropExisting( - IReadOnlyList migrationOperations, - IModel? model) - { - // The differ produces a DropIndexOperation + CreateIndexOperation pair when an index facet - // changes (e.g. fill factor, sort order, uniqueness, filter, columns). On SQL Server the - // pair can be collapsed into a single `CREATE INDEX ... WITH (DROP_EXISTING = ON)` which - // is more efficient: queries can continue using the old index while the new one is being - // built, instead of going un-indexed during the drop. See #35067. - // - // The collapse is only safe when the drop is IMMEDIATELY followed by the matching create. - // If anything sits between them (e.g. an AlterColumnOperation on the indexed column, which - // SQL Server only allows once the index is gone), removing the drop would re-introduce the - // old index before the intermediate operation runs and break the migration. The rewrite is - // also limited to non-special indexes (no memory-optimized, full-text or vector index, - // since those use different syntax/restrictions). - - // Scan for adjacent (DropIndex, CreateIndex) pairs with matching identity. - var dropsToRemove = new HashSet(); - for (var i = 0; i < migrationOperations.Count - 1; i++) - { - if (migrationOperations[i] is not DropIndexOperation dropOperation - || dropOperation.Table is null - || migrationOperations[i + 1] is not CreateIndexOperation createOperation - || createOperation.Table is null - || dropOperation.Name != createOperation.Name - || dropOperation.Table != createOperation.Table - || dropOperation.Schema != createOperation.Schema) - { - continue; - } - - // operations[i + 1] is the matching create, so the next operation cannot be a - // DropIndexOperation and can't start another pair; advance past it. - i++; - - // Skip special index types that don't support DROP_EXISTING. - if (createOperation[SqlServerAnnotationNames.FullTextIndex] is not null - || createOperation[SqlServerAnnotationNames.VectorIndexMetric] is not null - || IsMemoryOptimized(createOperation, model, createOperation.Schema, createOperation.Table)) - { - continue; - } - - createOperation.AddAnnotation(SqlServerAnnotationNames.UseDropExisting, true); - dropsToRemove.Add(dropOperation); - } - - if (dropsToRemove.Count == 0) - { - return migrationOperations; - } - - var resultOperations = new List(migrationOperations.Count - dropsToRemove.Count); - foreach (var migrationOperation in migrationOperations) - { - if (migrationOperation is DropIndexOperation dropOperation && dropsToRemove.Contains(dropOperation)) - { - continue; - } - - resultOperations.Add(migrationOperation); - } - - return resultOperations; - } - - private IReadOnlyList FixLegacyTemporalAnnotations(IReadOnlyList migrationOperations) - { - // short-circuit for non-temporal migrations (which is the majority) - if (migrationOperations.All(o => (o[SqlServerAnnotationNames.IsTemporal] as bool?) != true)) - { - return migrationOperations; - } - - var resultOperations = new List(migrationOperations.Count); - foreach (var migrationOperation in migrationOperations) - { - var isTemporal = (migrationOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; - if (!isTemporal) - { - resultOperations.Add(migrationOperation); - continue; - } - - switch (migrationOperation) - { - case CreateTableOperation createTableOperation: - - foreach (var column in createTableOperation.Columns) - { - NormalizeTemporalAnnotationsForAddColumnOperation(column); - } - - resultOperations.Add(migrationOperation); - break; - - case AddColumnOperation addColumnOperation: - NormalizeTemporalAnnotationsForAddColumnOperation(addColumnOperation); - resultOperations.Add(addColumnOperation); - break; - - case AlterColumnOperation alterColumnOperation: - RemoveLegacyTemporalColumnAnnotations(alterColumnOperation); - RemoveLegacyTemporalColumnAnnotations(alterColumnOperation.OldColumn); - if (!CanSkipAlterColumnOperation(alterColumnOperation, alterColumnOperation.OldColumn)) - { - resultOperations.Add(alterColumnOperation); - } - - break; - - case DropColumnOperation dropColumnOperation: - RemoveLegacyTemporalColumnAnnotations(dropColumnOperation); - resultOperations.Add(dropColumnOperation); - break; - - case RenameColumnOperation renameColumnOperation: - RemoveLegacyTemporalColumnAnnotations(renameColumnOperation); - resultOperations.Add(renameColumnOperation); - break; - - default: - resultOperations.Add(migrationOperation); - break; - } - } - - return resultOperations; - - static void NormalizeTemporalAnnotationsForAddColumnOperation(AddColumnOperation addColumnOperation) - { - var periodStartColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; - var periodEndColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; - if (periodStartColumnName == addColumnOperation.Name) - { - addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true); - } - else if (periodEndColumnName == addColumnOperation.Name) - { - addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true); - } - - RemoveLegacyTemporalColumnAnnotations(addColumnOperation); - } - - static void RemoveLegacyTemporalColumnAnnotations(MigrationOperation operation) - { - operation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); - operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName); - operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema); - operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); - operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); - } - - static bool CanSkipAlterColumnOperation(ColumnOperation column, ColumnOperation oldColumn) - => ColumnPropertiesAreTheSame(column, oldColumn) && AnnotationsAreTheSame(column, oldColumn); - - // don't compare name, table or schema - they are not being set in the model differ (since they should always be the same) - static bool ColumnPropertiesAreTheSame(ColumnOperation column, ColumnOperation oldColumn) - => column.ClrType == oldColumn.ClrType - && column.Collation == oldColumn.Collation - && column.ColumnType == oldColumn.ColumnType - && column.Comment == oldColumn.Comment - && column.ComputedColumnSql == oldColumn.ComputedColumnSql - && Equals(column.DefaultValue, oldColumn.DefaultValue) - && column.DefaultValueSql == oldColumn.DefaultValueSql - && column.IsDestructiveChange == oldColumn.IsDestructiveChange - && column.IsFixedLength == oldColumn.IsFixedLength - && column.IsNullable == oldColumn.IsNullable - && column.IsReadOnly == oldColumn.IsReadOnly - && column.IsRowVersion == oldColumn.IsRowVersion - && column.IsStored == oldColumn.IsStored - && column.IsUnicode == oldColumn.IsUnicode - && column.MaxLength == oldColumn.MaxLength - && column.Precision == oldColumn.Precision - && column.Scale == oldColumn.Scale; - - static bool AnnotationsAreTheSame(ColumnOperation column, ColumnOperation oldColumn) - { - var columnAnnotations = column.GetAnnotations().ToList(); - var oldColumnAnnotations = oldColumn.GetAnnotations().ToList(); - - return columnAnnotations.Count == oldColumnAnnotations.Count - && columnAnnotations.Zip(oldColumnAnnotations) - .All(x => x.First.Name == x.Second.Name - && StructuralComparisons.StructuralEqualityComparer.Equals(x.First.Value, x.Second.Value)); - } - } - - private IReadOnlyList RewriteOperations( - IReadOnlyList migrationOperations, - IModel? model, - MigrationsSqlGenerationOptions options) - { - migrationOperations = FixLegacyTemporalAnnotations(migrationOperations); - migrationOperations = RewriteDropAndCreateIndexAsDropExisting(migrationOperations, model); - - var operations = new List(); - var availableSchemas = new List(); - - // we need to know temporal information for all the tables involved in the migration - // problem is, the temporal information is stored only on table operations and not column operations - // if migration operation doesn't contain the table operation, or the table operation comes later - // we don't know what we should do - // to fix that, we loop through all the operations and extract initial temporal state for relevant tables - // if we don't encounter any table operations, then we can take information from the model - // since migration hasn't changed it at all - be we can only know that after looping though all ops - // once we have the initial state of the table, we can update it each time we encounter a table operation - // and we can use what we stored when dealing with all other operations (that don't contain temporal annotations themselves) - var temporalTableInformationMap = new Dictionary<(string TableName, string? Schema), TemporalOperationInformation>(); - var missingTemporalTableInformation = new List<(string TableName, string? Schema)>(); - - foreach (var operation in migrationOperations) - { - switch (operation) - { - case CreateTableOperation createTableOperation: - { - var tableName = createTableOperation.Name; - var rawSchema = createTableOperation.Schema; - var schema = rawSchema ?? model?.GetDefaultSchema(); - if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) - { - var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, createTableOperation); - temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; - } - - // no need to remove from missingTemporalTableInformation - CreateTable should be first operation for this table - // so there can't be entry for it in missingTemporalTableInformation (they are added by other/earlier operations on that table) - // the only possibility is that we had a table before, dropped it and now creating a new table with the same name - // but in this case we would have generated the necessary information from the DropTableOperation - // and also removed the missingTemporalTableInformation entry if there was one before - break; - } - - case DropTableOperation dropTableOperation: - { - var tableName = dropTableOperation.Name; - var rawSchema = dropTableOperation.Schema; - var schema = rawSchema ?? model?.GetDefaultSchema(); - if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) - { - var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, dropTableOperation); - temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; - } - - missingTemporalTableInformation.Remove((tableName, rawSchema)); - break; - } - - case RenameTableOperation renameTableOperation: - { - var tableName = renameTableOperation.Name; - var rawSchema = renameTableOperation.Schema; - var schema = rawSchema ?? model?.GetDefaultSchema(); - var newTableName = renameTableOperation.NewName!; - var newRawSchema = renameTableOperation.NewSchema; - var newSchema = newRawSchema ?? model?.GetDefaultSchema(); - - var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation); - if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) - { - temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; - } - - // we still need to check here - table with the new name could have existed before and have been deleted - // we want to preserve the original temporal info of that deleted table - if (!temporalTableInformationMap.ContainsKey((newTableName, newRawSchema))) - { - temporalTableInformationMap[(newTableName, newRawSchema)] = temporalTableInformation; - } - - missingTemporalTableInformation.Remove((tableName, rawSchema)); - missingTemporalTableInformation.Remove((newTableName, newRawSchema)); - - break; - } - - case AlterTableOperation alterTableOperation: - { - var tableName = alterTableOperation.Name; - var rawSchema = alterTableOperation.Schema; - var schema = rawSchema ?? model?.GetDefaultSchema(); - if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) - { - // we create the temporal info based on the OLD table here - we want the initial state - var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, alterTableOperation.OldTable); - - // The period-column hidden flags reflect the user's intent for the NEW state of the table, - // not the old state, so override them from the AlterTable operation itself when present. - if (alterTableOperation[SqlServerAnnotationNames.TemporalPeriodStartHidden] is bool startHidden) - { - temporalTableInformation.PeriodStartHidden = startHidden; - } - - if (alterTableOperation[SqlServerAnnotationNames.TemporalPeriodEndHidden] is bool endHidden) - { - temporalTableInformation.PeriodEndHidden = endHidden; - } - - temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; - } - - missingTemporalTableInformation.Remove((tableName, schema)); - break; - } - - default: - { - if (operation is ITableMigrationOperation tableMigrationOperation) - { - var tableName = tableMigrationOperation.Table; - var rawSchema = tableMigrationOperation.Schema; - if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema)) - && !missingTemporalTableInformation.Contains((tableName, rawSchema))) - { - missingTemporalTableInformation.Add((tableName, rawSchema)); - } - } - - break; - } - } - } - - // fill the missing temporal information from Relational Model - it's the second best source we have - // if we can't figure out proper temporal info from table annotations, - // and we don't have it in relational model (for whatever reason) we assume table is not temporal - // this last step is purely defensive and shouldn't happen in real situations - foreach (var (TableName, Schema) in missingTemporalTableInformation) - { - var table = model?.GetRelationalModel().FindTable(TableName, Schema)!; - if (table != null) - { - var schema = Schema ?? model?.GetDefaultSchema(); - - var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, table); - temporalTableInformationMap[(TableName, Schema)] = temporalTableInformation; - } - else - { - temporalTableInformationMap[(TableName, Schema)] = new TemporalOperationInformation - { - IsTemporalTable = false, - HistoryTableName = null, - HistoryTableSchema = null, - PeriodStartColumnName = null, - PeriodEndColumnName = null - }; - } - } - - var historyTables = new HashSet<(string Name, string? Schema)>( - temporalTableInformationMap.Values - .Where(t => t.IsTemporalTable && t.HistoryTableName != null) - .Select(t => (t.HistoryTableName!, t.HistoryTableSchema))); - - if (model != null) - { - foreach (var table in model.GetRelationalModel().Tables) - { - if ((table[SqlServerAnnotationNames.IsTemporal] as bool?) == true - && table[SqlServerAnnotationNames.TemporalHistoryTableName] is string modelHistoryTableName) - { - var modelHistoryTableSchema = - table[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string; - historyTables.Add((modelHistoryTableName, modelHistoryTableSchema)); - } - } - } - - // now we do proper processing - for table operations we look at the annotations on them - // and continuously update the stored temporal info as the table is being modified - // for column (and other) operations we don't have annotations on them, so we look into the - // information we stored in the initial pass and updated in when processing table ops that happened earlier - foreach (var operation in migrationOperations) - { - if (operation is EnsureSchemaOperation ensureSchemaOperation) - { - availableSchemas.Add(ensureSchemaOperation.Name); - } - - if (operation is not ITableMigrationOperation tableMigrationOperation) - { - operations.Add(operation); - continue; - } - - var tableName = tableMigrationOperation.Table; - var rawSchema = tableMigrationOperation.Schema; - - var suppressTransaction = IsMemoryOptimized(operation, model, rawSchema, tableName); - - var schema = rawSchema ?? model?.GetDefaultSchema(); - - TemporalOperationInformation temporalInformation; - if (operation is CreateTableOperation) - { - // for create table we always generate new temporal information from the operation itself - // just in case there was a table with that name before that got deleted/renamed - // also, temporal state (disabled versioning etc.) should always reset when creating a table - temporalInformation = BuildTemporalInformationFromMigrationOperation(schema, operation); - temporalTableInformationMap[(tableName, rawSchema)] = temporalInformation; - } - else - { - temporalInformation = temporalTableInformationMap[(tableName, rawSchema)]; - } - - switch (operation) - { - case CreateTableOperation createTableOperation: - { - // for create table we always generate new temporal information from the operation itself - // just in case there was a table with that name before that got deleted/renamed - // this shouldn't happen as we re-use existing tables rather than drop/recreate - // but we are being extra defensive here - // and also, temporal state (disabled versioning etc.) should always reset when creating a table - temporalInformation = BuildTemporalInformationFromMigrationOperation(schema, createTableOperation); - - if (temporalInformation.IsTemporalTable - && temporalInformation.HistoryTableSchema != schema - && temporalInformation.HistoryTableSchema != null - && !availableSchemas.Contains(temporalInformation.HistoryTableSchema)) - { - operations.Add(new EnsureSchemaOperation { Name = temporalInformation.HistoryTableSchema }); - availableSchemas.Add(temporalInformation.HistoryTableSchema); - } - - operations.Add(operation); - - break; - } - - case DropTableOperation dropTableOperation: - { - var isTemporalTable = (dropTableOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; - if (isTemporalTable) - { - // if we don't have temporal information, but we know table is temporal - // (based on the annotation found on the operation itself) - // we assume that versioning must be disabled, if we have temporal info we can check properly - if (temporalInformation is null || !temporalInformation.DisabledVersioning) - { - AddDisableVersioningOperation(tableName, schema, suppressTransaction); - } - - if (temporalInformation is not null) - { - temporalInformation.ShouldEnableVersioning = false; - temporalInformation.ShouldEnablePeriod = false; - } - - operations.Add(operation); - - var historyTableName = dropTableOperation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; - var historyTableSchema = - dropTableOperation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? schema; - var dropHistoryTableOperation = new DropTableOperation { Name = historyTableName!, Schema = historyTableSchema }; - operations.Add(dropHistoryTableOperation); - } - else - { - operations.Add(operation); - } - - // we removed the table, so we no longer need it's temporal information - // there will be no more operations involving this table - temporalTableInformationMap.Remove((tableName, schema)); - - break; - } - - case RenameTableOperation renameTableOperation: - { - temporalInformation ??= BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation); - - var isTemporalTable = (renameTableOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; - if (isTemporalTable) - { - DisableVersioning( - tableName, - schema, - temporalInformation, - suppressTransaction, - shouldEnableVersioning: true); - } - - operations.Add(operation); - - // since table was renamed, update entry in the temporal info map - temporalTableInformationMap[(renameTableOperation.NewName!, renameTableOperation.NewSchema)] = temporalInformation; - temporalTableInformationMap.Remove((tableName, schema)); - - break; - } - - case AlterTableOperation alterTableOperation: - { - var isTemporalTable = (alterTableOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; - var historyTableName = alterTableOperation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; - var historyTableSchema = alterTableOperation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? schema; - var periodStartColumnName = alterTableOperation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; - var periodEndColumnName = alterTableOperation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; - - var oldIsTemporalTable = (alterTableOperation.OldTable[SqlServerAnnotationNames.IsTemporal] as bool?) == true; - var oldHistoryTableName = - alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableName] as string; - var oldHistoryTableSchema = - alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string - ?? alterTableOperation.OldTable.Schema - ?? model?[RelationalAnnotationNames.DefaultSchema] as string; - - if (isTemporalTable) - { - if (!oldIsTemporalTable) - { - // converting from regular table to temporal table - enable period and versioning at the end - // other temporal information (history table, period columns etc) is added below - temporalInformation.ShouldEnablePeriod = true; - temporalInformation.ShouldEnableVersioning = true; - } - else - { - // changing something within temporal table - if (oldHistoryTableName != historyTableName - || oldHistoryTableSchema != historyTableSchema) - { - if (historyTableSchema != null - && !availableSchemas.Contains(historyTableSchema)) - { - operations.Add(new EnsureSchemaOperation { Name = historyTableSchema }); - availableSchemas.Add(historyTableSchema); - } - - operations.Add( - new RenameTableOperation - { - Name = oldHistoryTableName!, - Schema = oldHistoryTableSchema, - NewName = historyTableName, - NewSchema = historyTableSchema - }); - - temporalInformation.HistoryTableName = historyTableName; - temporalInformation.HistoryTableSchema = historyTableSchema; - } - } - } - else - { - if (oldIsTemporalTable) - { - // converting from temporal table to regular table - var oldPeriodStartColumnName = - alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; - var oldPeriodEndColumnName = - alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; - - DisableVersioning( - tableName, - schema, - temporalInformation, - suppressTransaction, - shouldEnableVersioning: null); - - if (!temporalInformation.DisabledPeriod) - { - DisablePeriod(tableName, schema, temporalInformation, suppressTransaction); - } - - if (oldHistoryTableName != null) - { - operations.Add(new DropTableOperation { Name = oldHistoryTableName, Schema = oldHistoryTableSchema }); - } - - // also clear any pending versioning/period, that would be switched on at the end - // we don't need it now that the table is no longer temporal - temporalInformation.ShouldEnableVersioning = false; - temporalInformation.ShouldEnablePeriod = false; - } - } - - temporalInformation.IsTemporalTable = isTemporalTable; - temporalInformation.HistoryTableName = historyTableName; - temporalInformation.HistoryTableSchema = historyTableSchema; - temporalInformation.PeriodStartColumnName = periodStartColumnName; - temporalInformation.PeriodEndColumnName = periodEndColumnName; - - if (isTemporalTable && historyTableName != null) - { - historyTables.Add((historyTableName, historyTableSchema)); - } - - operations.Add(operation); - break; - } - - case AddColumnOperation addColumnOperation: - { - // when adding a period column, we need to add it as a normal column first, and only later enable period - // removing the period information now, so that when we generate SQL that adds the column we won't be making them - // auto generated as period it won't work, unless period is enabled but we can't enable period without adding the - // columns first - chicken and egg - if (temporalInformation.IsTemporalTable) - { - addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); - addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); - addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); - - // model differ adds default value, but for period end we need to replace it with the correct one - - // DateTime.MaxValue - if (addColumnOperation.Name == temporalInformation.PeriodEndColumnName) - { - addColumnOperation.DefaultValue = DateTime.MaxValue; - } - - var isSparse = (addColumnOperation[SqlServerAnnotationNames.Sparse] as bool?) == true; - var isComputed = addColumnOperation.ComputedColumnSql != null; - - if (isSparse || isComputed) - { - DisableVersioning( - tableName, - schema, - temporalInformation, - suppressTransaction, - shouldEnableVersioning: true); - } - - // when adding sparse column to temporal table, we need to disable versioning. - // This is because it may be the case that HistoryTable is using compression (by default) - // and the add column operation fails in that situation - // in order to make it work we need to disable versioning (if we haven't done it already) - // and de-compress the HistoryTable - if (isSparse) - { - DecompressTable( - temporalInformation.HistoryTableName!, temporalInformation.HistoryTableSchema, suppressTransaction); - } - - if (addColumnOperation.ComputedColumnSql != null) - { - DisableVersioning( - tableName, - schema, - temporalInformation, - suppressTransaction, - shouldEnableVersioning: true); - } - - operations.Add(addColumnOperation); - - // when adding (non-period) column to an existing temporal table we need to check if we have disabled versioning - // due to some other operations in the same migration (e.g. delete column) - // if so, we need to also add the same column to history table - if (addColumnOperation.Name != temporalInformation.PeriodStartColumnName - && addColumnOperation.Name != temporalInformation.PeriodEndColumnName - && temporalInformation.DisabledVersioning) - { - var addHistoryTableColumnOperation = CopyColumnOperation(addColumnOperation); - addHistoryTableColumnOperation.Table = temporalInformation.HistoryTableName!; - addHistoryTableColumnOperation.Schema = temporalInformation.HistoryTableSchema; - - if (addHistoryTableColumnOperation.ComputedColumnSql != null) - { - // computed columns are not allowed inside HistoryTables - // but the historical computed value will be copied over to the non-computed counterpart, - // as long as their names and types (including nullability) match - // so we remove ComputedColumnSql info, so that the column in history table "appears normal" - addHistoryTableColumnOperation.ComputedColumnSql = null; - } - - // identity columns are not allowed inside HistoryTables - RemoveIdentityAnnotations(addHistoryTableColumnOperation); - - operations.Add(addHistoryTableColumnOperation); - } - } - else - { - // identity columns are not allowed inside HistoryTables - if (historyTables.Contains((tableName, schema))) - { - RemoveIdentityAnnotations(addColumnOperation); - } - - operations.Add(addColumnOperation); - } - - break; - } - - case DropColumnOperation dropColumnOperation: - { - if (temporalInformation.IsTemporalTable) - { - var droppingPeriodColumn = dropColumnOperation.Name == temporalInformation.PeriodStartColumnName - || dropColumnOperation.Name == temporalInformation.PeriodEndColumnName; - - // if we are dropping non-period column, we should enable versioning at the end. - // When dropping period column there is no need - we are removing the versioning for this table altogether - DisableVersioning( - tableName, - schema, - temporalInformation, - suppressTransaction, - shouldEnableVersioning: droppingPeriodColumn ? null : true); - - if (droppingPeriodColumn && !temporalInformation.DisabledPeriod) - { - DisablePeriod(tableName, schema, temporalInformation, suppressTransaction); - - // if we remove the period columns, it means we will be dropping the table - // also or at least convert it back to regular - no need to enable period later - temporalInformation.ShouldEnablePeriod = false; - } - - operations.Add(operation); - - if (!droppingPeriodColumn) - { - operations.Add( - new DropColumnOperation - { - Name = dropColumnOperation.Name, - Table = temporalInformation.HistoryTableName!, - Schema = temporalInformation.HistoryTableSchema - }); - } - } - else - { - operations.Add(operation); - } - - break; - } - - case RenameColumnOperation renameColumnOperation: - { - operations.Add(renameColumnOperation); - - // if we disabled period for the temporal table and now we are renaming the column, - // we need to also rename this same column in history table - if (temporalInformation.IsTemporalTable - && temporalInformation.DisabledVersioning - && temporalInformation.ShouldEnableVersioning) - { - var renameHistoryTableColumnOperation = new RenameColumnOperation - { - IsDestructiveChange = renameColumnOperation.IsDestructiveChange, - Name = renameColumnOperation.Name, - NewName = renameColumnOperation.NewName, - Table = temporalInformation.HistoryTableName!, - Schema = temporalInformation.HistoryTableSchema - }; - - operations.Add(renameHistoryTableColumnOperation); - } - - break; - } - - case AlterColumnOperation alterColumnOperation: - { - // we can remove temporal annotations, they don't make a difference when it comes to - // generating ALTER COLUMN operations and could just muddy the waters - alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); - alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); - alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); - alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); - alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); - alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); - - if (temporalInformation.IsTemporalTable) - { - if (alterColumnOperation.OldColumn.ComputedColumnSql != alterColumnOperation.ComputedColumnSql) - { - throw new NotSupportedException( - SqlServerStrings.TemporalMigrationModifyingComputedColumnNotSupported( - alterColumnOperation.Name, - alterColumnOperation.Table)); - } - - // for alter column operation converting column from nullable to non-nullable in the temporal table - // we must disable versioning in order to properly handle it - // specifically, switching values in history table from null to the default value - var changeToNonNullable = alterColumnOperation.OldColumn.IsNullable - && !alterColumnOperation.IsNullable; - - // for alter column converting to sparse we also need to disable versioning - // in case HistoryTable is compressed (so that we can de-compress it) - var changeToSparse = (alterColumnOperation.OldColumn[SqlServerAnnotationNames.Sparse] as bool?) != true - && (alterColumnOperation[SqlServerAnnotationNames.Sparse] as bool?) == true; - - // for alter column removing default value we also need to disable versioning - // because the default constraint needs to be removed from both main and history tables - var removingDefaultValue = (alterColumnOperation.OldColumn.DefaultValue is not null - || alterColumnOperation.OldColumn.DefaultValueSql is not null) - && alterColumnOperation.DefaultValue is null - && alterColumnOperation.DefaultValueSql is null; - - if (changeToNonNullable || changeToSparse || removingDefaultValue) - { - DisableVersioning( - tableName!, - schema, - temporalInformation, - suppressTransaction, - shouldEnableVersioning: true); - } - - if (changeToSparse) - { - DecompressTable( - temporalInformation.HistoryTableName!, temporalInformation.HistoryTableSchema, suppressTransaction); - } - - operations.Add(alterColumnOperation); - - // when modifying a period column, we need to perform the operations as a normal column first, and only later enable period - // removing the period information now, so that when we generate SQL that modifies the column we won't be making them auto generated as period - // (making column auto generated is not allowed in ALTER COLUMN statement) - // in later operation we enable the period and the period columns get set to auto generated automatically - // - // if the column is not period we just remove temporal information - it's no longer needed and could affect the generated sql - // we will generate all the necessary operations involved with temporal tables here - if (temporalInformation.DisabledVersioning && temporalInformation.ShouldEnableVersioning) - { - var alterHistoryTableColumn = CopyColumnOperation(alterColumnOperation); - alterHistoryTableColumn.Table = temporalInformation.HistoryTableName!; - alterHistoryTableColumn.Schema = temporalInformation.HistoryTableSchema; - alterHistoryTableColumn.OldColumn = CopyColumnOperation(alterColumnOperation.OldColumn); - alterHistoryTableColumn.OldColumn.Table = temporalInformation.HistoryTableName!; - alterHistoryTableColumn.OldColumn.Schema = temporalInformation.HistoryTableSchema; - - // identity columns are not allowed inside HistoryTables - RemoveIdentityAnnotations(alterHistoryTableColumn); - RemoveIdentityAnnotations(alterHistoryTableColumn.OldColumn); - - operations.Add(alterHistoryTableColumn); - } - } - else - { - // identity columns are not allowed inside HistoryTables - if (historyTables.Contains((tableName, schema))) - { - RemoveIdentityAnnotations(alterColumnOperation); - RemoveIdentityAnnotations(alterColumnOperation.OldColumn); - } - - operations.Add(alterColumnOperation); - } - - break; - } - - case DropPrimaryKeyOperation: - case AddPrimaryKeyOperation: - if (temporalInformation.IsTemporalTable) - { - DisableVersioning( - tableName!, - schema, - temporalInformation, - suppressTransaction, - shouldEnableVersioning: true); - } - - operations.Add(operation); - break; - - default: - operations.Add(operation); - break; - } - } - - foreach (var temporalInformation in temporalTableInformationMap.Where(x => x.Value.ShouldEnablePeriod)) - { - EnablePeriod( - temporalInformation.Key.TableName, - temporalInformation.Key.Schema, - temporalInformation.Value.PeriodStartColumnName!, - temporalInformation.Value.PeriodEndColumnName!, - temporalInformation.Value.PeriodStartHidden, - temporalInformation.Value.PeriodEndHidden, - temporalInformation.Value.SuppressTransaction); - } - - foreach (var temporalInformation in temporalTableInformationMap.Where(x => x.Value.ShouldEnableVersioning)) - { - EnableVersioning( - temporalInformation.Key.TableName, - temporalInformation.Key.Schema, - temporalInformation.Value.HistoryTableName!, - temporalInformation.Value.HistoryTableSchema, - temporalInformation.Value.SuppressTransaction); - } - - return operations; - - static TemporalOperationInformation BuildTemporalInformationFromMigrationOperation( - string? schema, - IAnnotatable operation) - { - var isTemporalTable = (operation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; - var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; - var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? schema; - var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; - var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; - - // Period columns default to HIDDEN; the annotation is only present when explicitly configured visible. - var periodStartHidden = operation[SqlServerAnnotationNames.TemporalPeriodStartHidden] as bool? ?? true; - var periodEndHidden = operation[SqlServerAnnotationNames.TemporalPeriodEndHidden] as bool? ?? true; - - return new TemporalOperationInformation - { - IsTemporalTable = isTemporalTable, - HistoryTableName = historyTableName, - HistoryTableSchema = historyTableSchema, - PeriodStartColumnName = periodStartColumnName, - PeriodEndColumnName = periodEndColumnName, - PeriodStartHidden = periodStartHidden, - PeriodEndHidden = periodEndHidden - }; - } - - void DisableVersioning( - string tableName, - string? schema, - TemporalOperationInformation temporalInformation, - bool suppressTransaction, - bool? shouldEnableVersioning) - { - if (!temporalInformation.DisabledVersioning - && !temporalInformation.ShouldEnableVersioning) - { - temporalInformation.DisabledVersioning = true; - - AddDisableVersioningOperation(tableName, schema, suppressTransaction); - - if (shouldEnableVersioning != null) - { - temporalInformation.ShouldEnableVersioning = shouldEnableVersioning.Value; - if (shouldEnableVersioning.Value) - { - temporalInformation.SuppressTransaction = suppressTransaction; - } - } - } - } - - void AddDisableVersioningOperation(string tableName, string? schema, bool suppressTransaction) - => operations.Add( - new SqlOperation - { - Sql = new StringBuilder() - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema)) - .AppendLine(" SET (SYSTEM_VERSIONING = OFF)") - .ToString(), - SuppressTransaction = suppressTransaction - }); - - void EnableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema, bool suppressTransaction) - { - var stringBuilder = new StringBuilder(); - - string? schemaVariable = null; - if (historyTableSchema == null) - { - schemaVariable = Uniquify("@historyTableSchema"); - // need to run command using EXEC to inject default schema - stringBuilder.AppendLine($"DECLARE {schemaVariable} nvarchar(max) = QUOTENAME(SCHEMA_NAME())"); - stringBuilder.Append("EXEC(N'"); - } - - var historyTable = historyTableSchema != null - ? Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName, historyTableSchema) - : Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName); - - stringBuilder - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)); - - if (historyTableSchema != null) - { - stringBuilder.AppendLine($" SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable}))"); - } - else - { - stringBuilder.AppendLine( - $" SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + {schemaVariable} + '.{historyTable}))')"); - } - - operations.Add( - new SqlOperation { Sql = stringBuilder.ToString(), SuppressTransaction = suppressTransaction }); - } - - void DisablePeriod( - string table, - string? schema, - TemporalOperationInformation temporalInformation, - bool suppressTransaction) - { - temporalInformation.DisabledPeriod = true; - - operations.Add( - new SqlOperation - { - Sql = new StringBuilder() - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) - .AppendLine(" DROP PERIOD FOR SYSTEM_TIME") - .ToString(), - SuppressTransaction = suppressTransaction - }); - } - - void EnablePeriod( - string table, - string? schema, - string periodStartColumnName, - string periodEndColumnName, - bool periodStartHidden, - bool periodEndHidden, - bool suppressTransaction) - { - var addPeriodSql = new StringBuilder() - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) - .Append(" ADD PERIOD FOR SYSTEM_TIME (") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName)) - .Append(", ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName)) - .Append(')') - .ToString(); - - if (options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) - { - addPeriodSql = new StringBuilder() - .Append("EXEC(N'") - .Append(addPeriodSql.Replace("'", "''")) - .Append("')") - .ToString(); - } - - operations.Add( - new SqlOperation { Sql = addPeriodSql, SuppressTransaction = suppressTransaction }); - - // Period columns are HIDDEN by default. Skip the `ADD HIDDEN` ALTER when the column was - // configured visible via TemporalPeriodPropertyBuilder.IsHidden(false). - if (periodStartHidden) - { - operations.Add( - new SqlOperation - { - Sql = new StringBuilder() - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) - .Append(" ALTER COLUMN ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName)) - .Append(" ADD HIDDEN") - .ToString(), - SuppressTransaction = suppressTransaction - }); - } - - if (periodEndHidden) - { - operations.Add( - new SqlOperation - { - Sql = new StringBuilder() - .Append("ALTER TABLE ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) - .Append(" ALTER COLUMN ") - .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName)) - .Append(" ADD HIDDEN") - .ToString(), - SuppressTransaction = suppressTransaction - }); - } - } - - void DecompressTable(string tableName, string? schema, bool suppressTransaction) - { - var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); - - var decompressTableCommand = new StringBuilder() - .Append("IF EXISTS (") - .Append("SELECT 1 FROM [sys].[tables] [t] ") - .Append("INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] ") - .Append($"WHERE [t].[name] = '{tableName}' "); - - if (schema != null) - { - decompressTableCommand.Append($"AND [t].[schema_id] = schema_id('{schema}') "); - } - - decompressTableCommand.AppendLine("AND data_compression <> 0)") - .Append("EXEC(") - .Append( - stringTypeMapping.GenerateSqlLiteral( - "ALTER TABLE " - + Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema) - + " REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE)" - + Dependencies.SqlGenerationHelper.StatementTerminator)) - .Append(")") - .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); - - operations.Add( - new SqlOperation { Sql = decompressTableCommand.ToString(), SuppressTransaction = suppressTransaction }); - } - - static TOperation CopyColumnOperation(ColumnOperation source) - where TOperation : ColumnOperation, new() - { - var result = new TOperation - { - ClrType = source.ClrType, - Collation = source.Collation, - ColumnType = source.ColumnType, - Comment = source.Comment, - ComputedColumnSql = source.ComputedColumnSql, - DefaultValue = source.DefaultValue, - DefaultValueSql = source.DefaultValueSql, - IsDestructiveChange = source.IsDestructiveChange, - IsFixedLength = source.IsFixedLength, - IsNullable = source.IsNullable, - IsRowVersion = source.IsRowVersion, - IsStored = source.IsStored, - IsUnicode = source.IsUnicode, - MaxLength = source.MaxLength, - Name = source.Name, - Precision = source.Precision, - Scale = source.Scale, - Table = source.Table, - Schema = source.Schema - }; - - foreach (var annotation in source.GetAnnotations()) - { - result.AddAnnotation(annotation.Name, annotation.Value); - } - - return result; - } - } - - private sealed class TemporalOperationInformation - { - public bool IsTemporalTable { get; set; } - public string? HistoryTableName { get; set; } - public string? HistoryTableSchema { get; set; } - public string? PeriodStartColumnName { get; set; } - public string? PeriodEndColumnName { get; set; } - - public bool DisabledVersioning { get; set; } - public bool DisabledPeriod { get; set; } - - public bool ShouldEnableVersioning { get; set; } - public bool ShouldEnablePeriod { get; set; } - public bool SuppressTransaction { get; set; } - - // Period columns default to HIDDEN. When converting an existing table to temporal, these flags - // capture the user-configured visibility from the period column annotations so EnablePeriod can - // conditionally emit `ALTER COLUMN ... ADD HIDDEN`. - public bool PeriodStartHidden { get; set; } = true; - public bool PeriodEndHidden { get; set; } = true; - } -} +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Globalization; +using System.Text; +using Microsoft.EntityFrameworkCore.SqlServer.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Update.Internal; + +// ReSharper disable once CheckNamespace +namespace Microsoft.EntityFrameworkCore.Migrations; + +/// +/// SQL Server-specific implementation of . +/// +/// +/// +/// The service lifetime is . This means that each +/// instance will use its own instance of this service. +/// The implementation may depend on other services registered with any lifetime. +/// The implementation does not need to be thread-safe. +/// +/// +/// See Database migrations, and +/// Accessing SQL Server and Azure SQL databases with EF Core +/// for more information and examples. +/// +/// +public class SqlServerMigrationsSqlGenerator : MigrationsSqlGenerator +{ + private IReadOnlyList _operations = null!; + private int _variableCounter = -1; + + private readonly ICommandBatchPreparer _commandBatchPreparer; + + /// + /// Creates a new instance. + /// + /// Parameter object containing dependencies for this service. + /// The command batch preparer. + public SqlServerMigrationsSqlGenerator( + MigrationsSqlGeneratorDependencies dependencies, + ICommandBatchPreparer commandBatchPreparer) + : base(dependencies) + => _commandBatchPreparer = commandBatchPreparer; + + /// + /// Generates commands from a list of operations. + /// + /// The operations. + /// The target model which may be if the operations exist without a model. + /// The options to use when generating commands. + /// The list of commands to be executed or scripted. + public override IReadOnlyList Generate( + IReadOnlyList operations, + IModel? model = null, + MigrationsSqlGenerationOptions options = MigrationsSqlGenerationOptions.Default) + { + _operations = operations; + try + { + return base.Generate(RewriteOperations(operations, model, options), model, options); + } + finally + { + _operations = null!; + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// + /// This method uses a double-dispatch mechanism to call the method + /// that is specific to a certain subtype of . Typically database providers + /// will override these specific methods rather than this method. However, providers can override + /// this methods to handle provider-specific operations. + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + switch (operation) + { + case SqlServerCreateDatabaseOperation createDatabaseOperation: + Generate(createDatabaseOperation, model, builder); + break; + case SqlServerDropDatabaseOperation dropDatabaseOperation: + Generate(dropDatabaseOperation, model, builder); + break; + default: + base.Generate(operation, model, builder); + break; + } + } + + /// + protected override void Generate(AddCheckConstraintOperation operation, IModel? model, MigrationCommandListBuilder builder) + => GenerateExecWhenIdempotent(builder, b => base.Generate(operation, model, b)); + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + AddColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate) + { + if (!terminate + && operation.Comment != null) + { + throw new ArgumentException(SqlServerStrings.CannotProduceUnterminatedSQLWithComments(nameof(AddColumnOperation))); + } + + if (IsIdentity(operation)) + { + // NB: This gets added to all added non-nullable columns by MigrationsModelDiffer. We need to suppress + // it, here because SQL Server can't have both IDENTITY and a DEFAULT constraint on the same column. + operation.DefaultValue = null; + } + + var needsExec = Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) + && operation.ComputedColumnSql != null; + if (needsExec) + { + var subBuilder = new MigrationCommandListBuilder(Dependencies); + base.Generate(operation, model, subBuilder, terminate: false); + subBuilder.EndCommand(); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + var command = subBuilder.GetCommandList().Single(); + + builder + .Append("EXEC(") + .Append(stringTypeMapping.GenerateSqlLiteral(command.CommandText)) + .Append(")"); + } + else + { + base.Generate(operation, model, builder, terminate: false); + } + + if (terminate) + { + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + if (operation.Comment != null) + { + AddDescription( + builder, operation.Comment, + operation.Schema, + operation.Table, + operation.Name); + } + + builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + AddForeignKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + AddPrimaryKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + AlterColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + if (operation[RelationalAnnotationNames.ColumnOrder] != operation.OldColumn[RelationalAnnotationNames.ColumnOrder]) + { + Dependencies.MigrationsLogger.ColumnOrderIgnoredWarning(operation); + } + + IEnumerable? indexesToRebuild = null; + var column = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) + ?.Columns.FirstOrDefault(c => c.Name == operation.Name); + + if (operation.ComputedColumnSql != operation.OldColumn.ComputedColumnSql + || operation.IsStored != operation.OldColumn.IsStored) + { + var dropColumnOperation = new DropColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name + }; + if (column != null) + { + dropColumnOperation.AddAnnotations(column.GetAnnotations()); + } + + var addColumnOperation = new AddColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name, + ClrType = operation.ClrType, + ColumnType = operation.ColumnType, + IsUnicode = operation.IsUnicode, + IsFixedLength = operation.IsFixedLength, + MaxLength = operation.MaxLength, + Precision = operation.Precision, + Scale = operation.Scale, + IsRowVersion = operation.IsRowVersion, + IsNullable = operation.IsNullable, + DefaultValue = operation.DefaultValue, + DefaultValueSql = operation.DefaultValueSql, + ComputedColumnSql = operation.ComputedColumnSql, + IsStored = operation.IsStored, + Comment = operation.Comment, + Collation = operation.Collation + }; + addColumnOperation.AddAnnotations(operation.GetAnnotations()); + + // TODO: Use a column rebuild instead + indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); + DropIndexes(indexesToRebuild, builder); + Generate(dropColumnOperation, model, builder, terminate: false); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + Generate(addColumnOperation, model, builder); + CreateIndexes(indexesToRebuild, builder); + builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + + return; + } + + var columnType = operation.ColumnType + ?? GetColumnType( + operation.Schema, + operation.Table, + operation.Name, + operation, + model); + + var narrowed = false; + var oldColumnSupported = IsOldColumnSupported(model); + string? oldType = null; + + // SQL Server can't ALTER COLUMN on a computed column when the expression is unchanged; see #33425. + var computedColumnIsNoOp = operation.ComputedColumnSql != null + && operation.OldColumn.ComputedColumnSql != null + && operation.ComputedColumnSql == operation.OldColumn.ComputedColumnSql + && operation.IsStored == operation.OldColumn.IsStored; + + if (oldColumnSupported && !computedColumnIsNoOp) + { + if (IsIdentity(operation) != IsIdentity(operation.OldColumn)) + { + throw new InvalidOperationException(SqlServerStrings.AlterIdentityColumn); + } + + oldType = operation.OldColumn.ColumnType + ?? GetColumnType( + operation.Schema, + operation.Table, + operation.Name, + operation.OldColumn, + model); + narrowed = columnType != oldType + || operation.Collation != operation.OldColumn.Collation + || operation is { IsNullable: false, OldColumn.IsNullable: true }; + } + + var sparseChanged = ((bool?)operation[SqlServerAnnotationNames.Sparse] ?? false) + != ((bool?)operation.OldColumn[SqlServerAnnotationNames.Sparse] ?? false); + if (narrowed || sparseChanged) + { + indexesToRebuild = GetIndexesToRebuild(column, operation).ToList(); + DropIndexes(indexesToRebuild, builder); + } + + // Handle change of identity seed value + if (IsIdentity(operation) && oldColumnSupported) + { + Check.DebugAssert(IsIdentity(operation.OldColumn), "Unsupported column change to identity"); + + var oldSeed = 1; + if (TryParseIdentitySeedIncrement(operation, out var newSeed, out _) + && (operation.OldColumn[SqlServerAnnotationNames.Identity] is null + || TryParseIdentitySeedIncrement(operation.OldColumn, out oldSeed, out _)) + && newSeed != oldSeed) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + var table = stringTypeMapping.GenerateSqlLiteral( + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)); + + builder + .Append($"DBCC CHECKIDENT({table}, RESEED, {newSeed})") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + + var newAnnotations = operation.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.Identity); + var oldAnnotations = operation.OldColumn.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.Identity); + + var alterStatementNeeded = narrowed + || !oldColumnSupported + || operation.ClrType != operation.OldColumn.ClrType + || columnType != operation.OldColumn.ColumnType + || operation.IsUnicode != operation.OldColumn.IsUnicode + || operation.IsFixedLength != operation.OldColumn.IsFixedLength + || operation.MaxLength != operation.OldColumn.MaxLength + || operation.Precision != operation.OldColumn.Precision + || operation.Scale != operation.OldColumn.Scale + || operation.IsRowVersion != operation.OldColumn.IsRowVersion + || operation.IsNullable != operation.OldColumn.IsNullable + || operation.Collation != operation.OldColumn.Collation + || HasDifferences(newAnnotations, oldAnnotations); + + if (computedColumnIsNoOp) + { + alterStatementNeeded = false; + } + + var (oldDefaultValue, oldDefaultValueSql) = (operation.OldColumn.DefaultValue, operation.OldColumn.DefaultValueSql); + + if (alterStatementNeeded + || !Equals(operation.DefaultValue, oldDefaultValue) + || operation.DefaultValueSql != oldDefaultValueSql) + { + var oldDefaultConstraintName = operation.OldColumn[RelationalAnnotationNames.DefaultConstraintName] as string; + + DropDefaultConstraint(operation.Schema, operation.Table, operation.Name, oldDefaultConstraintName, builder); + (oldDefaultValue, oldDefaultValueSql) = (null, null); + } + + // The column is being made non-nullable. Generate an update statement before doing that, to convert any existing null values to + // the default value (otherwise SQL Server fails). + if (operation is { IsNullable: false, OldColumn.IsNullable: true } + && (operation.DefaultValueSql is not null || operation.DefaultValue is not null)) + { + string defaultValueSql; + if (operation.DefaultValueSql is not null) + { + defaultValueSql = operation.DefaultValueSql; + } + else + { + Check.DebugAssert(operation.DefaultValue is not null); + + var typeMapping = Dependencies.TypeMappingSource.FindMapping(operation.DefaultValue.GetType(), columnType) + ?? Dependencies.TypeMappingSource.GetMappingForValue(operation.DefaultValue); + + defaultValueSql = typeMapping.GenerateSqlLiteral(operation.DefaultValue); + } + + var updateBuilder = new StringBuilder() + .Append("UPDATE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" SET ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" = ") + .Append(defaultValueSql) + .Append(" WHERE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" IS NULL"); + + if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + builder + .Append("EXEC(N'") + .Append(updateBuilder.ToString().TrimEnd('\n', '\r', ';').Replace("'", "''")) + .Append("')"); + } + else + { + builder.Append(updateBuilder.ToString()); + } + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + if (alterStatementNeeded) + { + // SQL Server can't ALTER COLUMN from json to a non JSON type; use rename-add-copy-drop instead. See #38364. + if ((oldType ?? operation.OldColumn.ColumnType) + ?.Equals("json", StringComparison.OrdinalIgnoreCase) + == true + && !columnType.Equals("json", StringComparison.OrdinalIgnoreCase)) + { + AlterColumnFromJson(operation, columnType, model, builder); + } + else + { + AppendAlterColumnDefinition(operation, operation.IsNullable, model, builder); + } + } + + 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, defaultConstraintName, builder); + builder + .Append(" FOR ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + if (operation.OldColumn.Comment != operation.Comment) + { + var dropDescription = operation.OldColumn.Comment != null; + if (dropDescription) + { + DropDescription( + builder, + operation.Schema, + operation.Table, + operation.Name); + } + + if (operation.Comment != null) + { + AddDescription( + builder, operation.Comment, + operation.Schema, + operation.Table, + operation.Name, + omitVariableDeclarations: dropDescription); + } + } + + if (narrowed || sparseChanged) + { + CreateIndexes(indexesToRebuild!, builder); + } + + builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + + private void AlterColumnFromJson( + AlterColumnOperation operation, + string columnType, + IModel? model, + MigrationCommandListBuilder builder) + { + var tempColumnName = "ef_temp_" + operation.Name; + + Rename( + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema) + + "." + + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name), + tempColumnName, + "COLUMN", + builder); + + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" ADD "); + + var addColumnOperation = new AddColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name, + ClrType = operation.ClrType, + ColumnType = operation.ColumnType, + IsUnicode = operation.IsUnicode, + IsFixedLength = operation.IsFixedLength, + MaxLength = operation.MaxLength, + Precision = operation.Precision, + Scale = operation.Scale, + IsRowVersion = operation.IsRowVersion, + IsNullable = true, + Collation = operation.Collation, + Comment = operation.Comment + }; + addColumnOperation.AddAnnotations( + operation.GetAnnotations().Where(a => a.Name != SqlServerAnnotationNames.Identity)); + + ColumnDefinition( + operation.Schema, + operation.Table, + operation.Name, + addColumnOperation, + model, + builder); + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + var updateSql = new StringBuilder() + .Append("UPDATE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" SET ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" = CONVERT(") + .Append(columnType) + .Append(", ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tempColumnName)) + .Append(")") + .ToString(); + + builder + .Append("EXEC(N'") + .Append(updateSql.Replace("'", "''")) + .Append("')"); + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" DROP COLUMN ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tempColumnName)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + if (!operation.IsNullable) + { + AppendAlterColumnDefinition(operation, false, model, builder); + } + } + + private void AppendAlterColumnDefinition( + AlterColumnOperation operation, + bool isNullable, + IModel? model, + MigrationCommandListBuilder builder) + { + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" ALTER COLUMN "); + + // NB: ComputedColumnSql, IsStored, DefaultValue, DefaultValueSql, Comment, ValueGenerationStrategy, and Identity are + // handled elsewhere. Don't copy them here. + var definitionOperation = new AlterColumnOperation + { + Schema = operation.Schema, + Table = operation.Table, + Name = operation.Name, + ClrType = operation.ClrType, + ColumnType = operation.ColumnType, + IsUnicode = operation.IsUnicode, + IsFixedLength = operation.IsFixedLength, + MaxLength = operation.MaxLength, + Precision = operation.Precision, + Scale = operation.Scale, + IsRowVersion = operation.IsRowVersion, + IsNullable = isNullable, + Collation = operation.Collation, + OldColumn = operation.OldColumn + }; + definitionOperation.AddAnnotations( + operation.GetAnnotations().Where(a => a.Name is not SqlServerAnnotationNames.ValueGenerationStrategy + and not SqlServerAnnotationNames.Identity)); + + ColumnDefinition( + operation.Schema, + operation.Table, + operation.Name, + definitionOperation, + model, + builder); + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + RenameIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + if (string.IsNullOrEmpty(operation.Table)) + { + throw new InvalidOperationException(SqlServerStrings.IndexTableRequired); + } + + Rename( + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema) + + "." + + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name), + operation.NewName, + "INDEX", + builder); + builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(RenameSequenceOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + var name = operation.Name; + if (operation.NewName != null + && operation.NewName != name) + { + Rename( + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema), + operation.NewName, + "OBJECT", + builder); + + name = operation.NewName; + } + + if (operation.NewSchema != operation.Schema + && (operation.NewSchema != null + || !HasLegacyRenameOperations(model))) + { + Transfer(operation.NewSchema, operation.Schema, name, builder); + } + + builder.EndCommand(); + } + + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + RestartSequenceOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + builder + .Append("ALTER SEQUENCE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) + .Append(" RESTART"); + + if (operation.StartValue.HasValue) + { + builder + .Append(" WITH ") + .Append(IntegerConstant(operation.StartValue.Value)); + } + + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + EndStatement(builder); + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + CreateTableOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + var hasComments = operation.Comment != null || operation.Columns.Any(c => c.Comment != null); + + if (!terminate && hasComments) + { + throw new ArgumentException(SqlServerStrings.CannotProduceUnterminatedSQLWithComments(nameof(CreateTableOperation))); + } + + var needsExec = false; + + var tableCreationOptions = new List(); + + if ((operation[SqlServerAnnotationNames.IsTemporal] as bool?) == true) + { + var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string + ?? model?.GetDefaultSchema(); + + needsExec = historyTableSchema == null; + var subBuilder = needsExec + ? new MigrationCommandListBuilder(Dependencies) + : builder; + + subBuilder + .Append("CREATE TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)) + .AppendLine(" ("); + + using (subBuilder.Indent()) + { + CreateTableColumns(operation, model, subBuilder); + CreateTableConstraints(operation, model, subBuilder); + subBuilder.AppendLine(","); + var startColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; + var endColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + var start = Dependencies.SqlGenerationHelper.DelimitIdentifier(startColumnName!); + var end = Dependencies.SqlGenerationHelper.DelimitIdentifier(endColumnName!); + subBuilder.AppendLine($"PERIOD FOR SYSTEM_TIME({start}, {end})"); + } + + subBuilder.Append(")"); + + var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; + string historyTable; + if (needsExec) + { + subBuilder + .EndCommand(); + + var execBody = subBuilder.GetCommandList().Single().CommandText.Replace("'", "''"); + + var schemaVariable = Uniquify("@historyTableSchema"); + builder + .AppendLine($"DECLARE {schemaVariable} nvarchar(max) = QUOTENAME(SCHEMA_NAME())") + .Append("EXEC(N'") + .Append(execBody); + + historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!); + tableCreationOptions.Add($"SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + {schemaVariable} + N'.{historyTable})"); + } + else + { + historyTable = Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName!, historyTableSchema); + tableCreationOptions.Add($"SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable})"); + } + } + else + { + base.Generate(operation, model, builder, terminate: false); + } + + var memoryOptimized = IsMemoryOptimized(operation); + if (memoryOptimized) + { + tableCreationOptions.Add("MEMORY_OPTIMIZED = ON"); + } + + if (tableCreationOptions.Count > 0) + { + builder.Append(" WITH ("); + if (tableCreationOptions.Count == 1) + { + builder + .Append(tableCreationOptions[0]) + .Append(")"); + } + else + { + builder.AppendLine(); + + using (builder.Indent()) + { + for (var i = 0; i < tableCreationOptions.Count; i++) + { + builder.Append(tableCreationOptions[i]); + + if (i < tableCreationOptions.Count - 1) + { + builder.Append(","); + } + + builder.AppendLine(); + } + } + + builder.Append(")"); + } + } + + if (needsExec) + { + builder.Append("')"); + } + + if (hasComments) + { + Check.DebugAssert(terminate, "terminate is false but there are comments"); + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + var firstDescription = true; + if (operation.Comment != null) + { + AddDescription(builder, operation.Comment, operation.Schema, operation.Name); + + firstDescription = false; + } + + foreach (var column in operation.Columns) + { + if (column.Comment == null) + { + continue; + } + + AddDescription( + builder, column.Comment, + operation.Schema, + operation.Name, + column.Name, + omitVariableDeclarations: !firstDescription); + + firstDescription = false; + } + + builder.EndCommand(suppressTransaction: memoryOptimized); + } + else if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: memoryOptimized); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + RenameTableOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + var name = operation.Name; + if (operation.NewName != null + && operation.NewName != name) + { + Rename( + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema), + operation.NewName, + "OBJECT", + builder); + + name = operation.NewName; + } + + if (operation.NewSchema != operation.Schema + && (operation.NewSchema != null + || !HasLegacyRenameOperations(model))) + { + Transfer(operation.NewSchema, operation.Schema, name, builder); + } + + builder.EndCommand(); + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropTableOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Name)); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + CreateIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + if (operation[SqlServerAnnotationNames.FullTextIndex] is string keyIndex) + { + GenerateFullTextIndex(keyIndex); + return; + } + + if (operation[SqlServerAnnotationNames.VectorIndexMetric] is string) + { + GenerateVectorIndex(); + return; + } + + if (operation[RelationalAnnotationNames.JsonIndex] is RelationalJsonIndex jsonIndex) + { + GenerateJsonIndex(jsonIndex); + return; + } + + var table = model?.GetRelationalModel().FindTable(operation.Table, operation.Schema); + var hasNullableColumns = operation.Columns.Any(c => table?.FindColumn(c)?.IsNullable != false); + + var memoryOptimized = IsMemoryOptimized(operation, model, operation.Schema, operation.Table); + if (memoryOptimized) + { + builder.Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append(" ADD INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" "); + + if (operation.IsUnique && !hasNullableColumns) + { + builder.Append("UNIQUE "); + } + + IndexTraits(operation, model, builder); + + builder.Append("("); + GenerateIndexColumnList(operation, model, builder); + builder.Append(")"); + } + else + { + var needsLegacyFilter = UseLegacyIndexFilters(operation, model); + var needsExec = Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent) + && (operation.Filter != null + || needsLegacyFilter); + var subBuilder = needsExec + ? new MigrationCommandListBuilder(Dependencies) + : builder; + + base.Generate(operation, model, subBuilder, terminate: false); + + if (needsExec) + { + subBuilder + .EndCommand(); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + var command = subBuilder.GetCommandList().Single(); + + builder + .Append("EXEC(") + .Append(stringTypeMapping.GenerateSqlLiteral(command.CommandText)) + .Append(")"); + } + } + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: memoryOptimized); + } + + void GenerateFullTextIndex(string keyIndex) + { + builder.Append("CREATE FULLTEXT INDEX ON ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append("("); + + var languages = (Dictionary?)operation.FindAnnotation(SqlServerAnnotationNames.FullTextLanguages)?.Value; + + for (var i = 0; i < operation.Columns.Length; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Columns[i])); + + if (languages is not null && languages.TryGetValue(operation.Columns[i], out var language)) + { + builder.Append(" LANGUAGE ").Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(language)); + } + } + + builder.Append(") KEY INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(keyIndex)); + + if (operation[SqlServerAnnotationNames.FullTextCatalog] is string catalog) + { + builder.Append(" ON ").Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(catalog)); + } + + if (operation[SqlServerAnnotationNames.FullTextChangeTracking] is FullTextChangeTracking changeTracking) + { + builder.Append(" WITH CHANGE_TRACKING = "); + builder.Append( + changeTracking switch + { + FullTextChangeTracking.Auto => "AUTO", + FullTextChangeTracking.Manual => "MANUAL", + FullTextChangeTracking.Off => "OFF", + FullTextChangeTracking.OffNoPopulation => "OFF, NO POPULATION", + + _ => throw new UnreachableException(), + }); + } + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + } + + void GenerateVectorIndex() + { + builder.Append("CREATE VECTOR INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" ON ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append("("); + GenerateIndexColumnList(operation, model, builder); + builder.Append(")"); + + IndexOptions(operation, model, builder); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + } + + void GenerateJsonIndex(RelationalJsonIndex jsonIndex) + { + var jsonColumn = jsonIndex.Elements[0].ContainingColumn.Name; + builder.Append("CREATE JSON INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" ON ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)) + .Append("(") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(jsonColumn)) + .Append(") FOR ("); + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + for (var i = 0; i < jsonIndex.Elements.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + var element = jsonIndex.Elements[i]; + // Add a trailing wildcard for the leaf JSON array + var segments = element is IRelationalJsonArray + ? [.. element.Path, StructuredJsonPathSegment.Array] + : element.Path; + builder.Append( + stringTypeMapping.GenerateSqlLiteral( + new StructuredJsonPath(segments, jsonIndex.CollectionIndices?[i]) + .ToString(wildcardForNullIndex: '*'))); + } + + builder.Append(")"); + + IndexOptions(operation, model, builder); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropPrimaryKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(EnsureSchemaOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + if (string.Equals(operation.Name, "dbo", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + builder + .Append("IF SCHEMA_ID(") + .Append(stringTypeMapping.GenerateSqlLiteral(operation.Name)) + .Append(") IS NULL EXEC(") + .Append( + stringTypeMapping.GenerateSqlLiteral( + "CREATE SCHEMA " + + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name) + + Dependencies.SqlGenerationHelper.StatementTerminator)) + .Append(")") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(); + } + + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + CreateSequenceOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + builder + .Append("CREATE SEQUENCE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name, operation.Schema)); + + if (operation.ClrType != typeof(long)) + { + var typeMapping = Dependencies.TypeMappingSource.GetMapping(operation.ClrType); + + builder + .Append(" AS ") + .Append(typeMapping.StoreTypeNameBase); + } + + builder + .Append(" START WITH ") + .Append(IntegerConstant(operation.StartValue)); + + SequenceOptions(operation, model, builder); + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + EndStatement(builder); + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected virtual void Generate( + SqlServerCreateDatabaseOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + builder + .Append("CREATE DATABASE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)); + + if (!string.IsNullOrEmpty(operation.FileName)) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + var fileName = ExpandFileName(operation.FileName); + var name = Path.GetFileNameWithoutExtension(fileName); + + var logFileName = Path.ChangeExtension(fileName, ".ldf"); + var logName = name + "_log"; + + // Match default naming behavior of SQL Server + logFileName = logFileName.Insert(logFileName.Length - ".ldf".Length, "_log"); + + builder + .AppendLine() + .Append("ON (NAME = ") + .Append(stringTypeMapping.GenerateSqlLiteral(name)) + .Append(", FILENAME = ") + .Append(stringTypeMapping.GenerateSqlLiteral(fileName)) + .Append(")") + .AppendLine() + .Append("LOG ON (NAME = ") + .Append(stringTypeMapping.GenerateSqlLiteral(logName)) + .Append(", FILENAME = ") + .Append(stringTypeMapping.GenerateSqlLiteral(logFileName)) + .Append(")"); + } + + if (!string.IsNullOrEmpty(operation.Collation)) + { + builder + .AppendLine() + .Append("COLLATE ") + .Append(operation.Collation); + } + + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true) + .AppendLine("IF SERVERPROPERTY('EngineEdition') <> 5") + .AppendLine("BEGIN"); + + using (builder.Indent()) + { + builder + .Append("ALTER DATABASE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" SET READ_COMMITTED_SNAPSHOT ON") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + builder + .Append("END") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + + private static string ExpandFileName(string fileName) + { + if (fileName.StartsWith("|DataDirectory|", StringComparison.OrdinalIgnoreCase)) + { + var dataDirectory = AppDomain.CurrentDomain.GetData("DataDirectory") as string; + if (string.IsNullOrEmpty(dataDirectory)) + { + dataDirectory = AppDomain.CurrentDomain.BaseDirectory; + } + + fileName = Path.Combine(dataDirectory, fileName["|DataDirectory|".Length..]); + } + + return Path.GetFullPath(fileName); + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected virtual void Generate( + SqlServerDropDatabaseOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + builder + .AppendLine("IF SERVERPROPERTY('EngineEdition') <> 5") + .AppendLine("BEGIN"); + + using (builder.Indent()) + { + builder + .Append("ALTER DATABASE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" SET SINGLE_USER WITH ROLLBACK IMMEDIATE") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + builder + .Append("END") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true) + .Append("DROP DATABASE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + AlterDatabaseOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + if (operation[SqlServerAnnotationNames.EditionOptions] is string editionOptions) + { + var dbVariable = Uniquify("@db_name"); + builder + .AppendLine("BEGIN") + .AppendLine($"DECLARE {dbVariable} nvarchar(max) = QUOTENAME(DB_NAME());") + .AppendLine($"EXEC(N'ALTER DATABASE ' + {dbVariable} + ' MODIFY ( ") + .Append(editionOptions.Replace("'", "''")) + .AppendLine(" );');") + .AppendLine("END") + .AppendLine(); + } + + if (operation.Collation != operation.OldDatabase.Collation) + { + var dbVariable = Uniquify("@db_name"); + builder + .AppendLine("BEGIN") + .AppendLine($"DECLARE {dbVariable} nvarchar(max) = QUOTENAME(DB_NAME());"); + + var collation = operation.Collation; + if (operation.Collation == null) + { + var collationVariable = Uniquify("@defaultCollation"); + builder.AppendLine($"DECLARE {collationVariable} nvarchar(max) = CAST(SERVERPROPERTY('Collation') AS nvarchar(max));"); + collation = "' + " + collationVariable + " + N'"; + } + + builder + .AppendLine($"EXEC(N'ALTER DATABASE ' + {dbVariable} + ' COLLATE {collation};');") + .AppendLine("END") + .AppendLine(); + } + + GenerateFullTextCatalogStatements(operation, builder); + + if (!IsMemoryOptimized(operation)) + { + builder.EndCommand(suppressTransaction: true); + return; + } + + builder.AppendLine("IF SERVERPROPERTY('IsXTPSupported') = 1 AND SERVERPROPERTY('EngineEdition') <> 5"); + using (builder.Indent()) + { + builder + .AppendLine("BEGIN") + .AppendLine("IF NOT EXISTS ("); + using (builder.Indent()) + { + builder + .Append("SELECT 1 FROM [sys].[filegroups] [FG] ") + .Append("JOIN [sys].[database_files] [F] ON [FG].[data_space_id] = [F].[data_space_id] ") + .AppendLine("WHERE [FG].[type] = N'FX' AND [F].[type] = 2)"); + } + + using (builder.Indent()) + { + var dbVariable = Uniquify("@db_name"); + builder + .AppendLine("BEGIN") + .AppendLine("ALTER DATABASE CURRENT SET AUTO_CLOSE OFF;") + .AppendLine($"DECLARE {dbVariable} nvarchar(max) = DB_NAME();") + .AppendLine("DECLARE @fg_name nvarchar(max);") + .AppendLine("SELECT TOP(1) @fg_name = [name] FROM [sys].[filegroups] WHERE [type] = N'FX';") + .AppendLine() + .AppendLine("IF @fg_name IS NULL"); + + using (builder.Indent()) + { + builder + .AppendLine("BEGIN") + .AppendLine($"SET @fg_name = QUOTENAME({dbVariable} + N'_MODFG');") + .AppendLine("EXEC(N'ALTER DATABASE CURRENT ADD FILEGROUP ' + @fg_name + ' CONTAINS MEMORY_OPTIMIZED_DATA;');") + .AppendLine("END"); + } + + var pathVariable = Uniquify("@path"); + builder + .AppendLine() + .AppendLine($"DECLARE {pathVariable} nvarchar(max);") + .Append($"SELECT TOP(1) {pathVariable} = [physical_name] FROM [sys].[database_files] ") + .AppendLine("WHERE charindex('\\', [physical_name]) > 0 ORDER BY [file_id];") + .AppendLine($"IF ({pathVariable} IS NULL)") + .IncrementIndent().AppendLine($"SET {pathVariable} = '\\' + {dbVariable};").DecrementIndent() + .AppendLine() + .AppendLine($"DECLARE @filename nvarchar(max) = right({pathVariable}, charindex('\\', reverse({pathVariable})) - 1);") + .AppendLine( + "SET @filename = REPLACE(left(@filename, len(@filename) - charindex('.', reverse(@filename))), '''', '''''') + N'_MOD';") + .AppendLine( + "DECLARE @new_path nvarchar(max) = REPLACE(CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(max)), '''', '''''') + @filename;") + .AppendLine() + .AppendLine("EXEC(N'"); + + using (builder.Indent()) + { + builder + .AppendLine("ALTER DATABASE CURRENT") + .AppendLine("ADD FILE (NAME=''' + @filename + ''', filename=''' + @new_path + ''')") + .AppendLine("TO FILEGROUP ' + @fg_name + ';')"); + } + + builder.AppendLine("END"); + } + + builder.AppendLine("END"); + } + + builder.AppendLine() + .AppendLine("IF SERVERPROPERTY('IsXTPSupported') = 1") + .AppendLine("EXEC(N'"); + using (builder.Indent()) + { + builder + .AppendLine("ALTER DATABASE CURRENT") + .AppendLine("SET MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT ON;')"); + } + + builder.EndCommand(suppressTransaction: true); + } + + private void GenerateFullTextCatalogStatements( + AlterDatabaseOperation operation, + MigrationCommandListBuilder builder) + { + var oldCatalogs = SqlServerFullTextCatalog.GetFullTextCatalogs(operation.OldDatabase).ToDictionary(c => c.Name, c => c); + var newCatalogs = SqlServerFullTextCatalog.GetFullTextCatalogs(operation).ToDictionary(c => c.Name, c => c); + + // Drop removed catalogs + foreach (var (name, _) in oldCatalogs) + { + if (!newCatalogs.ContainsKey(name)) + { + builder + .Append("DROP FULLTEXT CATALOG ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .AppendLine(); + } + } + + // Create added catalogs + foreach (var (name, catalog) in newCatalogs) + { + if (!oldCatalogs.ContainsKey(name)) + { + builder.Append("CREATE FULLTEXT CATALOG ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)); + + if (!catalog.IsAccentSensitive) + { + builder.Append(" WITH ACCENT_SENSITIVITY = OFF"); + } + + if (catalog.IsDefault) + { + builder.Append(" AS DEFAULT"); + } + + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .AppendLine(); + } + } + + // Alter changed catalogs + foreach (var (name, catalog) in newCatalogs) + { + if (oldCatalogs.TryGetValue(name, out var oldProps)) + { + if (oldProps.IsAccentSensitive != catalog.IsAccentSensitive) + { + builder + .Append("ALTER FULLTEXT CATALOG ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) + .Append(" REBUILD WITH ACCENT_SENSITIVITY = ") + .Append(catalog.IsAccentSensitive ? "ON" : "OFF") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .AppendLine(); + } + + if (!oldProps.IsDefault && catalog.IsDefault) + { + builder + .Append("ALTER FULLTEXT CATALOG ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) + .Append(" AS DEFAULT") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .AppendLine(); + } + } + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(AlterTableOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + if (IsMemoryOptimized(operation) + ^ IsMemoryOptimized(operation.OldTable)) + { + throw new InvalidOperationException(SqlServerStrings.AlterMemoryOptimizedTable); + } + + if (operation.OldTable.Comment != operation.Comment) + { + var dropDescription = operation.OldTable.Comment != null; + if (dropDescription) + { + DropDescription(builder, operation.Schema, operation.Name); + } + + if (operation.Comment != null) + { + AddDescription( + builder, + operation.Comment, + operation.Schema, + operation.Name, + omitVariableDeclarations: dropDescription); + } + } + + builder.EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Name)); + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropForeignKeyOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + base.Generate(operation, model, builder, terminate: false); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropIndexOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate) + { + if (string.IsNullOrEmpty(operation.Table)) + { + throw new InvalidOperationException(SqlServerStrings.IndexTableRequired); + } + + if (operation[SqlServerAnnotationNames.FullTextIndex] is string) + { + builder + .Append("DROP FULLTEXT INDEX ON ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table!, operation.Schema)); + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: true); + } + + return; + } + + var memoryOptimized = IsMemoryOptimized(operation, model, operation.Schema, operation.Table); + if (memoryOptimized) + { + builder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table!, operation.Schema)) + .Append(" DROP INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)); + } + else + { + builder + .Append("DROP INDEX ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name)) + .Append(" ON ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)); + } + + if (terminate) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: memoryOptimized); + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + DropColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + 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) + { + builder + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(suppressTransaction: IsMemoryOptimized(operation, model, operation.Schema, operation.Table)); + } + } + + /// + /// Builds commands for the given + /// by making calls on the given . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate( + RenameColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + Rename( + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema) + + "." + + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Name), + operation.NewName, + "COLUMN", + builder); + builder.EndCommand(); + } + + private enum ParsingState + { + Normal, + InBlockComment, + InSquareBrackets, + InDoubleQuotes, + InQuotes + } + + /// + /// Builds commands for the given by making calls on the given + /// , and then terminates the final command. + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + protected override void Generate(SqlOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + if (Options.HasFlag(MigrationsSqlGenerationOptions.Script)) + { + builder.Append(operation.Sql); + if (!operation.Sql.EndsWith('\n')) + { + builder.AppendLine(); + } + + EndStatement(builder, operation.SuppressTransaction); + return; + } + + var preBatched = operation.Sql + .Replace("\\\n", "") + .Replace("\\\r\n", "") + .Split(["\r\n", "\n"], StringSplitOptions.None); + + var state = ParsingState.Normal; + var batchBuilder = new StringBuilder(); + foreach (var line in preBatched) + { + var trimmed = line.TrimStart(); + + if (state == ParsingState.Normal + && trimmed.StartsWith("GO", StringComparison.OrdinalIgnoreCase) + && (trimmed.Length == 2 + || char.IsWhiteSpace(trimmed[2]))) + { + var batch = batchBuilder.ToString(); + batchBuilder.Clear(); + + var count = trimmed.Length >= 4 + && int.TryParse(trimmed.AsSpan(3), out var specifiedCount) + ? specifiedCount + : 1; + + for (var j = 0; j < count; j++) + { + AppendBatch(batch); + } + } + else + { + for (var i = 0; i < trimmed.Length; i++) + { + var c = trimmed[i]; + var next = i + 1 < trimmed.Length ? trimmed[i + 1] : '\0'; + + if (state == ParsingState.Normal && c == '-' && next == '-') + { + goto LineEnd; + } + + state = state switch + { + ParsingState.Normal when c == '\'' => ParsingState.InQuotes, + ParsingState.Normal when c == '[' => ParsingState.InSquareBrackets, + ParsingState.Normal when c == '"' => ParsingState.InDoubleQuotes, + ParsingState.Normal when c == '/' && next == '*' => ConsumeAndReturn(ref i, ParsingState.InBlockComment), + + ParsingState.InQuotes when c == '\'' => ParsingState.Normal, + + ParsingState.InSquareBrackets when c == ']' && next == ']' => ConsumeAndReturn( + ref i, ParsingState.InSquareBrackets), + ParsingState.InSquareBrackets when c == ']' => ParsingState.Normal, + + ParsingState.InDoubleQuotes when c == '"' => ParsingState.Normal, + + ParsingState.InBlockComment when c == '*' && next == '/' => ConsumeAndReturn(ref i, ParsingState.Normal), + + _ => state + }; + } + + LineEnd: + batchBuilder.AppendLine(line); + } + } + + AppendBatch(batchBuilder.ToString()); + + ParsingState ConsumeAndReturn(ref int index, ParsingState newState) + { + index++; + return newState; + } + + void AppendBatch(string batch) + { + if (!string.IsNullOrWhiteSpace(batch)) + { + builder.Append(batch); + EndStatement(builder, operation.SuppressTransaction); + } + } + } + + /// + /// Builds commands for the given by making calls on the given + /// . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to build the commands. + /// Indicates whether or not to terminate the command after generating SQL for the operation. + protected override void Generate( + InsertDataOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool terminate = true) + { + GenerateIdentityInsert(builder, operation, on: true, model); + + var sqlBuilder = new StringBuilder(); + + var modificationCommands = GenerateModificationCommands(operation, model).ToList(); + var updateSqlGenerator = (ISqlServerUpdateSqlGenerator)Dependencies.UpdateSqlGenerator; + + foreach (var batch in _commandBatchPreparer.CreateCommandBatches(modificationCommands, moreCommandSets: true)) + { + updateSqlGenerator.AppendBulkInsertOperation(sqlBuilder, batch.ModificationCommands, commandPosition: 0); + } + + if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + builder + .Append("EXEC(N'") + .Append(sqlBuilder.ToString().TrimEnd('\n', '\r', ';').Replace("'", "''")) + .Append("')") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + else + { + builder.Append(sqlBuilder.ToString()); + } + + GenerateIdentityInsert(builder, operation, on: false, model); + + if (terminate) + { + builder.EndCommand(); + } + } + + private void GenerateIdentityInsert(MigrationCommandListBuilder builder, InsertDataOperation operation, bool on, IModel? model) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + builder + .Append("IF EXISTS (SELECT * FROM [sys].[identity_columns] WHERE") + .Append(" [name] IN (") + .Append(string.Join(", ", operation.Columns.Select(stringTypeMapping.GenerateSqlLiteral))) + .Append(") AND [object_id] = OBJECT_ID(") + .Append( + stringTypeMapping.GenerateSqlLiteral( + Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema ?? model?.GetDefaultSchema()))) + .AppendLine("))"); + + using (builder.Indent()) + { + builder + .Append("SET IDENTITY_INSERT ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema ?? model?.GetDefaultSchema())) + .Append(on ? " ON" : " OFF") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + + /// + protected override void Generate(DeleteDataOperation operation, IModel? model, MigrationCommandListBuilder builder) + => GenerateExecWhenIdempotent(builder, b => base.Generate(operation, model, b)); + + /// + 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, + string name, + SequenceOperation operation, + IModel? model, + MigrationCommandListBuilder builder, + bool forAlter) + { + builder + .Append(" INCREMENT BY ") + .Append(IntegerConstant(operation.IncrementBy)); + + if (operation.MinValue.HasValue) + { + builder + .Append(" MINVALUE ") + .Append(IntegerConstant(operation.MinValue.Value)); + } + else if (forAlter) + { + builder.Append(" NO MINVALUE"); + } + + if (operation.MaxValue.HasValue) + { + builder + .Append(" MAXVALUE ") + .Append(IntegerConstant(operation.MaxValue.Value)); + } + else if (forAlter) + { + builder.Append(" NO MAXVALUE"); + } + + builder.Append(operation.IsCyclic ? " CYCLE" : " NO CYCLE"); + } + + /// + /// Generates a SQL fragment for a column definition for the given column metadata. + /// + /// The schema that contains the table, or to use the default schema. + /// The table that contains the column. + /// The column name. + /// The column metadata. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected override void ColumnDefinition( + string? schema, + string table, + string name, + ColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + if (operation.ComputedColumnSql != null) + { + ComputedColumnDefinition(schema, table, name, operation, model, builder); + + return; + } + + var columnType = operation.ColumnType ?? GetColumnType(schema, table, name, operation, model); + builder + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)) + .Append(" ") + .Append(columnType); + + if (operation.Collation != null) + { + // SQL Server collation docs: https://learn.microsoft.com/sql/relational-databases/collations/collation-and-unicode-support + + // The default behavior in MigrationsSqlGenerator is to quote collation names, but SQL Server does not support that. + // Instead, make sure the collation name only contains a restricted set of characters. + foreach (var c in operation.Collation) + { + if (!char.IsLetterOrDigit(c) && c != '_') + { + throw new InvalidOperationException(SqlServerStrings.InvalidCollationName(operation.Collation)); + } + } + + builder + .Append(" COLLATE ") + .Append(operation.Collation); + } + + if (operation[SqlServerAnnotationNames.Sparse] is bool isSparse && isSparse) + { + builder.Append(" SPARSE"); + } + + var isPeriodStartColumn = (operation[SqlServerAnnotationNames.TemporalIsPeriodStartColumn] as bool?) == true; + var isPeriodEndColumn = (operation[SqlServerAnnotationNames.TemporalIsPeriodEndColumn] as bool?) == true; + + if (isPeriodStartColumn || isPeriodEndColumn) + { + builder.Append(" GENERATED ALWAYS AS ROW "); + builder.Append(isPeriodStartColumn ? "START" : "END"); + + // Defaults to true to preserve backward compatibility - the period columns have always been hidden. + // Set to false via TemporalPeriodPropertyBuilder.IsHidden(false). + var hidden = operation[SqlServerAnnotationNames.IsHidden] as bool? ?? true; + if (hidden) + { + builder.Append(" HIDDEN"); + } + } + + 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, defaultConstraintName, builder); + } + + var identity = operation[SqlServerAnnotationNames.Identity] as string; + if (identity != null + || (operation[SqlServerAnnotationNames.ValueGenerationStrategy] as SqlServerValueGenerationStrategy?) + == SqlServerValueGenerationStrategy.IdentityColumn) + { + builder.Append(" IDENTITY"); + + if (!string.IsNullOrEmpty(identity) + && identity != "1, 1") + { + builder + .Append("(") + .Append(identity) + .Append(")"); + } + } + } + + /// + /// Generates a SQL fragment for a computed column definition for the given column metadata. + /// + /// The schema that contains the table, or to use the default schema. + /// The table that contains the column. + /// The column name. + /// The column metadata. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected override void ComputedColumnDefinition( + string? schema, + string table, + string name, + ColumnOperation operation, + IModel? model, + MigrationCommandListBuilder builder) + { + builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)); + + builder + .Append(" AS ") + .Append(operation.ComputedColumnSql!); + + if (operation.Collation != null) + { + builder + .Append(" COLLATE ") + .Append(operation.Collation); + } + + if (operation.IsStored == true) + { + builder.Append(" PERSISTED"); + } + } + + /// + /// Generates a rename. + /// + /// The old name. + /// The new name. + /// The command builder to use to build the commands. + protected virtual void Rename( + string name, + string newName, + MigrationCommandListBuilder builder) + => Rename(name, newName, /*type:*/ null, builder); + + /// + /// Generates a rename. + /// + /// The old name. + /// The new name. + /// If not , then appends literal for type of object being renamed (e.g. column or index.) + /// The command builder to use to build the commands. + protected virtual void Rename( + string name, + string newName, + string? type, + MigrationCommandListBuilder builder) + { + // Types come from https://learn.microsoft.com/sql/relational-databases/system-stored-procedures/sp-rename-transact-sql + var typeMappingSource = Dependencies.TypeMappingSource; + var nameTypeMapping = typeMappingSource.FindMapping(typeof(string), "nvarchar(776)")!; + + builder + .Append("EXEC sp_rename ") + .Append(nameTypeMapping.GenerateSqlLiteral(name)) + .Append(", ") + .Append(nameTypeMapping.GenerateSqlLiteral(newName)); + + if (type != null) + { + builder + .Append(", ") + .Append(typeMappingSource.FindMapping(typeof(string), "varchar(13)")!.GenerateSqlLiteral(type)); + } + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + /// + /// Generates a transfer from one schema to another. + /// + /// The schema to transfer to. + /// The schema to transfer from. + /// The name of the item to transfer. + /// The command builder to use to build the commands. + protected virtual void Transfer( + string? newSchema, + string? schema, + string name, + MigrationCommandListBuilder builder) + { + if (newSchema == null) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + var schemaVariable = Uniquify("@defaultSchema"); + builder + .AppendLine($"DECLARE {schemaVariable} nvarchar(max) = QUOTENAME(SCHEMA_NAME());") + .Append("EXEC(") + .Append($"N'ALTER SCHEMA ' + {schemaVariable} + ") + .Append( + stringTypeMapping.GenerateSqlLiteral( + " TRANSFER " + Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema) + ";")) + .AppendLine(");"); + } + else + { + builder + .Append("ALTER SCHEMA ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(newSchema)) + .Append(" TRANSFER ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name, schema)) + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + + /// + /// Generates a SQL fragment for traits of an index from a , + /// , or . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected override void IndexTraits(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + if (operation[SqlServerAnnotationNames.Clustered] is bool clustered) + { + builder.Append(clustered ? "CLUSTERED " : "NONCLUSTERED "); + } + } + + /// + /// Generates a SQL fragment for extras (filter, included columns, options) of an index from a . + /// + /// The operation. + /// The target model which may be if the operations exist without a model. + /// The command builder to use to add the SQL fragment. + protected override void IndexOptions(MigrationOperation operation, IModel? model, MigrationCommandListBuilder builder) + { + if (operation[SqlServerAnnotationNames.Include] is IReadOnlyList includeColumns + && includeColumns.Count > 0) + { + builder.Append(" INCLUDE ("); + for (var i = 0; i < includeColumns.Count; i++) + { + builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(includeColumns[i])); + + if (i != includeColumns.Count - 1) + { + builder.Append(", "); + } + } + + builder.Append(")"); + } + + if (operation is CreateIndexOperation createIndexOperation) + { + if (!string.IsNullOrEmpty(createIndexOperation.Filter)) + { + builder + .Append(" WHERE ") + .Append(createIndexOperation.Filter); + } + else if (UseLegacyIndexFilters(createIndexOperation, model)) + { + var table = model?.GetRelationalModel().FindTable(createIndexOperation.Table, createIndexOperation.Schema); + var nullableColumns = createIndexOperation.Columns + .Where(c => table?.FindColumn(c)?.IsNullable != false) + .ToList(); + + builder.Append(" WHERE "); + for (var i = 0; i < nullableColumns.Count; i++) + { + if (i != 0) + { + builder.Append(" AND "); + } + + builder + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(nullableColumns[i])) + .Append(" IS NOT NULL"); + } + } + } + + var options = new List(); + + if (operation[SqlServerAnnotationNames.FillFactor] is int fillFactor) + { + options.Add("FILLFACTOR = " + fillFactor); + } + + if (operation[SqlServerAnnotationNames.CreatedOnline] is bool isOnline && isOnline) + { + options.Add("ONLINE = ON"); + } + + if (operation[SqlServerAnnotationNames.SortInTempDb] is bool sortInTempDb && sortInTempDb) + { + options.Add("SORT_IN_TEMPDB = ON"); + } + + if (operation[SqlServerAnnotationNames.DataCompression] is DataCompressionType dataCompressionType) + { + options.Add( + "DATA_COMPRESSION = " + + dataCompressionType switch + { + DataCompressionType.None => "NONE", + DataCompressionType.Row => "ROW", + DataCompressionType.Page => "PAGE", + + _ => throw new UnreachableException(), + }); + } + + // When this CreateIndexOperation was rewritten from a Drop+Create pair (an index facet + // changed and the index needs to be recreated), emit DROP_EXISTING = ON so SQL Server + // atomically replaces the index without leaving the table un-indexed during the rebuild. + // See #35067. + if (operation[SqlServerAnnotationNames.UseDropExisting] is true) + { + options.Add("DROP_EXISTING = ON"); + } + + // Vector index options. + // Note that the metric facet is mandatory, and used to determine if the index is a vector index. + if (operation[SqlServerAnnotationNames.VectorIndexMetric] is string vectorMetric) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping("varchar(max)"); + + options.Add("METRIC = " + stringTypeMapping.GenerateSqlLiteral(vectorMetric)); + + if (operation[SqlServerAnnotationNames.VectorIndexType] is string vectorType) + { + options.Add("TYPE = " + stringTypeMapping.GenerateSqlLiteral(vectorType)); + } + } + + if (options.Count > 0) + { + builder + .Append(" WITH (") + .Append(string.Join(", ", options)) + .Append(")"); + } + } + + /// + /// Generates a SQL fragment for the given referential action. + /// + /// The referential action. + /// The command builder to use to add the SQL fragment. + protected override void ForeignKeyAction(ReferentialAction referentialAction, MigrationCommandListBuilder builder) + { + if (referentialAction == ReferentialAction.Restrict) + { + builder.Append("NO ACTION"); + } + else + { + base.ForeignKeyAction(referentialAction, builder); + } + } + + /// + /// Generates a SQL fragment to drop default constraints for a column. + /// + /// 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"); + + builder + .Append("DECLARE ") + .Append(variable) + .AppendLine(" nvarchar(max);") + .Append("SELECT ") + .Append(variable) + .AppendLine(" = QUOTENAME(OBJECT_NAME([c].[default_object_id]))") + .AppendLine("FROM [sys].[columns] [c]") + .Append("WHERE [c].[object_id] = OBJECT_ID(") + .Append( + stringTypeMapping.GenerateSqlLiteral( + Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema))) + .Append(") AND [c].[name] = ") + .Append(stringTypeMapping.GenerateSqlLiteral(columnName)) + .AppendLine(";") + .Append("IF ") + .Append(variable) + .Append(" IS NOT NULL EXEC(") + .Append( + stringTypeMapping.GenerateSqlLiteral( + "ALTER TABLE " + Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema) + " DROP CONSTRAINT ")) + .Append(" + ") + .Append(variable) + .Append(" + '") + .Append(Dependencies.SqlGenerationHelper.StatementTerminator) + .Append("')") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + /// + /// Gets the list of indexes that need to be rebuilt when the given column is changing. + /// + /// The column. + /// The operation which may require a rebuild. + /// The list of indexes affected. + protected virtual IEnumerable GetIndexesToRebuild( + IColumn? column, + MigrationOperation currentOperation) + { + if (column == null) + { + yield break; + } + + var table = column.Table; + var createIndexOperations = _operations.SkipWhile(o => o != currentOperation).Skip(1) + .OfType().Where(o => o.Table == table.Name && o.Schema == table.Schema).ToList(); + foreach (var index in table.Indexes) + { + var indexName = index.Name; + if (createIndexOperations.Any(o => o.Name == indexName)) + { + continue; + } + + if (index.Columns.Any(c => c == column)) + { + yield return index; + } + else if (index[SqlServerAnnotationNames.Include] is IReadOnlyList includeColumns + && includeColumns.Contains(column.Name)) + { + yield return index; + } + } + } + + /// + /// Generates SQL to drop the given indexes. + /// + /// The indexes to drop. + /// The command builder to use to build the commands. + protected virtual void DropIndexes( + IEnumerable indexes, + MigrationCommandListBuilder builder) + { + foreach (var index in indexes) + { + var table = index.Table; + var operation = new DropIndexOperation + { + Schema = table.Schema, + Table = table.Name, + Name = index.Name + }; + operation.AddAnnotations(index.GetAnnotations()); + + Generate(operation, table.Model.Model, builder, terminate: false); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + + /// + /// Generates SQL to create the given indexes. + /// + /// The indexes to create. + /// The command builder to use to build the commands. + protected virtual void CreateIndexes( + IEnumerable indexes, + MigrationCommandListBuilder builder) + { + foreach (var index in indexes) + { + Generate(CreateIndexOperation.CreateFrom(index), index.Table.Model.Model, builder, terminate: false); + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + + /// + /// Generates add commands for descriptions on tables and columns. + /// + /// The command builder to use to build the commands. + /// The new description to be applied. + /// The schema of the table. + /// The name of the table. + /// The name of the column. + /// + /// Indicates whether the variable declarations should be omitted. + /// + protected virtual void AddDescription( + MigrationCommandListBuilder builder, + string description, + string? schema, + string table, + string? column = null, + bool omitVariableDeclarations = false) + { + var schemaLiteral = Uniquify("@defaultSchema", increase: !omitVariableDeclarations); + var descriptionVariable = Uniquify("@description", increase: false); + + if (schema == null) + { + if (!omitVariableDeclarations) + { + builder.Append($"DECLARE {schemaLiteral} AS sysname") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + builder.Append($"SET {schemaLiteral} = SCHEMA_NAME()") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + else + { + schemaLiteral = Literal(schema); + } + + if (!omitVariableDeclarations) + { + builder.Append($"DECLARE {descriptionVariable} AS sql_variant") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + builder.Append($"SET {descriptionVariable} = {Literal(description)}") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + builder + .Append("EXEC sp_addextendedproperty 'MS_Description', ") + .Append(descriptionVariable) + .Append(", 'SCHEMA', ") + .Append(schemaLiteral) + .Append(", 'TABLE', ") + .Append(Literal(table)); + + if (column != null) + { + builder + .Append(", 'COLUMN', ") + .Append(Literal(column)); + } + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + string Literal(string s) + => SqlLiteral(s); + + static string SqlLiteral(string value) + { + var builder = new StringBuilder(); + + var start = 0; + int i; + int length; + var openApostrophe = false; + var lastConcatStartPoint = 0; + var concatCount = 1; + var concatStartList = new List(); + for (i = 0; i < value.Length; i++) + { + var lineFeed = value[i] == '\n'; + var carriageReturn = value[i] == '\r'; + var apostrophe = value[i] == '\''; + if (lineFeed || carriageReturn || apostrophe) + { + length = i - start; + if (length != 0) + { + if (!openApostrophe) + { + AddConcatOperatorIfNeeded(); + builder.Append("N\'"); + openApostrophe = true; + } + + builder.Append(value.AsSpan().Slice(start, length)); + } + + if (lineFeed || carriageReturn) + { + if (openApostrophe) + { + builder.Append('\''); + openApostrophe = false; + } + + AddConcatOperatorIfNeeded(); + builder + .Append("NCHAR(") + .Append(lineFeed ? "10" : "13") + .Append(')'); + } + else if (apostrophe) + { + if (!openApostrophe) + { + AddConcatOperatorIfNeeded(); + builder.Append("N'"); + openApostrophe = true; + } + + builder.Append("''"); + } + + start = i + 1; + } + } + + length = i - start; + if (length != 0) + { + if (!openApostrophe) + { + AddConcatOperatorIfNeeded(); + builder.Append("N\'"); + openApostrophe = true; + } + + builder.Append(value.AsSpan().Slice(start, length)); + } + + if (openApostrophe) + { + builder.Append('\''); + } + + for (var j = concatStartList.Count - 1; j >= 0; j--) + { + builder.Insert(concatStartList[j], "CONCAT("); + builder.Append(')'); + } + + if (builder.Length == 0) + { + builder.Append("N''"); + } + + var result = builder.ToString(); + + return result; + + void AddConcatOperatorIfNeeded() + { + if (builder.Length != 0) + { + builder.Append(", "); + concatCount++; + + if (concatCount == 2) + { + concatStartList.Add(lastConcatStartPoint); + } + + if (concatCount == 254) + { + lastConcatStartPoint = builder.Length; + concatCount = 1; + } + } + } + } + } + + /// + /// Generates drop commands for descriptions on tables and columns. + /// + /// The command builder to use to build the commands. + /// The schema of the table. + /// The name of the table. + /// The name of the column. + /// + /// Indicates whether the variable declarations should be omitted. + /// + protected virtual void DropDescription( + MigrationCommandListBuilder builder, + string? schema, + string table, + string? column = null, + bool omitVariableDeclarations = false) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + var schemaLiteral = Uniquify("@defaultSchema", increase: !omitVariableDeclarations); + var descriptionVariable = Uniquify("@description", increase: false); + if (schema == null) + { + if (!omitVariableDeclarations) + { + builder.Append($"DECLARE {schemaLiteral} AS sysname") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + builder.Append($"SET {schemaLiteral} = SCHEMA_NAME()") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + } + else + { + schemaLiteral = Literal(schema); + } + + if (!omitVariableDeclarations) + { + builder.Append($"DECLARE {descriptionVariable} AS sql_variant") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + } + + builder + .Append("EXEC sp_dropextendedproperty 'MS_Description', 'SCHEMA', ") + .Append(schemaLiteral) + .Append(", 'TABLE', ") + .Append(Literal(table)); + + if (column != null) + { + builder + .Append(", 'COLUMN', ") + .Append(Literal(column)); + } + + builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + string Literal(string s) + => stringTypeMapping.GenerateSqlLiteral(s); + } + + /// + /// Checks whether or not should have a filter generated for it by + /// Migrations. + /// + /// The index creation operation. + /// The target model. + /// if a filter should be generated. + protected virtual bool UseLegacyIndexFilters(CreateIndexOperation operation, IModel? model) + => (!TryGetVersion(model, out var version) || VersionComparer.Compare(version, "2.0.0") < 0) + && operation.Filter is null + && operation.IsUnique + && operation[SqlServerAnnotationNames.Clustered] is null or false + && model?.GetRelationalModel().FindTable(operation.Table, operation.Schema) is var table + && operation.Columns.Any(c => table?.FindColumn(c)?.IsNullable != false); + + private static string IntegerConstant(long value) + => string.Format(CultureInfo.InvariantCulture, "{0}", value); + + private static bool IsMemoryOptimized(Annotatable annotatable, IModel? model, string? schema, string tableName) + => annotatable[SqlServerAnnotationNames.MemoryOptimized] as bool? + ?? ((model?.GetRelationalModel().FindTable(tableName, schema)?[SqlServerAnnotationNames.MemoryOptimized] as bool?) == true); + + private static bool IsMemoryOptimized(Annotatable annotatable) + => (annotatable[SqlServerAnnotationNames.MemoryOptimized] as bool?) == true; + + private static bool IsIdentity(ColumnOperation operation) + => operation[SqlServerAnnotationNames.Identity] != null + || (operation[SqlServerAnnotationNames.ValueGenerationStrategy] as SqlServerValueGenerationStrategy?) + == SqlServerValueGenerationStrategy.IdentityColumn; + + private static void RemoveIdentityAnnotations(ColumnOperation operation) + { + operation.RemoveAnnotation(SqlServerAnnotationNames.Identity); + + if ((operation[SqlServerAnnotationNames.ValueGenerationStrategy] as SqlServerValueGenerationStrategy?) + == SqlServerValueGenerationStrategy.IdentityColumn) + { + operation.RemoveAnnotation(SqlServerAnnotationNames.ValueGenerationStrategy); + } + } + + private static bool TryParseIdentitySeedIncrement(ColumnOperation operation, out int seed, out int increment) + { + if (operation[SqlServerAnnotationNames.Identity] is string seedIncrement + && seedIncrement.Split(",") is [var seedString, var incrementString] + && int.TryParse(seedString, out var seedParsed) + && int.TryParse(incrementString, out var incrementParsed)) + { + (seed, increment) = (seedParsed, incrementParsed); + return true; + } + + (seed, increment) = (0, 0); + return false; + } + + private void GenerateExecWhenIdempotent( + MigrationCommandListBuilder builder, + Action generate) + { + if (Options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + var subBuilder = new MigrationCommandListBuilder(Dependencies); + generate(subBuilder); + + var command = subBuilder.GetCommandList().Single(); + builder + .Append("EXEC(N'") + .Append(command.CommandText.TrimEnd('\n', '\r', ';').Replace("'", "''")) + .Append("')") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator) + .EndCommand(command.TransactionSuppressed); + + return; + } + + generate(builder); + } + + private static bool HasDifferences(IEnumerable source, IEnumerable target) + { + var targetAnnotations = target.ToDictionary(a => a.Name); + + var count = 0; + foreach (var sourceAnnotation in source) + { + if (!targetAnnotations.TryGetValue(sourceAnnotation.Name, out var targetAnnotation) + || !Equals(sourceAnnotation.Value, targetAnnotation.Value)) + { + return true; + } + + count++; + } + + return count != targetAnnotations.Count; + } + + private string Uniquify(string variableName, bool increase = true) + { + if (increase) + { + _variableCounter++; + } + + return _variableCounter == 0 ? variableName : variableName + _variableCounter; + } + + private IReadOnlyList RewriteDropAndCreateIndexAsDropExisting( + IReadOnlyList migrationOperations, + IModel? model) + { + // The differ produces a DropIndexOperation + CreateIndexOperation pair when an index facet + // changes (e.g. fill factor, sort order, uniqueness, filter, columns). On SQL Server the + // pair can be collapsed into a single `CREATE INDEX ... WITH (DROP_EXISTING = ON)` which + // is more efficient: queries can continue using the old index while the new one is being + // built, instead of going un-indexed during the drop. See #35067. + // + // The collapse is only safe when the drop is IMMEDIATELY followed by the matching create. + // If anything sits between them (e.g. an AlterColumnOperation on the indexed column, which + // SQL Server only allows once the index is gone), removing the drop would re-introduce the + // old index before the intermediate operation runs and break the migration. The rewrite is + // also limited to non-special indexes (no memory-optimized, full-text or vector index, + // since those use different syntax/restrictions). + + // Scan for adjacent (DropIndex, CreateIndex) pairs with matching identity. + var dropsToRemove = new HashSet(); + for (var i = 0; i < migrationOperations.Count - 1; i++) + { + if (migrationOperations[i] is not DropIndexOperation dropOperation + || dropOperation.Table is null + || migrationOperations[i + 1] is not CreateIndexOperation createOperation + || createOperation.Table is null + || dropOperation.Name != createOperation.Name + || dropOperation.Table != createOperation.Table + || dropOperation.Schema != createOperation.Schema) + { + continue; + } + + // operations[i + 1] is the matching create, so the next operation cannot be a + // DropIndexOperation and can't start another pair; advance past it. + i++; + + // Skip special index types that don't support DROP_EXISTING. + if (createOperation[SqlServerAnnotationNames.FullTextIndex] is not null + || createOperation[SqlServerAnnotationNames.VectorIndexMetric] is not null + || IsMemoryOptimized(createOperation, model, createOperation.Schema, createOperation.Table)) + { + continue; + } + + createOperation.AddAnnotation(SqlServerAnnotationNames.UseDropExisting, true); + dropsToRemove.Add(dropOperation); + } + + if (dropsToRemove.Count == 0) + { + return migrationOperations; + } + + var resultOperations = new List(migrationOperations.Count - dropsToRemove.Count); + foreach (var migrationOperation in migrationOperations) + { + if (migrationOperation is DropIndexOperation dropOperation && dropsToRemove.Contains(dropOperation)) + { + continue; + } + + resultOperations.Add(migrationOperation); + } + + return resultOperations; + } + + private IReadOnlyList FixLegacyTemporalAnnotations(IReadOnlyList migrationOperations) + { + // short-circuit for non-temporal migrations (which is the majority) + if (migrationOperations.All(o => (o[SqlServerAnnotationNames.IsTemporal] as bool?) != true)) + { + return migrationOperations; + } + + var resultOperations = new List(migrationOperations.Count); + foreach (var migrationOperation in migrationOperations) + { + var isTemporal = (migrationOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; + if (!isTemporal) + { + resultOperations.Add(migrationOperation); + continue; + } + + switch (migrationOperation) + { + case CreateTableOperation createTableOperation: + + foreach (var column in createTableOperation.Columns) + { + NormalizeTemporalAnnotationsForAddColumnOperation(column); + } + + resultOperations.Add(migrationOperation); + break; + + case AddColumnOperation addColumnOperation: + NormalizeTemporalAnnotationsForAddColumnOperation(addColumnOperation); + resultOperations.Add(addColumnOperation); + break; + + case AlterColumnOperation alterColumnOperation: + RemoveLegacyTemporalColumnAnnotations(alterColumnOperation); + RemoveLegacyTemporalColumnAnnotations(alterColumnOperation.OldColumn); + if (!CanSkipAlterColumnOperation(alterColumnOperation, alterColumnOperation.OldColumn)) + { + resultOperations.Add(alterColumnOperation); + } + + break; + + case DropColumnOperation dropColumnOperation: + RemoveLegacyTemporalColumnAnnotations(dropColumnOperation); + resultOperations.Add(dropColumnOperation); + break; + + case RenameColumnOperation renameColumnOperation: + RemoveLegacyTemporalColumnAnnotations(renameColumnOperation); + resultOperations.Add(renameColumnOperation); + break; + + default: + resultOperations.Add(migrationOperation); + break; + } + } + + return resultOperations; + + static void NormalizeTemporalAnnotationsForAddColumnOperation(AddColumnOperation addColumnOperation) + { + var periodStartColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; + var periodEndColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + if (periodStartColumnName == addColumnOperation.Name) + { + addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true); + } + else if (periodEndColumnName == addColumnOperation.Name) + { + addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true); + } + + RemoveLegacyTemporalColumnAnnotations(addColumnOperation); + } + + static void RemoveLegacyTemporalColumnAnnotations(MigrationOperation operation) + { + operation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName); + operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName); + } + + static bool CanSkipAlterColumnOperation(ColumnOperation column, ColumnOperation oldColumn) + => ColumnPropertiesAreTheSame(column, oldColumn) && AnnotationsAreTheSame(column, oldColumn); + + // don't compare name, table or schema - they are not being set in the model differ (since they should always be the same) + static bool ColumnPropertiesAreTheSame(ColumnOperation column, ColumnOperation oldColumn) + => column.ClrType == oldColumn.ClrType + && column.Collation == oldColumn.Collation + && column.ColumnType == oldColumn.ColumnType + && column.Comment == oldColumn.Comment + && column.ComputedColumnSql == oldColumn.ComputedColumnSql + && Equals(column.DefaultValue, oldColumn.DefaultValue) + && column.DefaultValueSql == oldColumn.DefaultValueSql + && column.IsDestructiveChange == oldColumn.IsDestructiveChange + && column.IsFixedLength == oldColumn.IsFixedLength + && column.IsNullable == oldColumn.IsNullable + && column.IsReadOnly == oldColumn.IsReadOnly + && column.IsRowVersion == oldColumn.IsRowVersion + && column.IsStored == oldColumn.IsStored + && column.IsUnicode == oldColumn.IsUnicode + && column.MaxLength == oldColumn.MaxLength + && column.Precision == oldColumn.Precision + && column.Scale == oldColumn.Scale; + + static bool AnnotationsAreTheSame(ColumnOperation column, ColumnOperation oldColumn) + { + var columnAnnotations = column.GetAnnotations().ToList(); + var oldColumnAnnotations = oldColumn.GetAnnotations().ToList(); + + return columnAnnotations.Count == oldColumnAnnotations.Count + && columnAnnotations.Zip(oldColumnAnnotations) + .All(x => x.First.Name == x.Second.Name + && StructuralComparisons.StructuralEqualityComparer.Equals(x.First.Value, x.Second.Value)); + } + } + + private IReadOnlyList RewriteOperations( + IReadOnlyList migrationOperations, + IModel? model, + MigrationsSqlGenerationOptions options) + { + migrationOperations = FixLegacyTemporalAnnotations(migrationOperations); + migrationOperations = RewriteDropAndCreateIndexAsDropExisting(migrationOperations, model); + + var operations = new List(); + var availableSchemas = new List(); + + // we need to know temporal information for all the tables involved in the migration + // problem is, the temporal information is stored only on table operations and not column operations + // if migration operation doesn't contain the table operation, or the table operation comes later + // we don't know what we should do + // to fix that, we loop through all the operations and extract initial temporal state for relevant tables + // if we don't encounter any table operations, then we can take information from the model + // since migration hasn't changed it at all - be we can only know that after looping though all ops + // once we have the initial state of the table, we can update it each time we encounter a table operation + // and we can use what we stored when dealing with all other operations (that don't contain temporal annotations themselves) + var temporalTableInformationMap = new Dictionary<(string TableName, string? Schema), TemporalOperationInformation>(); + var missingTemporalTableInformation = new List<(string TableName, string? Schema)>(); + + foreach (var operation in migrationOperations) + { + switch (operation) + { + case CreateTableOperation createTableOperation: + { + var tableName = createTableOperation.Name; + var rawSchema = createTableOperation.Schema; + var schema = rawSchema ?? model?.GetDefaultSchema(); + if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) + { + var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, createTableOperation); + temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; + } + + // no need to remove from missingTemporalTableInformation - CreateTable should be first operation for this table + // so there can't be entry for it in missingTemporalTableInformation (they are added by other/earlier operations on that table) + // the only possibility is that we had a table before, dropped it and now creating a new table with the same name + // but in this case we would have generated the necessary information from the DropTableOperation + // and also removed the missingTemporalTableInformation entry if there was one before + break; + } + + case DropTableOperation dropTableOperation: + { + var tableName = dropTableOperation.Name; + var rawSchema = dropTableOperation.Schema; + var schema = rawSchema ?? model?.GetDefaultSchema(); + if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) + { + var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, dropTableOperation); + temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; + } + + missingTemporalTableInformation.Remove((tableName, rawSchema)); + break; + } + + case RenameTableOperation renameTableOperation: + { + var tableName = renameTableOperation.Name; + var rawSchema = renameTableOperation.Schema; + var schema = rawSchema ?? model?.GetDefaultSchema(); + var newTableName = renameTableOperation.NewName!; + var newRawSchema = renameTableOperation.NewSchema; + var newSchema = newRawSchema ?? model?.GetDefaultSchema(); + + var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation); + if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) + { + temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; + } + + // we still need to check here - table with the new name could have existed before and have been deleted + // we want to preserve the original temporal info of that deleted table + if (!temporalTableInformationMap.ContainsKey((newTableName, newRawSchema))) + { + temporalTableInformationMap[(newTableName, newRawSchema)] = temporalTableInformation; + } + + missingTemporalTableInformation.Remove((tableName, rawSchema)); + missingTemporalTableInformation.Remove((newTableName, newRawSchema)); + + break; + } + + case AlterTableOperation alterTableOperation: + { + var tableName = alterTableOperation.Name; + var rawSchema = alterTableOperation.Schema; + var schema = rawSchema ?? model?.GetDefaultSchema(); + if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema))) + { + // we create the temporal info based on the OLD table here - we want the initial state + var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, alterTableOperation.OldTable); + + // The period-column hidden flags reflect the user's intent for the NEW state of the table, + // not the old state, so override them from the AlterTable operation itself when present. + if (alterTableOperation[SqlServerAnnotationNames.TemporalPeriodStartHidden] is bool startHidden) + { + temporalTableInformation.PeriodStartHidden = startHidden; + } + + if (alterTableOperation[SqlServerAnnotationNames.TemporalPeriodEndHidden] is bool endHidden) + { + temporalTableInformation.PeriodEndHidden = endHidden; + } + + temporalTableInformationMap[(tableName, rawSchema)] = temporalTableInformation; + } + + missingTemporalTableInformation.Remove((tableName, schema)); + break; + } + + default: + { + if (operation is ITableMigrationOperation tableMigrationOperation) + { + var tableName = tableMigrationOperation.Table; + var rawSchema = tableMigrationOperation.Schema; + if (!temporalTableInformationMap.ContainsKey((tableName, rawSchema)) + && !missingTemporalTableInformation.Contains((tableName, rawSchema))) + { + missingTemporalTableInformation.Add((tableName, rawSchema)); + } + } + + break; + } + } + } + + // fill the missing temporal information from Relational Model - it's the second best source we have + // if we can't figure out proper temporal info from table annotations, + // and we don't have it in relational model (for whatever reason) we assume table is not temporal + // this last step is purely defensive and shouldn't happen in real situations + foreach (var (TableName, Schema) in missingTemporalTableInformation) + { + var table = model?.GetRelationalModel().FindTable(TableName, Schema)!; + if (table != null) + { + var schema = Schema ?? model?.GetDefaultSchema(); + + var temporalTableInformation = BuildTemporalInformationFromMigrationOperation(schema, table); + temporalTableInformationMap[(TableName, Schema)] = temporalTableInformation; + } + else + { + temporalTableInformationMap[(TableName, Schema)] = new TemporalOperationInformation + { + IsTemporalTable = false, + HistoryTableName = null, + HistoryTableSchema = null, + PeriodStartColumnName = null, + PeriodEndColumnName = null + }; + } + } + + var historyTables = new HashSet<(string Name, string? Schema)>( + temporalTableInformationMap.Values + .Where(t => t.IsTemporalTable && t.HistoryTableName != null) + .Select(t => (t.HistoryTableName!, t.HistoryTableSchema))); + + if (model != null) + { + foreach (var table in model.GetRelationalModel().Tables) + { + if ((table[SqlServerAnnotationNames.IsTemporal] as bool?) == true + && table[SqlServerAnnotationNames.TemporalHistoryTableName] is string modelHistoryTableName) + { + var modelHistoryTableSchema = + table[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string; + historyTables.Add((modelHistoryTableName, modelHistoryTableSchema)); + } + } + } + + // now we do proper processing - for table operations we look at the annotations on them + // and continuously update the stored temporal info as the table is being modified + // for column (and other) operations we don't have annotations on them, so we look into the + // information we stored in the initial pass and updated in when processing table ops that happened earlier + foreach (var operation in migrationOperations) + { + if (operation is EnsureSchemaOperation ensureSchemaOperation) + { + availableSchemas.Add(ensureSchemaOperation.Name); + } + + if (operation is not ITableMigrationOperation tableMigrationOperation) + { + operations.Add(operation); + continue; + } + + var tableName = tableMigrationOperation.Table; + var rawSchema = tableMigrationOperation.Schema; + + var suppressTransaction = IsMemoryOptimized(operation, model, rawSchema, tableName); + + var schema = rawSchema ?? model?.GetDefaultSchema(); + + TemporalOperationInformation temporalInformation; + if (operation is CreateTableOperation) + { + // for create table we always generate new temporal information from the operation itself + // just in case there was a table with that name before that got deleted/renamed + // also, temporal state (disabled versioning etc.) should always reset when creating a table + temporalInformation = BuildTemporalInformationFromMigrationOperation(schema, operation); + temporalTableInformationMap[(tableName, rawSchema)] = temporalInformation; + } + else + { + temporalInformation = temporalTableInformationMap[(tableName, rawSchema)]; + } + + switch (operation) + { + case CreateTableOperation createTableOperation: + { + // for create table we always generate new temporal information from the operation itself + // just in case there was a table with that name before that got deleted/renamed + // this shouldn't happen as we re-use existing tables rather than drop/recreate + // but we are being extra defensive here + // and also, temporal state (disabled versioning etc.) should always reset when creating a table + temporalInformation = BuildTemporalInformationFromMigrationOperation(schema, createTableOperation); + + if (temporalInformation.IsTemporalTable + && temporalInformation.HistoryTableSchema != schema + && temporalInformation.HistoryTableSchema != null + && !availableSchemas.Contains(temporalInformation.HistoryTableSchema)) + { + operations.Add(new EnsureSchemaOperation { Name = temporalInformation.HistoryTableSchema }); + availableSchemas.Add(temporalInformation.HistoryTableSchema); + } + + operations.Add(operation); + + break; + } + + case DropTableOperation dropTableOperation: + { + var isTemporalTable = (dropTableOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; + if (isTemporalTable) + { + // if we don't have temporal information, but we know table is temporal + // (based on the annotation found on the operation itself) + // we assume that versioning must be disabled, if we have temporal info we can check properly + if (temporalInformation is null || !temporalInformation.DisabledVersioning) + { + AddDisableVersioningOperation(tableName, schema, suppressTransaction); + } + + if (temporalInformation is not null) + { + temporalInformation.ShouldEnableVersioning = false; + temporalInformation.ShouldEnablePeriod = false; + } + + operations.Add(operation); + + var historyTableName = dropTableOperation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; + var historyTableSchema = + dropTableOperation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? schema; + var dropHistoryTableOperation = new DropTableOperation { Name = historyTableName!, Schema = historyTableSchema }; + operations.Add(dropHistoryTableOperation); + } + else + { + operations.Add(operation); + } + + // we removed the table, so we no longer need it's temporal information + // there will be no more operations involving this table + temporalTableInformationMap.Remove((tableName, schema)); + + break; + } + + case RenameTableOperation renameTableOperation: + { + temporalInformation ??= BuildTemporalInformationFromMigrationOperation(schema, renameTableOperation); + + var isTemporalTable = (renameTableOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; + if (isTemporalTable) + { + DisableVersioning( + tableName, + schema, + temporalInformation, + suppressTransaction, + shouldEnableVersioning: true); + } + + operations.Add(operation); + + // since table was renamed, update entry in the temporal info map + temporalTableInformationMap[(renameTableOperation.NewName!, renameTableOperation.NewSchema)] = temporalInformation; + temporalTableInformationMap.Remove((tableName, schema)); + + break; + } + + case AlterTableOperation alterTableOperation: + { + var isTemporalTable = (alterTableOperation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; + var historyTableName = alterTableOperation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; + var historyTableSchema = alterTableOperation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? schema; + var periodStartColumnName = alterTableOperation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; + var periodEndColumnName = alterTableOperation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + + var oldIsTemporalTable = (alterTableOperation.OldTable[SqlServerAnnotationNames.IsTemporal] as bool?) == true; + var oldHistoryTableName = + alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableName] as string; + var oldHistoryTableSchema = + alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string + ?? alterTableOperation.OldTable.Schema + ?? model?[RelationalAnnotationNames.DefaultSchema] as string; + + if (isTemporalTable) + { + if (!oldIsTemporalTable) + { + // converting from regular table to temporal table - enable period and versioning at the end + // other temporal information (history table, period columns etc) is added below + temporalInformation.ShouldEnablePeriod = true; + temporalInformation.ShouldEnableVersioning = true; + } + else + { + // changing something within temporal table + if (oldHistoryTableName != historyTableName + || oldHistoryTableSchema != historyTableSchema) + { + if (historyTableSchema != null + && !availableSchemas.Contains(historyTableSchema)) + { + operations.Add(new EnsureSchemaOperation { Name = historyTableSchema }); + availableSchemas.Add(historyTableSchema); + } + + operations.Add( + new RenameTableOperation + { + Name = oldHistoryTableName!, + Schema = oldHistoryTableSchema, + NewName = historyTableName, + NewSchema = historyTableSchema + }); + + temporalInformation.HistoryTableName = historyTableName; + temporalInformation.HistoryTableSchema = historyTableSchema; + } + } + } + else + { + if (oldIsTemporalTable) + { + // converting from temporal table to regular table + var oldPeriodStartColumnName = + alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; + var oldPeriodEndColumnName = + alterTableOperation.OldTable[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + + DisableVersioning( + tableName, + schema, + temporalInformation, + suppressTransaction, + shouldEnableVersioning: null); + + if (!temporalInformation.DisabledPeriod) + { + DisablePeriod(tableName, schema, temporalInformation, suppressTransaction); + } + + if (oldHistoryTableName != null) + { + operations.Add(new DropTableOperation { Name = oldHistoryTableName, Schema = oldHistoryTableSchema }); + } + + // also clear any pending versioning/period, that would be switched on at the end + // we don't need it now that the table is no longer temporal + temporalInformation.ShouldEnableVersioning = false; + temporalInformation.ShouldEnablePeriod = false; + } + } + + temporalInformation.IsTemporalTable = isTemporalTable; + temporalInformation.HistoryTableName = historyTableName; + temporalInformation.HistoryTableSchema = historyTableSchema; + temporalInformation.PeriodStartColumnName = periodStartColumnName; + temporalInformation.PeriodEndColumnName = periodEndColumnName; + + if (isTemporalTable && historyTableName != null) + { + historyTables.Add((historyTableName, historyTableSchema)); + } + + operations.Add(operation); + break; + } + + case AddColumnOperation addColumnOperation: + { + // when adding a period column, we need to add it as a normal column first, and only later enable period + // removing the period information now, so that when we generate SQL that adds the column we won't be making them + // auto generated as period it won't work, unless period is enabled but we can't enable period without adding the + // columns first - chicken and egg + if (temporalInformation.IsTemporalTable) + { + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + addColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); + + // model differ adds default value, but for period end we need to replace it with the correct one - + // DateTime.MaxValue + if (addColumnOperation.Name == temporalInformation.PeriodEndColumnName) + { + addColumnOperation.DefaultValue = DateTime.MaxValue; + } + + var isSparse = (addColumnOperation[SqlServerAnnotationNames.Sparse] as bool?) == true; + var isComputed = addColumnOperation.ComputedColumnSql != null; + + if (isSparse || isComputed) + { + DisableVersioning( + tableName, + schema, + temporalInformation, + suppressTransaction, + shouldEnableVersioning: true); + } + + // when adding sparse column to temporal table, we need to disable versioning. + // This is because it may be the case that HistoryTable is using compression (by default) + // and the add column operation fails in that situation + // in order to make it work we need to disable versioning (if we haven't done it already) + // and de-compress the HistoryTable + if (isSparse) + { + DecompressTable( + temporalInformation.HistoryTableName!, temporalInformation.HistoryTableSchema, suppressTransaction); + } + + if (addColumnOperation.ComputedColumnSql != null) + { + DisableVersioning( + tableName, + schema, + temporalInformation, + suppressTransaction, + shouldEnableVersioning: true); + } + + operations.Add(addColumnOperation); + + // when adding (non-period) column to an existing temporal table we need to check if we have disabled versioning + // due to some other operations in the same migration (e.g. delete column) + // if so, we need to also add the same column to history table + if (addColumnOperation.Name != temporalInformation.PeriodStartColumnName + && addColumnOperation.Name != temporalInformation.PeriodEndColumnName + && temporalInformation.DisabledVersioning) + { + var addHistoryTableColumnOperation = CopyColumnOperation(addColumnOperation); + addHistoryTableColumnOperation.Table = temporalInformation.HistoryTableName!; + addHistoryTableColumnOperation.Schema = temporalInformation.HistoryTableSchema; + + if (addHistoryTableColumnOperation.ComputedColumnSql != null) + { + // computed columns are not allowed inside HistoryTables + // but the historical computed value will be copied over to the non-computed counterpart, + // as long as their names and types (including nullability) match + // so we remove ComputedColumnSql info, so that the column in history table "appears normal" + addHistoryTableColumnOperation.ComputedColumnSql = null; + } + + // identity columns are not allowed inside HistoryTables + RemoveIdentityAnnotations(addHistoryTableColumnOperation); + + operations.Add(addHistoryTableColumnOperation); + } + } + else + { + // identity columns are not allowed inside HistoryTables + if (historyTables.Contains((tableName, schema))) + { + RemoveIdentityAnnotations(addColumnOperation); + } + + operations.Add(addColumnOperation); + } + + break; + } + + case DropColumnOperation dropColumnOperation: + { + if (temporalInformation.IsTemporalTable) + { + var droppingPeriodColumn = dropColumnOperation.Name == temporalInformation.PeriodStartColumnName + || dropColumnOperation.Name == temporalInformation.PeriodEndColumnName; + + // if we are dropping non-period column, we should enable versioning at the end. + // When dropping period column there is no need - we are removing the versioning for this table altogether + DisableVersioning( + tableName, + schema, + temporalInformation, + suppressTransaction, + shouldEnableVersioning: droppingPeriodColumn ? null : true); + + if (droppingPeriodColumn && !temporalInformation.DisabledPeriod) + { + DisablePeriod(tableName, schema, temporalInformation, suppressTransaction); + + // if we remove the period columns, it means we will be dropping the table + // also or at least convert it back to regular - no need to enable period later + temporalInformation.ShouldEnablePeriod = false; + } + + operations.Add(operation); + + if (!droppingPeriodColumn) + { + operations.Add( + new DropColumnOperation + { + Name = dropColumnOperation.Name, + Table = temporalInformation.HistoryTableName!, + Schema = temporalInformation.HistoryTableSchema + }); + } + } + else + { + operations.Add(operation); + } + + break; + } + + case RenameColumnOperation renameColumnOperation: + { + operations.Add(renameColumnOperation); + + // if we disabled period for the temporal table and now we are renaming the column, + // we need to also rename this same column in history table + if (temporalInformation.IsTemporalTable + && temporalInformation.DisabledVersioning + && temporalInformation.ShouldEnableVersioning) + { + var renameHistoryTableColumnOperation = new RenameColumnOperation + { + IsDestructiveChange = renameColumnOperation.IsDestructiveChange, + Name = renameColumnOperation.Name, + NewName = renameColumnOperation.NewName, + Table = temporalInformation.HistoryTableName!, + Schema = temporalInformation.HistoryTableSchema + }; + + operations.Add(renameHistoryTableColumnOperation); + } + + break; + } + + case AlterColumnOperation alterColumnOperation: + { + // we can remove temporal annotations, they don't make a difference when it comes to + // generating ALTER COLUMN operations and could just muddy the waters + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + alterColumnOperation.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn); + alterColumnOperation.OldColumn.RemoveAnnotation(SqlServerAnnotationNames.IsHidden); + + if (temporalInformation.IsTemporalTable) + { + if (alterColumnOperation.OldColumn.ComputedColumnSql != alterColumnOperation.ComputedColumnSql) + { + throw new NotSupportedException( + SqlServerStrings.TemporalMigrationModifyingComputedColumnNotSupported( + alterColumnOperation.Name, + alterColumnOperation.Table)); + } + + // for alter column operation converting column from nullable to non-nullable in the temporal table + // we must disable versioning in order to properly handle it + // specifically, switching values in history table from null to the default value + var changeToNonNullable = alterColumnOperation.OldColumn.IsNullable + && !alterColumnOperation.IsNullable; + + // for alter column converting to sparse we also need to disable versioning + // in case HistoryTable is compressed (so that we can de-compress it) + var changeToSparse = (alterColumnOperation.OldColumn[SqlServerAnnotationNames.Sparse] as bool?) != true + && (alterColumnOperation[SqlServerAnnotationNames.Sparse] as bool?) == true; + + // for alter column removing default value we also need to disable versioning + // because the default constraint needs to be removed from both main and history tables + var removingDefaultValue = (alterColumnOperation.OldColumn.DefaultValue is not null + || alterColumnOperation.OldColumn.DefaultValueSql is not null) + && alterColumnOperation.DefaultValue is null + && alterColumnOperation.DefaultValueSql is null; + + if (changeToNonNullable || changeToSparse || removingDefaultValue) + { + DisableVersioning( + tableName!, + schema, + temporalInformation, + suppressTransaction, + shouldEnableVersioning: true); + } + + if (changeToSparse) + { + DecompressTable( + temporalInformation.HistoryTableName!, temporalInformation.HistoryTableSchema, suppressTransaction); + } + + operations.Add(alterColumnOperation); + + // when modifying a period column, we need to perform the operations as a normal column first, and only later enable period + // removing the period information now, so that when we generate SQL that modifies the column we won't be making them auto generated as period + // (making column auto generated is not allowed in ALTER COLUMN statement) + // in later operation we enable the period and the period columns get set to auto generated automatically + // + // if the column is not period we just remove temporal information - it's no longer needed and could affect the generated sql + // we will generate all the necessary operations involved with temporal tables here + if (temporalInformation.DisabledVersioning && temporalInformation.ShouldEnableVersioning) + { + var alterHistoryTableColumn = CopyColumnOperation(alterColumnOperation); + alterHistoryTableColumn.Table = temporalInformation.HistoryTableName!; + alterHistoryTableColumn.Schema = temporalInformation.HistoryTableSchema; + alterHistoryTableColumn.OldColumn = CopyColumnOperation(alterColumnOperation.OldColumn); + alterHistoryTableColumn.OldColumn.Table = temporalInformation.HistoryTableName!; + alterHistoryTableColumn.OldColumn.Schema = temporalInformation.HistoryTableSchema; + + // identity columns are not allowed inside HistoryTables + RemoveIdentityAnnotations(alterHistoryTableColumn); + RemoveIdentityAnnotations(alterHistoryTableColumn.OldColumn); + + operations.Add(alterHistoryTableColumn); + } + } + else + { + // identity columns are not allowed inside HistoryTables + if (historyTables.Contains((tableName, schema))) + { + RemoveIdentityAnnotations(alterColumnOperation); + RemoveIdentityAnnotations(alterColumnOperation.OldColumn); + } + + operations.Add(alterColumnOperation); + } + + break; + } + + case DropPrimaryKeyOperation: + case AddPrimaryKeyOperation: + if (temporalInformation.IsTemporalTable) + { + DisableVersioning( + tableName!, + schema, + temporalInformation, + suppressTransaction, + shouldEnableVersioning: true); + } + + operations.Add(operation); + break; + + default: + operations.Add(operation); + break; + } + } + + foreach (var temporalInformation in temporalTableInformationMap.Where(x => x.Value.ShouldEnablePeriod)) + { + EnablePeriod( + temporalInformation.Key.TableName, + temporalInformation.Key.Schema, + temporalInformation.Value.PeriodStartColumnName!, + temporalInformation.Value.PeriodEndColumnName!, + temporalInformation.Value.PeriodStartHidden, + temporalInformation.Value.PeriodEndHidden, + temporalInformation.Value.SuppressTransaction); + } + + foreach (var temporalInformation in temporalTableInformationMap.Where(x => x.Value.ShouldEnableVersioning)) + { + EnableVersioning( + temporalInformation.Key.TableName, + temporalInformation.Key.Schema, + temporalInformation.Value.HistoryTableName!, + temporalInformation.Value.HistoryTableSchema, + temporalInformation.Value.SuppressTransaction); + } + + return operations; + + static TemporalOperationInformation BuildTemporalInformationFromMigrationOperation( + string? schema, + IAnnotatable operation) + { + var isTemporalTable = (operation[SqlServerAnnotationNames.IsTemporal] as bool?) == true; + var historyTableName = operation[SqlServerAnnotationNames.TemporalHistoryTableName] as string; + var historyTableSchema = operation[SqlServerAnnotationNames.TemporalHistoryTableSchema] as string ?? schema; + var periodStartColumnName = operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string; + var periodEndColumnName = operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string; + + // Period columns default to HIDDEN; the annotation is only present when explicitly configured visible. + var periodStartHidden = operation[SqlServerAnnotationNames.TemporalPeriodStartHidden] as bool? ?? true; + var periodEndHidden = operation[SqlServerAnnotationNames.TemporalPeriodEndHidden] as bool? ?? true; + + return new TemporalOperationInformation + { + IsTemporalTable = isTemporalTable, + HistoryTableName = historyTableName, + HistoryTableSchema = historyTableSchema, + PeriodStartColumnName = periodStartColumnName, + PeriodEndColumnName = periodEndColumnName, + PeriodStartHidden = periodStartHidden, + PeriodEndHidden = periodEndHidden + }; + } + + void DisableVersioning( + string tableName, + string? schema, + TemporalOperationInformation temporalInformation, + bool suppressTransaction, + bool? shouldEnableVersioning) + { + if (!temporalInformation.DisabledVersioning + && !temporalInformation.ShouldEnableVersioning) + { + temporalInformation.DisabledVersioning = true; + + AddDisableVersioningOperation(tableName, schema, suppressTransaction); + + if (shouldEnableVersioning != null) + { + temporalInformation.ShouldEnableVersioning = shouldEnableVersioning.Value; + if (shouldEnableVersioning.Value) + { + temporalInformation.SuppressTransaction = suppressTransaction; + } + } + } + } + + void AddDisableVersioningOperation(string tableName, string? schema, bool suppressTransaction) + => operations.Add( + new SqlOperation + { + Sql = new StringBuilder() + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema)) + .AppendLine(" SET (SYSTEM_VERSIONING = OFF)") + .ToString(), + SuppressTransaction = suppressTransaction + }); + + void EnableVersioning(string table, string? schema, string historyTableName, string? historyTableSchema, bool suppressTransaction) + { + var stringBuilder = new StringBuilder(); + + string? schemaVariable = null; + if (historyTableSchema == null) + { + schemaVariable = Uniquify("@historyTableSchema"); + // need to run command using EXEC to inject default schema + stringBuilder.AppendLine($"DECLARE {schemaVariable} nvarchar(max) = QUOTENAME(SCHEMA_NAME())"); + stringBuilder.Append("EXEC(N'"); + } + + var historyTable = historyTableSchema != null + ? Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName, historyTableSchema) + : Dependencies.SqlGenerationHelper.DelimitIdentifier(historyTableName); + + stringBuilder + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)); + + if (historyTableSchema != null) + { + stringBuilder.AppendLine($" SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = {historyTable}))"); + } + else + { + stringBuilder.AppendLine( + $" SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = ' + {schemaVariable} + '.{historyTable}))')"); + } + + operations.Add( + new SqlOperation { Sql = stringBuilder.ToString(), SuppressTransaction = suppressTransaction }); + } + + void DisablePeriod( + string table, + string? schema, + TemporalOperationInformation temporalInformation, + bool suppressTransaction) + { + temporalInformation.DisabledPeriod = true; + + operations.Add( + new SqlOperation + { + Sql = new StringBuilder() + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) + .AppendLine(" DROP PERIOD FOR SYSTEM_TIME") + .ToString(), + SuppressTransaction = suppressTransaction + }); + } + + void EnablePeriod( + string table, + string? schema, + string periodStartColumnName, + string periodEndColumnName, + bool periodStartHidden, + bool periodEndHidden, + bool suppressTransaction) + { + var addPeriodSql = new StringBuilder() + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) + .Append(" ADD PERIOD FOR SYSTEM_TIME (") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName)) + .Append(", ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName)) + .Append(')') + .ToString(); + + if (options.HasFlag(MigrationsSqlGenerationOptions.Idempotent)) + { + addPeriodSql = new StringBuilder() + .Append("EXEC(N'") + .Append(addPeriodSql.Replace("'", "''")) + .Append("')") + .ToString(); + } + + operations.Add( + new SqlOperation { Sql = addPeriodSql, SuppressTransaction = suppressTransaction }); + + // Period columns are HIDDEN by default. Skip the `ADD HIDDEN` ALTER when the column was + // configured visible via TemporalPeriodPropertyBuilder.IsHidden(false). + if (periodStartHidden) + { + operations.Add( + new SqlOperation + { + Sql = new StringBuilder() + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) + .Append(" ALTER COLUMN ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodStartColumnName)) + .Append(" ADD HIDDEN") + .ToString(), + SuppressTransaction = suppressTransaction + }); + } + + if (periodEndHidden) + { + operations.Add( + new SqlOperation + { + Sql = new StringBuilder() + .Append("ALTER TABLE ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(table, schema)) + .Append(" ALTER COLUMN ") + .Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(periodEndColumnName)) + .Append(" ADD HIDDEN") + .ToString(), + SuppressTransaction = suppressTransaction + }); + } + } + + void DecompressTable(string tableName, string? schema, bool suppressTransaction) + { + var stringTypeMapping = Dependencies.TypeMappingSource.GetMapping(typeof(string)); + + var decompressTableCommand = new StringBuilder() + .Append("IF EXISTS (") + .Append("SELECT 1 FROM [sys].[tables] [t] ") + .Append("INNER JOIN [sys].[partitions] [p] ON [t].[object_id] = [p].[object_id] ") + .Append($"WHERE [t].[name] = '{tableName}' "); + + if (schema != null) + { + decompressTableCommand.Append($"AND [t].[schema_id] = schema_id('{schema}') "); + } + + decompressTableCommand.AppendLine("AND data_compression <> 0)") + .Append("EXEC(") + .Append( + stringTypeMapping.GenerateSqlLiteral( + "ALTER TABLE " + + Dependencies.SqlGenerationHelper.DelimitIdentifier(tableName, schema) + + " REBUILD PARTITION = ALL WITH (DATA_COMPRESSION = NONE)" + + Dependencies.SqlGenerationHelper.StatementTerminator)) + .Append(")") + .AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator); + + operations.Add( + new SqlOperation { Sql = decompressTableCommand.ToString(), SuppressTransaction = suppressTransaction }); + } + + static TOperation CopyColumnOperation(ColumnOperation source) + where TOperation : ColumnOperation, new() + { + var result = new TOperation + { + ClrType = source.ClrType, + Collation = source.Collation, + ColumnType = source.ColumnType, + Comment = source.Comment, + ComputedColumnSql = source.ComputedColumnSql, + DefaultValue = source.DefaultValue, + DefaultValueSql = source.DefaultValueSql, + IsDestructiveChange = source.IsDestructiveChange, + IsFixedLength = source.IsFixedLength, + IsNullable = source.IsNullable, + IsRowVersion = source.IsRowVersion, + IsStored = source.IsStored, + IsUnicode = source.IsUnicode, + MaxLength = source.MaxLength, + Name = source.Name, + Precision = source.Precision, + Scale = source.Scale, + Table = source.Table, + Schema = source.Schema + }; + + foreach (var annotation in source.GetAnnotations()) + { + result.AddAnnotation(annotation.Name, annotation.Value); + } + + return result; + } + } + + private sealed class TemporalOperationInformation + { + public bool IsTemporalTable { get; set; } + public string? HistoryTableName { get; set; } + public string? HistoryTableSchema { get; set; } + public string? PeriodStartColumnName { get; set; } + public string? PeriodEndColumnName { get; set; } + + public bool DisabledVersioning { get; set; } + public bool DisabledPeriod { get; set; } + + public bool ShouldEnableVersioning { get; set; } + public bool ShouldEnablePeriod { get; set; } + public bool SuppressTransaction { get; set; } + + // Period columns default to HIDDEN. When converting an existing table to temporal, these flags + // capture the user-configured visibility from the period column annotations so EnablePeriod can + // conditionally emit `ALTER COLUMN ... ADD HIDDEN`. + public bool PeriodStartHidden { get; set; } = true; + public bool PeriodEndHidden { get; set; } = true; + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs index c16835862c0..87ca8e094fa 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Migrations/MigrationsSqlServerTest.cs @@ -1959,6 +1959,40 @@ FROM [sys].[columns] [c] """); } + [Fact] + public virtual async Task Alter_column_make_sparse_with_index() + { + await Test( + builder => builder.Entity( + "People", + e => + { + e.Property("SomeProperty"); + e.HasIndex("SomeProperty"); + }), + builder => { }, + builder => builder.Entity("People").Property("SomeProperty").IsSparse(), + model => + { + var table = Assert.Single(model.Tables); + var column = Assert.Single(table.Columns); + Assert.True((bool?)column[SqlServerAnnotationNames.Sparse]); + Assert.Single(table.Indexes); + }); + + AssertSql( + """ +DROP INDEX [IX_People_SomeProperty] ON [People]; +DECLARE @var nvarchar(max); +SELECT @var = QUOTENAME(OBJECT_NAME([c].[default_object_id])) +FROM [sys].[columns] [c] +WHERE [c].[object_id] = OBJECT_ID(N'[People]') AND [c].[name] = N'SomeProperty'; +IF @var IS NOT NULL EXEC(N'ALTER TABLE [People] DROP CONSTRAINT ' + @var + ';'); +ALTER TABLE [People] ALTER COLUMN [SomeProperty] nvarchar(450) SPARSE NULL; +CREATE INDEX [IX_People_SomeProperty] ON [People] ([SomeProperty]); +"""); + } + public override async Task Drop_column() { await base.Drop_column();