Skip to content

supabase db reset fails on multi-statement migrations (42601) and CONCURRENTLY in pipeline (25001) #5139

@Apaskin

Description

@Apaskin

Environment` down goes in the body. Nothing in this document needs editing before filing.


Title

supabase db reset fails on multi-statement migrations: SQLSTATE 42601 "cannot insert multiple commands into a prepared statement"


Environment

  • CLI versions tested (all reproduce the issue, see version matrix below): v2.89.0, v2.90.0, v2.91.0, v2.92.1, v2.95.3
  • OS: Windows 11 Home 10.0.26200
  • Docker: Docker version 29.2.1, build a5c7197
  • Postgres (in the Supabase-managed supabase_db_* container): PostgreSQL 17.6
  • Shell: Git Bash on Windows

Summary

supabase db reset (and by extension supabase migration up) cannot replay a real-world Supabase migrations directory end-to-end. Two distinct CLI-side failures are involved:

  1. v2.89.0–v2.91.0: Any migration file containing more than one SQL statement is rejected by Postgres with SQLSTATE 42601 ("cannot insert multiple commands into a prepared statement"). The CLI is sending the entire file as a single Parse message in the extended query protocol, which Postgres does not allow.
  2. v2.92.1, v2.95.3: The multi-statement issue above is fixed, but the CLI now wraps each migration in a pipeline. Any migration containing CREATE INDEX CONCURRENTLY (or any other statement that cannot run inside a pipeline / transaction) is rejected with SQLSTATE 25001 ("CREATE INDEX CONCURRENTLY cannot be executed within a pipeline").

Either bug alone is sufficient to break local replay; together they mean no released CLI version can replay this codebase. The same migration files apply correctly via psql and via the Supabase platform / dashboard / production CI path.


Steps to reproduce

Bug 1 — multi-statement file rejected (v2.89.0 – v2.91.0)

Create supabase/migrations/20260101000000_multi_statement_repro.sql with the following 14 lines:

-- Reproduces SQLSTATE 42601 on `supabase db reset` (CLI v2.89-v2.91).
-- This file is valid SQL and applies cleanly via `psql`.

CREATE TABLE IF NOT EXISTS public.cli_repro_a (id INT);
CREATE TABLE IF NOT EXISTS public.cli_repro_b (id INT);

DO $$
BEGIN
  INSERT INTO public.cli_repro_a VALUES (1);
  INSERT INTO public.cli_repro_b VALUES (2);
END
$$;

-- end of file

Then run:

supabase db reset

Bug 2 — pipeline wrapper breaks CONCURRENTLY (v2.92.1, v2.95.3)

Create supabase/migrations/20260101000001_concurrently_repro.sql:

-- Reproduces SQLSTATE 25001 on `supabase db reset` (CLI v2.92+).
CREATE TABLE IF NOT EXISTS public.cli_repro_c (id INT);
CREATE INDEX CONCURRENTLY IF NOT EXISTS cli_repro_c_idx ON public.cli_repro_c (id);

Then run:

supabase db reset

Expected behaviour

supabase db reset should apply both repro files successfully, since they:

  • are valid SQL,
  • apply cleanly via psql (single-statement protocol path),
  • apply cleanly when sent to the Supabase platform's migrations endpoint,
  • match patterns found in many real production migrations (multi-statement DDL, idempotent index creation, DO blocks).

Actual behaviour

Bug 1

Applying migration 20260101000000_multi_statement_repro.sql...
ERROR: cannot insert multiple commands into a prepared statement (SQLSTATE 42601)

supabase db reset --debug shows the protocol-level cause:

PG Recv: {"Type":"ErrorResponse","Severity":"ERROR","Code":"42601",
         "Message":"cannot insert multiple commands into a prepared statement",
         "File":"postgres.c","Line":1478,"Routine":"exec_parse_message"}

exec_parse_message is the Postgres backend handler for the Parse message in the extended query protocol. The CLI is sending the entire multi-statement file as a single Parse message, which Postgres rejects by design — Parse only accepts a single command. The simple-query protocol (used by psql by default) is what supports multiple commands per message.

Bug 2

Applying migration 20260101000001_concurrently_repro.sql...
ERROR: CREATE INDEX CONCURRENTLY cannot be executed within a pipeline (SQLSTATE 25001)

This is a regression from the v2.91→v2.92 change that fixed Bug 1. Earlier CLI versions ran each file in a per-file session without an implicit pipeline/transaction wrapper, which is what CREATE INDEX CONCURRENTLY requires.


Workaround currently in use

We bypass the CLI's migration runner entirely with a small Node script that pipes each migration through docker exec ... psql:

  • File: scripts/load/apply-migrations-via-psql.mjs
  • Behaviour: drops + recreates the public schema (preserving the supabase-managed auth, storage, realtime, extensions, and supabase_migrations schemas), then iterates supabase/migrations/*.sql in chronological order and applies each one via docker exec -i <supabase_db_container> psql -v ON_ERROR_STOP=1. Each applied file is recorded in supabase_migrations.schema_migrations so supabase migration list reflects reality afterward.
  • Result: applies all 137 migrations in ~30 seconds, no errors.

The relevant excerpt:

// scripts/load/apply-migrations-via-psql.mjs (excerpt)
for (const file of migrationFiles) {
  const sql = readFileSync(file, "utf8");
  execSync(
    `docker exec -i -e PGPASSWORD=postgres ${dbContainer} psql -U postgres -d postgres -v ON_ERROR_STOP=1`,
    { input: sql, stdio: ["pipe", "inherit", "inherit"] },
  );
  // ...record in supabase_migrations.schema_migrations
}

This works because psql reads from stdin using the simple-query protocol, which accepts multiple commands in a single message and does not implicitly wrap statements in a pipeline.


Scope of impact in our codebase

A static-analysis sweep of our supabase/migrations/ directory (137 files total, generated by scripts/load/enumerate-multistmt-migrations.mjs) shows:

  • Bug 1 (multi-statement) affects 97 of 137 migration files. Examples include multi-statement DDL, accompanying RLS policy creation, idempotent grants, and DO $$ ... $$ bodies that share a file with other statements. None of these files are pathological — they're idiomatic migration files.
  • Bug 2 (CONCURRENTLY in a pipeline) affects 1 of 137 migration files. That file's own header comment even anticipates the constraint:

    "CONCURRENTLY cannot run inside a transaction; this migration relies on Supabase CLI's per-file session (which does not auto-wrap in BEGIN/COMMIT)."
    That assumption was correct on v2.91 and earlier. v2.92+ broke it.

The largest multi-statement files (purely as a sense-check of "this is normal real-world migration content"):

Statements File
43 20260212100000_user_taxonomy.sql
42 20260203000000_initial_schema.sql
39 20260316200001_harden_security_definer_privileges_part2.sql
33 20260305200000_v1_content_model.sql
30 20260221000000_import_v2_staging.sql

Production migration application is unaffected

Migrations applied through the Supabase platform / Dashboard / production CI path apply cleanly — that path uses the simple-query protocol and does not wrap statements in a pipeline. This issue is local-CLI-only, but it means every developer running supabase db reset against a real Supabase project that uses multi-statement migrations or CONCURRENTLY indexes will hit it.


Version matrix

CLI version Bug 1 (multi-statement) Bug 2 (CONCURRENTLY in pipeline) Replays this codebase end-to-end?
v2.89.0 � FAIL (not reached) No
v2.90.0 � FAIL (not reached) No
v2.91.0 � FAIL (not reached) No
v2.92.1 ✅ pass � FAIL No
v2.95.3 ✅ pass � FAIL No

No tested version replays a real-world migrations directory containing both multi-statement files and CREATE INDEX CONCURRENTLY migrations.


Suggested direction (optional)

For Bug 1: send each file using the simple-query protocol (PQexec-equivalent, single Query message containing the file body), or split each file at top-level statement boundaries before sending each one through the extended protocol.

For Bug 2: do not implicitly wrap each migration file in a pipeline. Either run each statement as its own round-trip, or detect statements that cannot run in a pipeline (CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY, VACUUM, ALTER SYSTEM, etc.) and run them outside the wrapper.

These are suggestions only — happy to test a candidate fix on the affected codebase if useful.


Reference files (in our codebase, for context only — not required to triage)

  • Workaround script: scripts/load/apply-migrations-via-psql.mjs
  • Static enumeration of affected files: scripts/load/enumerate-multistmt-migrations.mjs, output in scripts/load/multistmt-migrations.txt
  • Failure captures: scripts/load/db-reset-output.txt, scripts/load/db-reset-output-2.txt

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions