Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -354,4 +354,12 @@ public static class SqlServerAnnotationNames
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public const string FullTextCatalogs = Prefix + "FullTextCatalogs";

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public const string UseDropExisting = Prefix + "UseDropExisting";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2132,6 +2132,15 @@ protected override void IndexOptions(MigrationOperation operation, IModel? model
});
}

// 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)
Expand Down Expand Up @@ -2664,6 +2673,79 @@ private string Uniquify(string variableName, bool increase = true)
return _variableCounter == 0 ? variableName : variableName + _variableCounter;
}

private IReadOnlyList<MigrationOperation> RewriteDropAndCreateIndexAsDropExisting(
IReadOnlyList<MigrationOperation> migrationOperations)
{
// 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 rewrite is limited to non-special indexes (no memory-optimized, full-text or vector
// index, since those use different syntax/restrictions).

// Short-circuit when no DropIndex+CreateIndex pair is possible.
if (!migrationOperations.OfType<DropIndexOperation>().Any()
|| !migrationOperations.OfType<CreateIndexOperation>().Any())
{
return migrationOperations;
}

// Map (Name, Table, Schema) → DropIndexOperation, only for drops we may collapse.
var dropsByIdentity = new Dictionary<(string Name, string Table, string? Schema), DropIndexOperation>();
foreach (var dropOperation in migrationOperations.OfType<DropIndexOperation>())
{
if (dropOperation.Table is null)
{
continue;
}

dropsByIdentity[(dropOperation.Name, dropOperation.Table, dropOperation.Schema)] = dropOperation;
}

// Identify the drops we are going to collapse and mark the matching creates.
var dropsToRemove = new HashSet<DropIndexOperation>();
foreach (var createOperation in migrationOperations.OfType<CreateIndexOperation>())
{
if (createOperation.Table is null
|| !dropsByIdentity.TryGetValue(
(createOperation.Name, createOperation.Table, createOperation.Schema), out var dropOperation))
{
continue;
}

// 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: null, createOperation.Schema, createOperation.Table))
{
continue;
}

createOperation.AddAnnotation(SqlServerAnnotationNames.UseDropExisting, true);
dropsToRemove.Add(dropOperation);
}

if (dropsToRemove.Count == 0)
{
return migrationOperations;
}

var resultOperations = new List<MigrationOperation>(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<MigrationOperation> FixLegacyTemporalAnnotations(IReadOnlyList<MigrationOperation> migrationOperations)
{
// short-circuit for non-temporal migrations (which is the majority)
Expand Down Expand Up @@ -2797,6 +2879,7 @@ private IReadOnlyList<MigrationOperation> RewriteOperations(
MigrationsSqlGenerationOptions options)
{
migrationOperations = FixLegacyTemporalAnnotations(migrationOperations);
migrationOperations = RewriteDropAndCreateIndexAsDropExisting(migrationOperations);

var operations = new List<MigrationOperation>();
var availableSchemas = new List<string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2039,11 +2039,7 @@ public override async Task Alter_index_make_unique()

AssertSql(
"""
DROP INDEX [IX_People_X] ON [People];
""",
//
"""
CREATE UNIQUE INDEX [IX_People_X] ON [People] ([X]);
CREATE UNIQUE INDEX [IX_People_X] ON [People] ([X]) WITH (DROP_EXISTING = ON);
""");
}

Expand All @@ -2053,11 +2049,74 @@ public override async Task Alter_index_change_sort_order()

AssertSql(
"""
DROP INDEX [IX_People_X_Y_Z] ON [People];
""",
//
CREATE INDEX [IX_People_X_Y_Z] ON [People] ([X], [Y] DESC, [Z]) WITH (DROP_EXISTING = ON);
""");
}

[ConditionalFact]
public virtual async Task Alter_index_fill_factor_uses_drop_existing()
{
// Regression test for #35067: when only an index facet (fill factor here) changes, the
// migration must collapse the differ's Drop+Create pair into a single CREATE INDEX
// ... WITH (DROP_EXISTING = ON), which lets queries continue using the old index while the
// new one is being built.
await Test(
builder => builder.Entity(
"People", e =>
{
e.Property<int>("Id");
e.Property<string>("Name").IsRequired();
e.HasIndex("Name").HasFillFactor(80);
}),
builder => builder.Entity(
"People", e =>
{
e.Property<int>("Id");
e.Property<string>("Name").IsRequired();
e.HasIndex("Name").HasFillFactor(90);
}),
model =>
{
var table = Assert.Single(model.Tables);
var index = Assert.Single(table.Indexes);
Assert.Equal(90, index[SqlServerAnnotationNames.FillFactor]);
});

AssertSql(
"""
CREATE INDEX [IX_People_X_Y_Z] ON [People] ([X], [Y] DESC, [Z]);
CREATE INDEX [IX_People_Name] ON [People] ([Name]) WITH (FILLFACTOR = 90, DROP_EXISTING = ON);
""");
}

[ConditionalFact]
public virtual async Task Alter_index_filter_uses_drop_existing()
{
// Changing the index filter must also collapse to CREATE INDEX ... WITH (DROP_EXISTING = ON).
await Test(
builder => builder.Entity(
"People", e =>
{
e.Property<int>("Id");
e.Property<string>("Name");
e.HasIndex("Name").HasFilter("[Name] IS NOT NULL");
}),
builder => builder.Entity(
"People", e =>
{
e.Property<int>("Id");
e.Property<string>("Name");
e.HasIndex("Name").HasFilter("[Name] IS NOT NULL AND [Name] <> N''");
}),
model =>
{
var table = Assert.Single(model.Tables);
var index = Assert.Single(table.Indexes);
Assert.Equal("([Name] IS NOT NULL AND [Name]<>N'')", index.Filter);
});

AssertSql(
"""
CREATE INDEX [IX_People_Name] ON [People] ([Name]) WHERE [Name] IS NOT NULL AND [Name] <> N'' WITH (DROP_EXISTING = ON);
""");
}

Expand Down
Loading