Skip to content

Statement splitter mis-parses nested dollar-quoted SQL — fails with SQLSTATE 42601 on platform auto-apply #5146

@ron-waypoint

Description

@ron-waypoint

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).

Metadata

Metadata

Assignees

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