Skip to content

Idempotent migration scripts wrap CREATE INDEX CONCURRENTLY in a DO block #3921

Description

@bogdan-apaleo

Description

IsCreatedConcurrently() emits CREATE INDEX CONCURRENTLY and suppresses the surrounding transaction (#1210, #1214). dotnet ef migrations script without --idempotent leaves that statement outside the transaction, and PostgreSQL accepts it.

dotnet ef migrations script --idempotent still wraps every command in NpgsqlHistoryRepository.GetBeginIfNotExistsScript, including commands marked TransactionSuppressed. That method is a DO $EF$ block, because PostgreSQL has no top-level IF. PostgreSQL rejects CREATE INDEX CONCURRENTLY inside a function.

database update is unaffected. It runs the suppressed command on its own, outside a transaction and outside DO.

Reproduction

Npgsql.EntityFrameworkCore.PostgreSQL 10.0.3, Microsoft.EntityFrameworkCore 10.0.10, dotnet-ef 10.0.10, PostgreSQL 14.13.

modelBuilder.Entity<Blog>(b => b.HasIndex(x => x.Name).IsCreatedConcurrently());

dotnet ef migrations script --idempotent emits:

COMMIT;

DO $EF$
BEGIN
    IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260925155729_AddNameIndex') THEN
    CREATE INDEX CONCURRENTLY "IX_Blogs_Name" ON "Blogs" ("Name");
    END IF;
END $EF$;

The table create is already committed. Executing the DO block on PostgreSQL 14.13, with an empty __EFMigrationsHistory, fails:

ERROR:  CREATE INDEX CONCURRENTLY cannot be executed from a function
CONTEXT:  SQL statement "CREATE INDEX CONCURRENTLY "IX_Blogs_Name" ON "Blogs" ("Name")"
PL/pgSQL function inline_code_block line 4 at SQL statement

The history insert comes after that statement, so it never runs. A second attempt enters the IF again and fails on CREATE TABLE with relation "Blogs" already exists.

Without --idempotent, the same migration emits a top-level statement, which succeeds:

COMMIT;

CREATE INDEX CONCURRENTLY "IX_Blogs_Name" ON "Blogs" ("Name");

Expected

Transaction-suppressed commands should be emitted outside the DO $EF$ block. The history check cannot be a PL/pgSQL IF around CREATE INDEX CONCURRENTLY.

EF Core's Migrator.GenerateSqlScript applies GetBeginIfNotExistsScript to every command, including TransactionSuppressed ones. #1214 set suppressTransaction on the create-index command, which is why the script commits first. The idempotent wrapper is a separate layer and still produces SQL PostgreSQL will not run.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions