Skip to content

Commit d231ccb

Browse files
committed
Fix to #35108 - Temporal table migration regression from EF Core 8 to 9
In 9 we changed the way we process migration of temporal tables. One of the changes was drastically reducing the number of annotations for columns which are part of temporal tables. This however caused regressions for cases where migration code was created using EF8 (and containing those legacy annotations) but then executed using EF9 tooling. Specifically, extra annotations were generating a number of superfluous Alter Column operations (which were only modifying those annotations). In EF8 we had logic to weed out those operations, but it was removed in EF9. Fix is to remove all the legacy annotations on column operations before we start processing them. We no longer rely on them, but rather use annotations on Table operations and/or relational model. The only exception is CreateColumnOperation, so for it we convert old annotations to TemporalIsPeriodStartColumn and TemporalIsPeriodEndColumn where appropriate. Also, we are bringing back logic from EF8 which removed unnecessary AlterColumnOperations if the old and new columns are the same after the legacy temporal annotations have been removed. Also fixes #35148 which was the same underlying issue.
1 parent f57e57f commit d231ccb

File tree

2 files changed

+1177
-11
lines changed

2 files changed

+1177
-11
lines changed

src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs

Lines changed: 125 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections;
45
using System.Globalization;
56
using System.Text;
67
using Microsoft.EntityFrameworkCore.SqlServer.Internal;
@@ -1627,17 +1628,6 @@ protected override void ColumnDefinition(
16271628
var isPeriodStartColumn = operation[SqlServerAnnotationNames.TemporalIsPeriodStartColumn] as bool? == true;
16281629
var isPeriodEndColumn = operation[SqlServerAnnotationNames.TemporalIsPeriodEndColumn] as bool? == true;
16291630

1630-
// falling back to legacy annotations, in case the migration was generated using pre-9.0 bits
1631-
if (!isPeriodStartColumn && !isPeriodEndColumn)
1632-
{
1633-
if (operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] is string periodStartColumnName
1634-
&& operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] is string periodEndColumnName)
1635-
{
1636-
isPeriodStartColumn = operation.Name == periodStartColumnName;
1637-
isPeriodEndColumn = operation.Name == periodEndColumnName;
1638-
}
1639-
}
1640-
16411631
if (isPeriodStartColumn || isPeriodEndColumn)
16421632
{
16431633
builder.Append(" GENERATED ALWAYS AS ROW ");
@@ -2391,11 +2381,135 @@ private string Uniquify(string variableName, bool increase = true)
23912381
return _variableCounter == 0 ? variableName : variableName + _variableCounter;
23922382
}
23932383

2384+
private IReadOnlyList<MigrationOperation> FixLegacyTemporalAnnotations(IReadOnlyList<MigrationOperation> migrationOperations)
2385+
{
2386+
var resultOperations = new List<MigrationOperation>();
2387+
foreach (var migrationOperation in migrationOperations)
2388+
{
2389+
var isTemporal = migrationOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true;
2390+
if (!isTemporal)
2391+
{
2392+
resultOperations.Add(migrationOperation);
2393+
continue;
2394+
}
2395+
2396+
switch (migrationOperation)
2397+
{
2398+
case CreateTableOperation createTableOperation:
2399+
2400+
foreach (var column in createTableOperation.Columns)
2401+
{
2402+
NormalizeTemporalAnnotationsForAddColumnOperation(column);
2403+
}
2404+
2405+
resultOperations.Add(migrationOperation);
2406+
break;
2407+
2408+
case AddColumnOperation addColumnOperation:
2409+
NormalizeTemporalAnnotationsForAddColumnOperation(addColumnOperation);
2410+
resultOperations.Add(addColumnOperation);
2411+
break;
2412+
2413+
case AlterColumnOperation alterColumnOperation:
2414+
RemoveLegacyTemporalColumnAnnotations(alterColumnOperation);
2415+
RemoveLegacyTemporalColumnAnnotations(alterColumnOperation.OldColumn);
2416+
if (!CanSkipAlterColumnOperation(alterColumnOperation, alterColumnOperation.OldColumn))
2417+
{
2418+
resultOperations.Add(alterColumnOperation);
2419+
}
2420+
2421+
break;
2422+
2423+
case DropColumnOperation dropColumnOperation:
2424+
RemoveLegacyTemporalColumnAnnotations(dropColumnOperation);
2425+
resultOperations.Add(dropColumnOperation);
2426+
break;
2427+
2428+
case RenameColumnOperation renameColumnOperation:
2429+
RemoveLegacyTemporalColumnAnnotations(renameColumnOperation);
2430+
resultOperations.Add(renameColumnOperation);
2431+
break;
2432+
2433+
default:
2434+
resultOperations.Add(migrationOperation);
2435+
break;
2436+
}
2437+
}
2438+
2439+
return resultOperations;
2440+
2441+
static void NormalizeTemporalAnnotationsForAddColumnOperation(AddColumnOperation addColumnOperation)
2442+
{
2443+
var periodStartColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string;
2444+
var periodEndColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string;
2445+
if (periodStartColumnName == addColumnOperation.Name)
2446+
{
2447+
addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true);
2448+
}
2449+
else if (periodEndColumnName == addColumnOperation.Name)
2450+
{
2451+
addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true);
2452+
}
2453+
2454+
RemoveLegacyTemporalColumnAnnotations(addColumnOperation);
2455+
}
2456+
2457+
static void RemoveLegacyTemporalColumnAnnotations(MigrationOperation operation)
2458+
{
2459+
operation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
2460+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName);
2461+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema);
2462+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
2463+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
2464+
}
2465+
2466+
static bool CanSkipAlterColumnOperation(ColumnOperation first, ColumnOperation second)
2467+
=> ColumnPropertiesAreTheSame(first, second) && AnnotationsAreTheSame(first, second);
2468+
2469+
// don't compare name, table or schema - they are not being set in the model differ (since they should always be the same)
2470+
static bool ColumnPropertiesAreTheSame(ColumnOperation first, ColumnOperation second)
2471+
=> first.ClrType == second.ClrType
2472+
&& first.Collation == second.Collation
2473+
&& first.ColumnType == second.ColumnType
2474+
&& first.Comment == second.Comment
2475+
&& first.ComputedColumnSql == second.ComputedColumnSql
2476+
&& Equals(first.DefaultValue, second.DefaultValue)
2477+
&& first.DefaultValueSql == second.DefaultValueSql
2478+
&& first.IsDestructiveChange == second.IsDestructiveChange
2479+
&& first.IsFixedLength == second.IsFixedLength
2480+
&& first.IsNullable == second.IsNullable
2481+
&& first.IsReadOnly == second.IsReadOnly
2482+
&& first.IsRowVersion == second.IsRowVersion
2483+
&& first.IsStored == second.IsStored
2484+
&& first.IsUnicode == second.IsUnicode
2485+
&& first.MaxLength == second.MaxLength
2486+
&& first.Precision == second.Precision
2487+
&& first.Scale == second.Scale;
2488+
2489+
static bool AnnotationsAreTheSame(ColumnOperation first, ColumnOperation second)
2490+
{
2491+
var firstAnnotations = first.GetAnnotations().OrderBy(x => x.Name).ToList();
2492+
var secondAnnotations = second.GetAnnotations().OrderBy(x => x.Name).ToList();
2493+
2494+
if (firstAnnotations.Count != secondAnnotations.Count)
2495+
{
2496+
return false;
2497+
}
2498+
2499+
return firstAnnotations.Zip(
2500+
secondAnnotations,
2501+
(f, s) => f.Name == s.Name && StructuralComparisons.StructuralEqualityComparer.Equals(f.Value, s.Value))
2502+
.All(x => x == true);
2503+
}
2504+
}
2505+
23942506
private IReadOnlyList<MigrationOperation> RewriteOperations(
23952507
IReadOnlyList<MigrationOperation> migrationOperations,
23962508
IModel? model,
23972509
MigrationsSqlGenerationOptions options)
23982510
{
2511+
migrationOperations = FixLegacyTemporalAnnotations(migrationOperations);
2512+
23992513
var operations = new List<MigrationOperation>();
24002514
var availableSchemas = new List<string>();
24012515

0 commit comments

Comments
 (0)