diff --git a/Justfile b/Justfile index bc427e47ff..e3e98f71a9 100644 --- a/Justfile +++ b/Justfile @@ -231,6 +231,13 @@ test-unit: #!/usr/bin/env bash if command -v cargo-nextest &>/dev/null; then cargo nextest run -p buzz-core -p buzz-auth --lib + # buzz-db migrator/lint tests: pure SQL-parsing unit tests (no infra). + # They guard the embedded-migrator invariant (exactly the consolidated + # 0001; cutover/backfill stays an operator script, not startup state) + # and the tenant-scoping lints. The Postgres-backed buzz-db tests are + # #[ignore]d, so --lib runs only the infra-free set. Without this gate a + # stray file in migrations/ or a broken lint ships green. + cargo nextest run -p buzz-db --lib # Multi-tenant conformance gate (buzz-conformance): the independent # replay checker + golden fixtures. No infra — pure in-process trace # replay — so it belongs in the unit job. Run all targets (lib + the diff --git a/migrations/0002_backfill_default_community.sql b/scripts/cutover/1321_backfill_default_community.sql similarity index 95% rename from migrations/0002_backfill_default_community.sql rename to scripts/cutover/1321_backfill_default_community.sql index 65739b0ee7..fc5146ff48 100644 --- a/migrations/0002_backfill_default_community.sql +++ b/scripts/cutover/1321_backfill_default_community.sql @@ -1,4 +1,4 @@ --- 0002_backfill_default_community.sql +-- scripts/cutover/1321_backfill_default_community.sql -- -- One-off cutover: single-community (pre-1321) Postgres -> multi-tenant (1321) -- schema. Assigns EVERY existing row to one default community derived from the @@ -46,15 +46,18 @@ -- PRECONDITIONS -- * DB is the pre-1321 single-community schema (no community_id columns, no -- `communities` table). Guard below refuses to run otherwise. --- * Run psql from the buzz repo root so the \i path resolves. +-- * Run psql with this script via -f (not piped on stdin): the Phase B +-- include below uses \ir, which resolves relative to THIS file, so the +-- repo's migrations/0001 is found regardless of the operator's cwd. -- * TAKE A pg_dump / PVC SNAPSHOT FIRST. No down-path; the snapshot IS the -- rollback. -- --- USAGE (from the buzz repo root) +-- USAGE (run from anywhere; \ir below resolves the schema include relative to +-- this file, so cwd does not matter) -- psql "$DATABASE_URL" \ -- -v host="'sprout-oss.stage.blox.sqprod.co'" \ -- -v ON_ERROR_STOP=1 \ --- -f migrations/0002_backfill_default_community.sql +-- -f scripts/cutover/1321_backfill_default_community.sql -- -- After commit: boot the 1321 relay with BUZZ_AUTO_MIGRATE=false. The schema is -- already correct; the relay's idempotent boot-time @@ -128,7 +131,9 @@ END $move_types$; -- ── Phase B: the authoritative 1321 schema, verbatim, into clean public ────── -\i migrations/0001_initial_schema.sql +-- \ir = include relative to THIS script's directory (scripts/cutover/), so the +-- repo's migrations/0001 resolves regardless of the operator's cwd. +\ir ../../migrations/0001_initial_schema.sql -- ── Phase C: the one default community, normalized like the relay does ─────── INSERT INTO public.communities (host) diff --git a/scripts/cutover/README.md b/scripts/cutover/README.md new file mode 100644 index 0000000000..27388bdaa2 --- /dev/null +++ b/scripts/cutover/README.md @@ -0,0 +1,87 @@ +# 1321 cutover: single-community → multi-tenant + +`1321_backfill_default_community.sql` is a **one-off operator script**, not a +startup migration. It takes a pre-1321 (single-community) Buzz Postgres to the +1321 multi-tenant schema, assigning every existing row to one default community +derived from the deployment host. + +> It is **not** embedded in the relay binary. The relay's `sqlx::migrate!` +> embeds only `migrations/0001_initial_schema.sql` — the consolidated 1321 +> schema. Fresh deployments need only that; this script exists solely to carry +> existing pre-1321 data across the rewrite. (It uses psql client features — +> `\set`, `\ir`, `:'var'` interpolation, `\gset` — that the embedded sqlx +> migrator cannot run, which is why it must live here and not under +> `migrations/`.) + +## When you need it + +Only when upgrading a Postgres that already holds **pre-1321 single-community +data** to 1321. A brand-new deployment does **not** run this — it provisions +from `migrations/0001_initial_schema.sql` (or `schema/schema.sql`) directly. + +## Preconditions + +- The DB is pristine pre-1321: no `communities` table, no `community_id` + columns, no leftover `legacy` schema. The script's guard **refuses to run** + otherwise (already-migrated, partially-migrated, or failed-prior-run states + all raise and abort — see the guard block at the top of the script). +- **Take a `pg_dump` / PVC snapshot first.** There is no down-path. The + snapshot **is** the rollback. + +## Run it + +The whole script runs in **one transaction** (all-or-nothing). Pass the +deployment host as `:host`; it is normalized exactly as the relay normalizes a +community host (`rtrim(lower(host), '.')`). + +```sh +psql "$DATABASE_URL" \ + -v host="'your-deployment-host.example.com'" \ + -v ON_ERROR_STOP=1 \ + -f scripts/cutover/1321_backfill_default_community.sql +``` + +`-f` (not stdin) matters: the script `\ir`-includes +`../../migrations/0001_initial_schema.sql` **relative to its own location**, so +it resolves the canonical 1321 schema regardless of your cwd. + +### What it does + +1. Renames all pre-1321 tables and enum types aside into a `legacy` schema. +2. `\ir`-includes the repo's `0001` verbatim → the correct 1321 schema in a + clean `public` (structurally identical to a fresh 1321 DB **by + construction** — no hand-transcription). +3. Creates the one default community from `:host`. +4. Copies every row forward, stamped with that `community_id` (`search_tsv` + regenerates as GENERATED; new-in-1321 columns default NULL). +5. `DROP SCHEMA legacy CASCADE`. + +## After it commits + +Boot the 1321 relay with **`BUZZ_AUTO_MIGRATE=false`**. The schema is already +correct; the relay's idempotent boot-time `ensure_configured_community` / +allowlist→relay_members backfill finds this community and no-ops. + +If you want the relay's sqlx migrator to consider `0001` already applied (so a +later `BUZZ_AUTO_MIGRATE=true` boot doesn't try to re-run it), seed its +`_sqlx_migrations` row to match a fresh 1321 install: + +```sql +-- version 1, description 'initial schema'; checksum must match the embedded 0001. +-- Easiest: diff _sqlx_migrations against a freshly-migrated 1321 DB and copy the row. +``` + +This is **not** required for correct operation — a fresh `0001` run against the +already-correct schema is a no-op only if the migrator's checksum matches, so +prefer leaving `BUZZ_AUTO_MIGRATE=false` unless you have a reason to seed. + +## Verify (post-commit, outside the txn) + +```sql +SELECT count(*) FROM communities; -- expect 1 +SELECT count(*) FROM events WHERE community_id IS NULL; -- expect 0 +SELECT to_regnamespace('legacy'); -- expect NULL (gone) +``` + +For a stronger guarantee, take a constraint/trigger/index/column diff of this +DB against a freshly-migrated 1321 DB — it should be empty. diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 525b430ffc..4febb5592a 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -84,6 +84,15 @@ run_unit_tests() { run_test_step "buzz-auth unit tests" \ cargo test -p buzz-auth --lib -- --nocapture + # buzz-db migrator/lint unit tests (no infra): guard the embedded-migrator + # invariant (exactly the consolidated 0001; cutover/backfill stays an operator + # script, not startup state) and the tenant-scoping lints. The Postgres-backed + # buzz-db tests are #[ignore]d; nothing here (or in integration mode below, + # which runs `cargo test -p buzz-db` without --ignored) runs them — they need a + # separate isolated-DB gate, so --lib keeps this step infra-free. + run_test_step "buzz-db unit tests" \ + cargo test -p buzz-db --lib -- --nocapture + # Multi-tenant conformance gate: independent replay checker + golden # fixtures (buzz-conformance). Pure in-process trace replay, no infra. run_test_step "buzz-conformance tests" \