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();