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:
- 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.
- 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:
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:
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
Environment` down goes in the body. Nothing in this document needs editing before filing.
Title
supabase db resetfails on multi-statement migrations: SQLSTATE 42601 "cannot insert multiple commands into a prepared statement"Environment
supabase_db_*container): PostgreSQL 17.6Summary
supabase db reset(and by extensionsupabase migration up) cannot replay a real-world Supabase migrations directory end-to-end. Two distinct CLI-side failures are involved: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.CREATE INDEX CONCURRENTLY(or any other statement that cannot run inside a pipeline / transaction) is rejected withSQLSTATE 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
psqland 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.sqlwith the following 14 lines:Then run:
Bug 2 — pipeline wrapper breaks CONCURRENTLY (v2.92.1, v2.95.3)
Create
supabase/migrations/20260101000001_concurrently_repro.sql:Then run:
Expected behaviour
supabase db resetshould apply both repro files successfully, since they:psql(single-statement protocol path),DOblocks).Actual behaviour
Bug 1
supabase db reset --debugshows the protocol-level cause:exec_parse_messageis 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 bypsqlby default) is what supports multiple commands per message.Bug 2
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 CONCURRENTLYrequires.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:scripts/load/apply-migrations-via-psql.mjspublicschema (preserving the supabase-managedauth,storage,realtime,extensions, andsupabase_migrationsschemas), then iteratessupabase/migrations/*.sqlin chronological order and applies each one viadocker exec -i <supabase_db_container> psql -v ON_ERROR_STOP=1. Each applied file is recorded insupabase_migrations.schema_migrationssosupabase migration listreflects reality afterward.The relevant excerpt:
This works because
psqlreads 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 byscripts/load/enumerate-multistmt-migrations.mjs) shows:DO $$ ... $$bodies that share a file with other statements. None of these files are pathological — they're idiomatic migration files.The largest multi-statement files (purely as a sense-check of "this is normal real-world migration content"):
20260212100000_user_taxonomy.sql20260203000000_initial_schema.sql20260316200001_harden_security_definer_privileges_part2.sql20260305200000_v1_content_model.sql20260221000000_import_v2_staging.sqlProduction 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 resetagainst a real Supabase project that uses multi-statement migrations or CONCURRENTLY indexes will hit it.Version matrix
No tested version replays a real-world migrations directory containing both multi-statement files and
CREATE INDEX CONCURRENTLYmigrations.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)
scripts/load/apply-migrations-via-psql.mjsscripts/load/enumerate-multistmt-migrations.mjs, output inscripts/load/multistmt-migrations.txtscripts/load/db-reset-output.txt,scripts/load/db-reset-output-2.txt