mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-08-12 20:51:37 +00:00
e18ba679be
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.
89 lines
3.4 KiB
PL/PgSQL
89 lines
3.4 KiB
PL/PgSQL
-- Minimal stand-ins for what Supabase provides, so migrations can be applied to
|
|
-- a plain Postgres in CI.
|
|
--
|
|
-- The migrations reference auth.uid() 127 times, auth.users 95 times,
|
|
-- auth.role() 11 times and storage.objects 3 times. None of that exists in a
|
|
-- stock Postgres image, so without these stubs every run would fail on the
|
|
-- environment rather than on anything the migrations got wrong.
|
|
--
|
|
-- These are deliberately the thinnest thing that lets DDL resolve. They are not
|
|
-- a Supabase emulation and must never be applied to a real database: RLS
|
|
-- policies compiled against these stubs would behave differently, because
|
|
-- auth.uid() here returns NULL rather than the caller's id.
|
|
|
|
CREATE SCHEMA IF NOT EXISTS auth;
|
|
CREATE SCHEMA IF NOT EXISTS storage;
|
|
CREATE SCHEMA IF NOT EXISTS extensions;
|
|
|
|
-- Supabase installs these into the `extensions` schema, and the dumped schema
|
|
-- calls them fully qualified as extensions.uuid_generate_v4(). Installing them
|
|
-- into public instead makes the baseline fail on a name that does resolve in
|
|
-- production.
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp" SCHEMA extensions;
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto" SCHEMA extensions;
|
|
|
|
-- Enough of auth.users for foreign keys to resolve. Real Supabase has far more
|
|
-- columns; migrations only reference id and email.
|
|
CREATE TABLE IF NOT EXISTS auth.users (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
email text
|
|
);
|
|
|
|
-- In production these read the request's JWT claims. Here they return NULL,
|
|
-- which is fine for CREATE POLICY (only the expression has to type-check) and
|
|
-- is why this must not touch a real database.
|
|
CREATE OR REPLACE FUNCTION auth.uid() RETURNS uuid
|
|
LANGUAGE sql STABLE AS $$ SELECT NULL::uuid $$;
|
|
|
|
CREATE OR REPLACE FUNCTION auth.role() RETURNS text
|
|
LANGUAGE sql STABLE AS $$ SELECT NULL::text $$;
|
|
|
|
CREATE OR REPLACE FUNCTION auth.email() RETURNS text
|
|
LANGUAGE sql STABLE AS $$ SELECT NULL::text $$;
|
|
|
|
-- storage.objects: three migrations attach policies to it.
|
|
CREATE TABLE IF NOT EXISTS storage.objects (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bucket_id text,
|
|
name text,
|
|
owner uuid
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS storage.buckets (
|
|
id text PRIMARY KEY,
|
|
name text
|
|
);
|
|
|
|
-- Roles the migrations GRANT to. CREATE ROLE is not idempotent, hence the guard.
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN
|
|
CREATE ROLE anon NOLOGIN;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN
|
|
CREATE ROLE authenticated NOLOGIN;
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN
|
|
CREATE ROLE service_role NOLOGIN BYPASSRLS;
|
|
END IF;
|
|
-- The dump carries ALTER DEFAULT PRIVILEGES FOR ROLE supabase_admin, which
|
|
-- requires membership in that role rather than just its existence.
|
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'supabase_admin') THEN
|
|
CREATE ROLE supabase_admin NOLOGIN;
|
|
END IF;
|
|
END $$;
|
|
|
|
GRANT supabase_admin TO CURRENT_USER;
|
|
|
|
GRANT USAGE ON SCHEMA public, auth, storage TO anon, authenticated, service_role;
|
|
|
|
-- Supabase ships a realtime publication that migrations add tables to.
|
|
-- Without it, ALTER PUBLICATION fails on the environment rather than on
|
|
-- anything the migration got wrong.
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'supabase_realtime') THEN
|
|
CREATE PUBLICATION supabase_realtime;
|
|
END IF;
|
|
END $$;
|