Repository navigation
Description
Describe the bug
AtomicState.Next ends a BEGIN ATOMIC function body as soon as the last three bytes of the accumulated
buffer are end (case-insensitively), with no identifier-boundary check — while the opener,
isBeginAtomic, checks boundaries on both sides. Because of that asymmetry, an ordinary column or alias
whose name merely ends in those three letters (pending, pending_change, append, legend, …) closes
the body early. The statement is then emitted at the next ;, truncated, and Postgres answers:
ERROR: syntax error at end of input (SQLSTATE 42601)
The migration is valid SQL: psql -f applies the same file without complaint. It fails only through the
CLI (supabase db start, supabase migration up, supabase db push), which makes it invisible to every
check that does not go through the splitter and turns it into a CI-only failure.
To Reproduce
Two files, 131 bytes each, differing in one letter. The first fails, the second applies:
-- FAILS: the body closes inside `pending`, so the statement is cut at the `;` and never terminated
create or replace function public.probe_splitter() returns integer
language sql
immutable
begin atomic
select 1 as pending;
end;-- APPLIES: identical, alias renamed so it no longer ends in those three letters
create or replace function public.probe_splitter() returns integer
language sql
immutable
begin atomic
select 1 as pxnding;
end;Run either as the only migration in a project and supabase migration up --db-url <any database>. The error
output echoes the statement it sent, and you can see it stop mid-body with no terminator.
Not length-dependent: padding the failing file from 4.5 KB to 35 KB changes nothing, and the two files above
are byte-for-byte the same length.
Expected behavior
An identifier that happens to end in the delimiter keyword is not the delimiter. The body should close only
at a standalone END.
Root cause
In apps/cli-go/pkg/parser/state.go at v2.117.0 (still present in the latest release):
-
isBeginAtomic(≈ L56-77) rejects a match whose neighbouring runeisIdentifierRune— on both sides,
forATOMICand forBEGIN. -
AtomicState.Next(≈ L197-209) does only:window := data[len(data)-len(s.delimiter):] if strings.EqualFold(string(window), string(s.delimiter)) { return &ReadyState{} }
No boundary check.
isIdentifierRune(≈ L79) is right there and already used by the opener, so the fix
looks like applying the same guard to the byte precedingwindowwhen the delimiter isEND_ATOMIC.
Two further symptoms of the same line, for completeness
case … endinside an atomic body closes it early. That is Migrations break when a BEGIN ATOMIC statement includes CASE #3474, which was closed by staleness with an
invitation to/reopenif it still reproduces — it does, onv2.117.0. It is a real keyword, so it is
arguably a harder call than the identifier case above.- Parentheses only protect until the first
).AtomicStateis also used withdelimiter: []byte{')'}
(≈ L45), and a)returnsReadyState, which collapses the nesting:select coalesce(p_pending, 0);
applies, whileselect coalesce((select 0), p_pending);fails. So "wrap it in parentheses" is not a
reliable workaround.
Double-quoting the identifier (select 1 as "pending";) does work, because the quote state suppresses the
check. Comments are also safe.
Why this is not a duplicate
- SQL splitter matches 'atomic' too aggressively, triggering SQLSTATE 42601 #5062 (closed, completed) and Statement splitter misparses
atomicin function names as BEGIN ATOMIC keyword #5020 / Database migration fails on local server when table or column name contains 'atomic' #4746 fixed the opener —atomicappearing inside a name. The
boundary checks inisBeginAtomicare that fix. The closer never received the equivalent. - Migrations break when a BEGIN ATOMIC statement includes CASE #3474 reports the
case … endsymptom only, and was stale-closed rather than declined on the merits. - Statement splitter mis-parses nested dollar-quoted SQL — fails with SQLSTATE 42601 on platform auto-apply #5146 is about nested dollar quoting, a different state.
Happy to fold this into #3474 instead if you would rather keep one thread — I filed it separately because the
plain-identifier case is broader than CASE, and because the asymmetry between opener and closer looks like
the single thing to fix.
Impact
A truncation that produces 42601 is at least loud. One caveat worth flagging: the CLI wraps a migration
file in a transaction, so the usual case rolls back cleanly — but a file carrying its own commit;
before the trigger leaves the earlier half committed with no row written to schema_migrations, i.e. half a
migration applied and unrecorded, and the retry then collides with "already exists".
System information
- Reproduced on CLI
2.109.1; root cause verified by inspection of the releasedv2.117.0source (the
closer is unchanged). - OS: Linux.
- No
--create-ticketid: the reproduction above is self-contained and needs no project telemetry.
Activity
L4XB commentedon Sep 11, 2026
A fix for this is ready in #6570 (TypeScript splitter and the Go one, with tests for aliases ending in
end, identifiers starting withend, and the ways a realENDcan be followed). The PR was closed automatically because this issue does not carry theopen-for-contributionlabel yet; if a maintainer adds it, I will reopen the PR.7ttp commentedon Sep 14, 2026
thanks folks! for the prs :) 💚
will update once it lands in...
fix(cli): respect atomic END boundaries (CLI-2405) (supabase#6587)