Commit Graph

2 Commits

Author SHA1 Message Date
pezkuwichain e18ba679be db: replace the migration history with a baseline of the live schema
The migrations did not describe this database, and could not be made to.

Three findings, in the order they surfaced:

  * 25 functions declared across six migrations — all recorded as applied — did
    not exist. Their tables did. Two were reached by the app, so merchant tier
    upgrades and post-trade reputation updates had been quietly dead. This only
    came to light because a user hit "Could not find the function
    public.upsert_user_profile(...)" while toggling a notification setting.

  * admin_roles has three conflicting definitions across the set and production
    matches none of them. 001 says (id, user_id, role, granted_by, granted_at),
    COMBINED says (user_id, role, created_at), production has
    (id, user_id, role, permissions, created_at, updated_at).

  * Applied to an empty database, five migrations fail. The legacy 0NN filenames
    sort before the 14-digit timestamps they depend on — "013" < "20241117054600"
    — so 013 runs before the migration creating the table it alters. The set
    could never have been replayed from scratch.

So it could not be tested, could not rebuild the database, and did not match what
was running. Widening or patching it would have been dressing up a history that
was already fiction.

The baseline is a pg_dump of the live public schema, which matches production by
construction. Privileges are included deliberately: the REVOKEs on
lock_escrow_internal, release_escrow_internal, refund_escrow_internal and
request_withdraw are the 20260725030000 hardening, and dropping them would hand
fund movement back to anon.

Verified step by step against production before committing:
  - applies to an empty database cleanly, after three real obstacles were fixed
    (extensions live in the `extensions` schema, supabase_admin membership,
    platform-level ALTER DEFAULT PRIVILEGES that cannot apply outside Supabase)
  - produces 83 tables / 39 functions / 222 indexes / 360 policies — identical
    counts to production
  - recorded as applied in supabase_migrations without touching the 37 existing
    rows, then dry-run confirmed "up to date — no pending migrations", so the
    next deploy will not try to replay it over live tables
  - drift check now reports "every declared function is present"; the known-gaps
    list drops from 22 entries to zero

The old files move to migrations/archive/ rather than being deleted — they are
the only record of why parts of this look the way they do. Their README says
plainly not to run them, and why.

CI now applies migrations to an empty Postgres on every PR. That check was
impossible while the old set was the starting point; it is the thing that stops
this class of drift from being discovered by a user again.
2026-07-31 18:49:59 -07:00
pezkuwichain 658c99b9bb fix(db): repair functions their migrations recorded but never created
A user hit "Could not find the function public.upsert_user_profile(...) in the
schema cache" when toggling push notifications. The function was missing, and so
were the profiles columns it writes — while schema_migrations listed both 002 and
004 as applied.

That pattern turned out to be widespread. Comparing every function declared
across the migrations against the live database: 25 are missing, from six
migrations all recorded as applied. Their tables exist; only the function bodies
are absent. Those files carry "Run this in Supabase SQL Editor" headers, so they
were pasted in by hand before apply-migrations.sh existed and a run that stopped
partway was still recorded. The runner has skipped them ever since, which is why
this stayed invisible until it surfaced as a user-facing error.

apply-migrations.sh is not at fault: it wraps each migration and its tracking row
in one transaction with ON_ERROR_STOP, so it cannot half-record anything. It
inherited a dirty history.

Three of the 25 are actually reached by the app, so those are repaired here:

  apply_for_tier_upgrade   MerchantApplication.tsx:213 - tier upgrades were dead
  check_tier_eligibility   called by the above
  update_p2p_reputation    shared/lib/p2p-fiat.ts:803 - reputation never updated

This is forward-only repair, not a replay of the source files. Replaying them
would abort on their bare CREATE POLICY/INDEX/TRIGGER statements now that the
tables exist, and 20241117054602 in particular would overwrite 016's newer
cancel_expired_trades with its own older definition.

Verified before writing: every table the three functions touch is present, and
the whole migration was run inside BEGIN/ROLLBACK against production — three
functions created, signatures matching what the callers pass, then rolled back
leaving nothing behind.

The remaining 22 stay missing on purpose and are listed in known-schema-gaps.txt.
Nothing calls them, and several are trigger bodies whose triggers were never
created either, so creating them would switch on behaviour that has never run.

Also adds a drift check to apply-migrations.sh: after applying, it compares
declared functions against the database and flags anything missing that is not in
the known-gaps list. Being recorded as applied is not proof of having been
applied, and that gap should never again be discovered by a user. The check
already earned itself — it caught update_p2p_reputation, which my own first pass
had missed to a regex that dropped digits from function names.

upsert_user_profile and the profiles notification columns were applied directly
to the database from their existing migrations (002, 004), which are fully
idempotent; no new migration was needed for them.
2026-07-30 09:16:01 -07:00