You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 terminatedcreate or replacefunctionpublic.probe_splitter() returns integer
language sql
immutable
begin atomic
select1as pending;
end;
-- APPLIES: identical, alias renamed so it no longer ends in those three letterscreate or replacefunctionpublic.probe_splitter() returns integer
language sql
immutable
begin atomic
select1as 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 rune isIdentifierRune — on both sides,
for ATOMIC and for BEGIN.
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 preceding window when the delimiter is END_ATOMIC.
Two further symptoms of the same line, for completeness
case … end inside 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 /reopen if it still reproduces — it does, on v2.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 ). AtomicState is also used with delimiter: []byte{')'}
(≈ L45), and a ) returns ReadyState, which collapses the nesting: select coalesce(p_pending, 0);
applies, while select 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.
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 owncommit;
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 released v2.117.0 source (the
closer is unchanged).
OS: Linux.
No --create-ticket id: the reproduction above is self-contained and needs no project telemetry.
Describe the bug
AtomicState.Nextends aBEGIN ATOMICfunction body as soon as the last three bytes of the accumulatedbuffer 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 aliaswhose name merely ends in those three letters (
pending,pending_change,append,legend, …) closesthe body early. The statement is then emitted at the next
;, truncated, and Postgres answers:The migration is valid SQL:
psql -fapplies the same file without complaint. It fails only through theCLI (
supabase db start,supabase migration up,supabase db push), which makes it invisible to everycheck 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:
Run either as the only migration in a project and
supabase migration up --db-url <any database>. The erroroutput 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.goatv2.117.0(still present in the latest release):isBeginAtomic(≈ L56-77) rejects a match whose neighbouring runeisIdentifierRune— on both sides,for
ATOMICand forBEGIN.AtomicState.Next(≈ L197-209) does only:No boundary check.
isIdentifierRune(≈ L79) is right there and already used by the opener, so the fixlooks like applying the same guard to the byte preceding
windowwhen 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 aninvitation to
/reopenif it still reproduces — it does, onv2.117.0. It is a real keyword, so it isarguably a harder call than the identifier case above.
).AtomicStateis also used withdelimiter: []byte{')'}(≈ L45), and a
)returnsReadyState, which collapses the nesting:select coalesce(p_pending, 0);applies, while
select coalesce((select 0), p_pending);fails. So "wrap it in parentheses" is not areliable workaround.
Double-quoting the identifier (
select 1 as "pending";) does work, because the quote state suppresses thecheck. Comments are also safe.
Why this is not a duplicate
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. Theboundary checks in
isBeginAtomicare that fix. The closer never received the equivalent.case … endsymptom only, and was stale-closed rather than declined on the merits.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 likethe single thing to fix.
Impact
A truncation that produces
42601is at least loud. One caveat worth flagging: the CLI wraps a migrationfile 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 amigration applied and unrecorded, and the retry then collides with "already exists".
System information
2.109.1; root cause verified by inspection of the releasedv2.117.0source (thecloser is unchanged).
--create-ticketid: the reproduction above is self-contained and needs no project telemetry.