Summary
The CLI's SQL statement splitter mis-parses migrations that contain nested dollar-quoted blocks (a DO $outer$ ... $outer$; whose body itself contains another dollar-quoted string such as EXECUTE $sql$ ... $sql$;). The splitter loses track of dollar-quote nesting depth, treats statement-terminating semicolons inside the outer block as top-level boundaries, and bundles multiple separate statements into a single string. PostgreSQL then rejects the bundle with SQLSTATE 42601 ("cannot insert multiple commands into a prepared statement"), even though the SQL is well-formed and all dollar tags are balanced per PostgreSQL semantics.
Affected file
pkg/parser/state.go — specifically the dollar-quote handling in the Ready and DollarState transitions (the same neighborhood as the isBeginAtomic check).
Symptom
ERROR: cannot insert multiple commands into a prepared statement (SQLSTATE 42601)
At statement: N
When this happens during the Supabase platform's GitHub branching auto-apply, the preview branch lands in MIGRATIONS_FAILED and the "Supabase Preview" GitHub check reports SKIPPED. There is no inline error annotation on the PR — the only signal an author sees is the SKIPPED status.
Minimal reproduction
A single migration file is enough:
-- 20260101000000_test_nested_dollar_quotes.sql
DO $boot$
BEGIN
EXECUTE $sql$
SELECT 1;
$sql$;
END;
$boot$;
CREATE TABLE example (id integer);
Apply via the Supabase platform's GitHub branching auto-apply on a fresh preview branch.
A real-world variant that triggered this in our project used three levels of dollar-quoting ($migration_<v>$ ... DO $boot$ ... EXECUTE $sql$ ... $sql$ ... END $boot$ ... $migration_<v>$) inside an UPDATE ... ARRAY[...] payload, where the outermost tag was used precisely because the body already contained $boot$ as a string literal. The splitter chokes there as well, even though all three tag pairs are balanced and well-nested per PostgreSQL.
Expected behavior
Both statements (DO $boot$ ... $boot$; and CREATE TABLE example ...;) execute as separate commands. The table is created. The preview branch becomes ready.
Actual behavior
The splitter bundles the two statements (and any subsequent statements in the file) into a single string and submits them together. PostgreSQL rejects the bundle with SQLSTATE 42601. Migrations halt; the preview branch is broken.
Why this matters
Every PR whose migrations touch this pattern silently breaks the Supabase Preview workflow. PR authors get no inline error annotation — only the SKIPPED status on the "Supabase Preview" check, which is easy to miss and hard to diagnose without access to the platform's internal apply logs. Nested dollar-quoting is a standard, idiomatic PostgreSQL technique (recommended by the PostgreSQL docs precisely so authors can embed dollar-tags inside dollar-tags without escaping), so it is reasonable for migration authors to expect it to Just Work.
Related known bug
Issues #4746, #5020, and #5062 documented a different splitter bug in the same file: isBeginAtomic matches data[-6:] == "ATOMIC" without an identifier-rune-boundary check, so any identifier ending in those six letters (case-insensitive) trips a false BEGIN ATOMIC state. That issue is a cousin of this one but is a distinct code path — the bug reported here is about nested dollar-quote depth tracking, not about identifier-suffix matching, and reproduces with SQL that has no atomic substring anywhere.
Workaround
Rewrite affected migrations to avoid nested dollar-quoting:
- Replace inner dollar-quoted strings with plain string literals using
'' escaping.
- Or split logic into multiple non-nested top-level statements so no
DO block contains another dollar-quoted body.
Both workarounds are invasive: they force migration authors to know about and route around a CLI bug, and they make some otherwise idiomatic PL/pgSQL patterns unavailable.
Suggested fix direction
Fix the splitter's nesting tracking in pkg/parser/state.go:
- Track an explicit depth/stack of active dollar tags rather than matching on the most recent tag only.
- When inside a dollar-quoted block, treat the body as opaque to top-level state transitions until the matching closing tag is seen at the correct depth.
- While in that neighborhood, the
isBeginAtomic check should also be guarded by an identifier-rune-boundary check (related to the _atomic cousin bug).
Either change in isolation does not fix the other; both deserve attention.
Environment
- Supabase CLI v1.226.0, as embedded in the Supabase platform's GitHub branching auto-apply (the version pinned by the platform integration as of 2026-04-28). Likely affects neighbouring versions — please confirm by checking when the splitter logic last changed in
pkg/parser/state.go.
- Discovered via the platform GitHub branching integration, not via a local
supabase CLI invocation. Reproducible by anyone using the platform GitHub integration with a migration file matching the minimal repro above.
- OS: Supabase managed runner (not applicable to local repro).
Summary
The CLI's SQL statement splitter mis-parses migrations that contain nested dollar-quoted blocks (a
DO $outer$ ... $outer$;whose body itself contains another dollar-quoted string such asEXECUTE $sql$ ... $sql$;). The splitter loses track of dollar-quote nesting depth, treats statement-terminating semicolons inside the outer block as top-level boundaries, and bundles multiple separate statements into a single string. PostgreSQL then rejects the bundle withSQLSTATE 42601("cannot insert multiple commands into a prepared statement"), even though the SQL is well-formed and all dollar tags are balanced per PostgreSQL semantics.Affected file
pkg/parser/state.go— specifically the dollar-quote handling in theReadyandDollarStatetransitions (the same neighborhood as theisBeginAtomiccheck).Symptom
When this happens during the Supabase platform's GitHub branching auto-apply, the preview branch lands in
MIGRATIONS_FAILEDand the "Supabase Preview" GitHub check reportsSKIPPED. There is no inline error annotation on the PR — the only signal an author sees is theSKIPPEDstatus.Minimal reproduction
A single migration file is enough:
Apply via the Supabase platform's GitHub branching auto-apply on a fresh preview branch.
A real-world variant that triggered this in our project used three levels of dollar-quoting (
$migration_<v>$ ... DO $boot$ ... EXECUTE $sql$ ... $sql$ ... END $boot$ ... $migration_<v>$) inside anUPDATE ... ARRAY[...]payload, where the outermost tag was used precisely because the body already contained$boot$as a string literal. The splitter chokes there as well, even though all three tag pairs are balanced and well-nested per PostgreSQL.Expected behavior
Both statements (
DO $boot$ ... $boot$;andCREATE TABLE example ...;) execute as separate commands. The table is created. The preview branch becomes ready.Actual behavior
The splitter bundles the two statements (and any subsequent statements in the file) into a single string and submits them together. PostgreSQL rejects the bundle with
SQLSTATE 42601. Migrations halt; the preview branch is broken.Why this matters
Every PR whose migrations touch this pattern silently breaks the Supabase Preview workflow. PR authors get no inline error annotation — only the
SKIPPEDstatus on the "Supabase Preview" check, which is easy to miss and hard to diagnose without access to the platform's internal apply logs. Nested dollar-quoting is a standard, idiomatic PostgreSQL technique (recommended by the PostgreSQL docs precisely so authors can embed dollar-tags inside dollar-tags without escaping), so it is reasonable for migration authors to expect it to Just Work.Related known bug
Issues #4746, #5020, and #5062 documented a different splitter bug in the same file:
isBeginAtomicmatchesdata[-6:] == "ATOMIC"without an identifier-rune-boundary check, so any identifier ending in those six letters (case-insensitive) trips a falseBEGIN ATOMICstate. That issue is a cousin of this one but is a distinct code path — the bug reported here is about nested dollar-quote depth tracking, not about identifier-suffix matching, and reproduces with SQL that has noatomicsubstring anywhere.Workaround
Rewrite affected migrations to avoid nested dollar-quoting:
''escaping.DOblock contains another dollar-quoted body.Both workarounds are invasive: they force migration authors to know about and route around a CLI bug, and they make some otherwise idiomatic PL/pgSQL patterns unavailable.
Suggested fix direction
Fix the splitter's nesting tracking in
pkg/parser/state.go:isBeginAtomiccheck should also be guarded by an identifier-rune-boundary check (related to the_atomiccousin bug).Either change in isolation does not fix the other; both deserve attention.
Environment
pkg/parser/state.go.supabaseCLI invocation. Reproducible by anyone using the platform GitHub integration with a migration file matching the minimal repro above.