From 2c5cb9fc6756a2c5d927809655ac7c204150f6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCpcan=20=C3=87ak=C4=B1r?= <69207222+eypcnckr@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:05:51 +0300 Subject: [PATCH] Support DROP INDEX CONCURRENTLY CREATE INDEX CONCURRENTLY has been supported since #968, but the matching drop was not: Generate(DropIndexOperation) always emitted a plain DROP INDEX inside the migration transaction, so removing an index created with IsCreatedConcurrently() takes an ACCESS EXCLUSIVE lock on the table. Two things were missing: - NpgsqlMigrationsAnnotationProvider did not override ForRemove(ITableIndex), so the Npgsql:CreatedConcurrently annotation never reached the DropIndexOperation produced by the migrations differ. - Generate(DropIndexOperation) did not look at that annotation. Honour the annotation on both sides, and suppress the transaction for the drop, mirroring what Generate(CreateIndexOperation) already does. --- .../NpgsqlMigrationsAnnotationProvider.cs | 15 ++++++++ .../NpgsqlMigrationsSqlGenerator.cs | 15 ++++++-- .../Migrations/MigrationsNpgsqlTest.cs | 18 +++++++++ .../NpgsqlMigrationsSqlGeneratorTest.cs | 37 +++++++++++++++++++ 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs index 30aeafb919..dc85a55658 100644 --- a/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs +++ b/src/EFCore.PG/Migrations/Internal/NpgsqlMigrationsAnnotationProvider.cs @@ -31,4 +31,19 @@ public NpgsqlMigrationsAnnotationProvider(MigrationsAnnotationProviderDependenci /// public override IEnumerable ForRemove(IRelationalModel model) => model.Model.GetAnnotations().Where(NpgsqlAnnotationHelper.IsRelationalModelAnnotation); + + /// + /// 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. + /// + public override IEnumerable ForRemove(ITableIndex index) + { + // Model validation ensures that these facets are the same on all mapped indexes + if (index.MappedIndexes.First().IsCreatedConcurrently() is { } isCreatedConcurrently) + { + yield return new Annotation(NpgsqlAnnotationNames.CreatedConcurrently, isCreatedConcurrently); + } + } } diff --git a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs index c5478e4cf4..3f3e97bcfa 100644 --- a/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs +++ b/src/EFCore.PG/Migrations/NpgsqlMigrationsSqlGenerator.cs @@ -1491,14 +1491,21 @@ protected override void Generate( Check.NotNull(operation, nameof(operation)); Check.NotNull(builder, nameof(builder)); - builder - .Append("DROP INDEX ") - .Append(DelimitIdentifier(operation.Name, operation.Schema)); + builder.Append("DROP INDEX "); + + var concurrently = operation[NpgsqlAnnotationNames.CreatedConcurrently] as bool? == true; + if (concurrently) + { + builder.Append("CONCURRENTLY "); + } + + builder.Append(DelimitIdentifier(operation.Name, operation.Schema)); if (terminate) { builder.AppendLine(";"); - EndStatement(builder); + // Concurrent indexes cannot be dropped within a transaction + EndStatement(builder, suppressTransaction: concurrently); } } diff --git a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs index bfdc208ae5..7a0455f967 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/MigrationsNpgsqlTest.cs @@ -2019,6 +2019,24 @@ await Test( AssertSql("""CREATE INDEX CONCURRENTLY "IX_People_Age" ON "People" ("Age");"""); } + [Fact] + public virtual async Task Drop_index_concurrently() + { + await Test( + builder => builder.Entity( + "People", e => + { + e.Property("Id"); + e.Property("Age"); + }), + builder => builder.Entity("People").HasIndex("Age") + .IsCreatedConcurrently(), + _ => { }, + asserter: null); // No scaffolding for IsCreatedConcurrently + + AssertSql("""DROP INDEX CONCURRENTLY "IX_People_Age";"""); + } + [Fact] public virtual async Task Create_index_with_method() { diff --git a/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs b/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs index 2cf1b21d9b..c11e1bb5ba 100644 --- a/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs +++ b/test/EFCore.PG.FunctionalTests/Migrations/NpgsqlMigrationsSqlGeneratorTest.cs @@ -619,6 +619,43 @@ public override void InsertDataOperation_throws_for_unsupported_column_types() #pragma warning restore 618 + [Fact] + public virtual void DropIndexOperation_concurrently() + { + Generate( + new DropIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo", + [NpgsqlAnnotationNames.CreatedConcurrently] = true + }); + + AssertSql( + """ +DROP INDEX CONCURRENTLY dbo."IX_People_Name"; + +"""); + } + + [Fact] + public virtual void DropIndexOperation_not_concurrently() + { + Generate( + new DropIndexOperation + { + Name = "IX_People_Name", + Table = "People", + Schema = "dbo" + }); + + AssertSql( + """ +DROP INDEX dbo."IX_People_Name"; + +"""); + } + protected override string GetGeometryCollectionStoreType() => "GEOMETRY(GEOMETRYCOLLECTION)"; }