Skip to content

Commit 4b3e12f

Browse files
authored
Fix to #35108 - Temporal table migration regression from EF Core 8 to 9 (#35289)
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.
1 parent 7937010 commit 4b3e12f

File tree

2 files changed

+1202
-11
lines changed

2 files changed

+1202
-11
lines changed

src/EFCore.SqlServer/Migrations/SqlServerMigrationsSqlGenerator.cs

Lines changed: 130 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;
@@ -1599,17 +1600,6 @@ protected override void ColumnDefinition(
15991600
var isPeriodStartColumn = operation[SqlServerAnnotationNames.TemporalIsPeriodStartColumn] as bool? == true;
16001601
var isPeriodEndColumn = operation[SqlServerAnnotationNames.TemporalIsPeriodEndColumn] as bool? == true;
16011602

1602-
// falling back to legacy annotations, in case the migration was generated using pre-9.0 bits
1603-
if (!isPeriodStartColumn && !isPeriodEndColumn)
1604-
{
1605-
if (operation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] is string periodStartColumnName
1606-
&& operation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] is string periodEndColumnName)
1607-
{
1608-
isPeriodStartColumn = operation.Name == periodStartColumnName;
1609-
isPeriodEndColumn = operation.Name == periodEndColumnName;
1610-
}
1611-
}
1612-
16131603
if (isPeriodStartColumn || isPeriodEndColumn)
16141604
{
16151605
builder.Append(" GENERATED ALWAYS AS ROW ");
@@ -2363,11 +2353,140 @@ private string Uniquify(string variableName, bool increase = true)
23632353
return _variableCounter == 0 ? variableName : variableName + _variableCounter;
23642354
}
23652355

2356+
private IReadOnlyList<MigrationOperation> FixLegacyTemporalAnnotations(IReadOnlyList<MigrationOperation> migrationOperations)
2357+
{
2358+
// short-circuit for non-temporal migrations (which is the majority)
2359+
if (migrationOperations.All(o => o[SqlServerAnnotationNames.IsTemporal] as bool? != true))
2360+
{
2361+
return migrationOperations;
2362+
}
2363+
2364+
var resultOperations = new List<MigrationOperation>(migrationOperations.Count);
2365+
foreach (var migrationOperation in migrationOperations)
2366+
{
2367+
var isTemporal = migrationOperation[SqlServerAnnotationNames.IsTemporal] as bool? == true;
2368+
if (!isTemporal)
2369+
{
2370+
resultOperations.Add(migrationOperation);
2371+
continue;
2372+
}
2373+
2374+
switch (migrationOperation)
2375+
{
2376+
case CreateTableOperation createTableOperation:
2377+
2378+
foreach (var column in createTableOperation.Columns)
2379+
{
2380+
NormalizeTemporalAnnotationsForAddColumnOperation(column);
2381+
}
2382+
2383+
resultOperations.Add(migrationOperation);
2384+
break;
2385+
2386+
case AddColumnOperation addColumnOperation:
2387+
NormalizeTemporalAnnotationsForAddColumnOperation(addColumnOperation);
2388+
resultOperations.Add(addColumnOperation);
2389+
break;
2390+
2391+
case AlterColumnOperation alterColumnOperation:
2392+
RemoveLegacyTemporalColumnAnnotations(alterColumnOperation);
2393+
RemoveLegacyTemporalColumnAnnotations(alterColumnOperation.OldColumn);
2394+
if (!CanSkipAlterColumnOperation(alterColumnOperation, alterColumnOperation.OldColumn))
2395+
{
2396+
resultOperations.Add(alterColumnOperation);
2397+
}
2398+
2399+
break;
2400+
2401+
case DropColumnOperation dropColumnOperation:
2402+
RemoveLegacyTemporalColumnAnnotations(dropColumnOperation);
2403+
resultOperations.Add(dropColumnOperation);
2404+
break;
2405+
2406+
case RenameColumnOperation renameColumnOperation:
2407+
RemoveLegacyTemporalColumnAnnotations(renameColumnOperation);
2408+
resultOperations.Add(renameColumnOperation);
2409+
break;
2410+
2411+
default:
2412+
resultOperations.Add(migrationOperation);
2413+
break;
2414+
}
2415+
}
2416+
2417+
return resultOperations;
2418+
2419+
static void NormalizeTemporalAnnotationsForAddColumnOperation(AddColumnOperation addColumnOperation)
2420+
{
2421+
var periodStartColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodStartColumnName] as string;
2422+
var periodEndColumnName = addColumnOperation[SqlServerAnnotationNames.TemporalPeriodEndColumnName] as string;
2423+
if (periodStartColumnName == addColumnOperation.Name)
2424+
{
2425+
addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodStartColumn, true);
2426+
}
2427+
else if (periodEndColumnName == addColumnOperation.Name)
2428+
{
2429+
addColumnOperation.AddAnnotation(SqlServerAnnotationNames.TemporalIsPeriodEndColumn, true);
2430+
}
2431+
2432+
RemoveLegacyTemporalColumnAnnotations(addColumnOperation);
2433+
}
2434+
2435+
static void RemoveLegacyTemporalColumnAnnotations(MigrationOperation operation)
2436+
{
2437+
operation.RemoveAnnotation(SqlServerAnnotationNames.IsTemporal);
2438+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableName);
2439+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalHistoryTableSchema);
2440+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodStartColumnName);
2441+
operation.RemoveAnnotation(SqlServerAnnotationNames.TemporalPeriodEndColumnName);
2442+
}
2443+
2444+
static bool CanSkipAlterColumnOperation(ColumnOperation column, ColumnOperation oldColumn)
2445+
=> ColumnPropertiesAreTheSame(column, oldColumn) && AnnotationsAreTheSame(column, oldColumn);
2446+
2447+
// don't compare name, table or schema - they are not being set in the model differ (since they should always be the same)
2448+
static bool ColumnPropertiesAreTheSame(ColumnOperation column, ColumnOperation oldColumn)
2449+
=> column.ClrType == oldColumn.ClrType
2450+
&& column.Collation == oldColumn.Collation
2451+
&& column.ColumnType == oldColumn.ColumnType
2452+
&& column.Comment == oldColumn.Comment
2453+
&& column.ComputedColumnSql == oldColumn.ComputedColumnSql
2454+
&& Equals(column.DefaultValue, oldColumn.DefaultValue)
2455+
&& column.DefaultValueSql == oldColumn.DefaultValueSql
2456+
&& column.IsDestructiveChange == oldColumn.IsDestructiveChange
2457+
&& column.IsFixedLength == oldColumn.IsFixedLength
2458+
&& column.IsNullable == oldColumn.IsNullable
2459+
&& column.IsReadOnly == oldColumn.IsReadOnly
2460+
&& column.IsRowVersion == oldColumn.IsRowVersion
2461+
&& column.IsStored == oldColumn.IsStored
2462+
&& column.IsUnicode == oldColumn.IsUnicode
2463+
&& column.MaxLength == oldColumn.MaxLength
2464+
&& column.Precision == oldColumn.Precision
2465+
&& column.Scale == oldColumn.Scale;
2466+
2467+
static bool AnnotationsAreTheSame(ColumnOperation column, ColumnOperation oldColumn)
2468+
{
2469+
var columnAnnotations = column.GetAnnotations().ToList();
2470+
var oldColumnAnnotations = oldColumn.GetAnnotations().ToList();
2471+
2472+
if (columnAnnotations.Count != oldColumnAnnotations.Count)
2473+
{
2474+
return false;
2475+
}
2476+
2477+
return columnAnnotations.Zip(oldColumnAnnotations)
2478+
.All(x => x.First.Name == x.Second.Name
2479+
&& StructuralComparisons.StructuralEqualityComparer.Equals(x.First.Value, x.Second.Value));
2480+
}
2481+
}
2482+
23662483
private IReadOnlyList<MigrationOperation> RewriteOperations(
23672484
IReadOnlyList<MigrationOperation> migrationOperations,
23682485
IModel? model,
23692486
MigrationsSqlGenerationOptions options)
23702487
{
2488+
migrationOperations = FixLegacyTemporalAnnotations(migrationOperations);
2489+
23712490
var operations = new List<MigrationOperation>();
23722491
var availableSchemas = new List<string>();
23732492

0 commit comments

Comments
 (0)