Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
206 lines
11 KiB
Markdown
206 lines
11 KiB
Markdown
---
|
|
name: writing-migrations
|
|
description: Generate Supabase SQL migrations at deploy time from the session branch diff. Load this when your developer dispatch asks you to generate the deploy-time migration (it points you at the shared <base>/simple worktree). This is NOT a feature ticket — the workflow below replaces the normal ticket rules: here you DO write SQL under supabase/migrations/, and you do not edit TS/TSX/CSS.
|
|
---
|
|
|
|
# Writing Migrations (deploy-time round)
|
|
|
|
## Overview
|
|
|
|
A workflow that turns a session's *code* changes into the *minimal* SQL that
|
|
makes the real Supabase schema match what the session's app now expects —
|
|
nothing more.
|
|
You (the `developer`) have been asked to generate the deploy-time migration. The
|
|
"never write migrations / no schema" rule of normal ticket work does NOT apply
|
|
here: writing SQL under `supabase/migrations/` is the whole job. Do not edit any
|
|
TS/TSX/CSS — the schema diff already comes from the session branch; you only
|
|
translate it to SQL.
|
|
Exit criterion: either a committed, idempotent migration whose delta is provably the net schema change, or an explicit `NO_MIGRATION_NEEDED` when there is no schema impact.
|
|
|
|
Your worktree is `<WORKTREE_BASE>/simple` on `<SESSION_SHORT_ID>/simple`, forked from `session/<SESSION_SHORT_ID>` (`<WORKTREE_BASE>` =
|
|
`/tmp/<$CLAUDE_PROJECT_DIR with every "/" replaced by "_">/<SESSION_ID>`). Every Bash call must `cd <WORKTREE_PATH> && …` (stateless shells).
|
|
|
|
## When to Use
|
|
|
|
- The deploy-time migration round, after a session's app changes have merged to `session/<SESSION_SHORT_ID>` and need their schema counterpart.
|
|
- Triggered only by the orchestrator dispatching a `developer` that loads this skill, never invoked ad hoc by a feature developer (per-ticket developers never write SQL).
|
|
|
|
Skip (write nothing, report `NO_MIGRATION_NEEDED`) when the net diff has no
|
|
schema impact: CSS, layout, copy, or test-only changes.
|
|
|
|
## Process
|
|
|
|
Each step ends in a checkpoint, do not advance until its evidence holds.
|
|
|
|
### 1. Compute the session's net change
|
|
|
|
```bash
|
|
cd <WORKTREE_PATH>
|
|
git diff session-base/<SESSION_SHORT_ID>..session/<SESSION_SHORT_ID>
|
|
```
|
|
|
|
This is the branch's full diff since creation. Do NOT use `git merge-base`
|
|
(it collapses after the first promotion). Do NOT diff against the base branch
|
|
(other sessions pollute it).
|
|
|
|
**Checkpoint:** you have the full session diff, produced from the two-dot range above (not merge-base, not the base branch).
|
|
|
|
### 2. Identify schema-relevant changes
|
|
|
|
From that diff, keep only changes that imply a database schema change:
|
|
- Entity TypeScript types (e.g. `src/**/types.ts`, resource type defs).
|
|
- Fake-data generators that add/remove fields.
|
|
- dataProvider resource registrations (new resource = new table).
|
|
Ignore CSS, component layout, copy, tests.
|
|
|
|
**Checkpoint:** you have an explicit list of schema-relevant changes (possibly
|
|
empty). If empty, jump to step 3's no-op path.
|
|
|
|
### 3. Regenerate ONE net migration against what is ALREADY DEPLOYED (session-base)
|
|
|
|
"Deployed" = the migrations present at `session-base/<SESSION_SHORT_ID>` (the fork
|
|
anchor, i.e. the last deployed state). A migration YOU generated in an EARLIER
|
|
iteration of this same session is NOT deployed: it sits on the session branch but was
|
|
never pushed. Treating it as deployed is exactly what makes iterations STACK (add
|
|
column `foo`, then a second migration to rename it to `bar`, ...) instead of one clean
|
|
migration. The user does not want to push N migrations per change.
|
|
|
|
So, before authoring any SQL:
|
|
|
|
1. Find this session's own prior migration(s):
|
|
`git diff --diff-filter=A session-base/<SESSION_SHORT_ID>..session/<SESSION_SHORT_ID> -- supabase/migrations/`
|
|
(files ADDED since the fork = generated by an earlier iteration of this session).
|
|
2. **Delete them.** You will regenerate ONE fresh file that is the whole net change.
|
|
3. Author SQL for the NET delta between the desired schema (from the TS types) and the
|
|
schema as of `session-base` ONLY. Never layer an incremental delta on top of your
|
|
own prior migration.
|
|
|
|
Result: however many times the user iterates before deploy, the session carries
|
|
exactly ONE migration = `net(session-base -> final schema)`. If the net diff has no
|
|
schema impact, write **nothing** and report `NO_MIGRATION_NEEDED`.
|
|
|
|
**Hard boundary:** overwrite-and-regenerate is safe ONLY because the migration is not
|
|
yet deployed. Once it is pushed / promoted it is immutable; a later change is a NEW
|
|
forward migration, never an edit of the pushed one.
|
|
|
|
**Surface note.** Technical-harness writes to `supabase/migrations/`. On the web-chat
|
|
surface (CRM Builder) the launcher stages migrations in `supabase/migrations-pending/`
|
|
and promotes them to `supabase/migrations/` after the user confirms: follow the
|
|
launcher's staging, do not invent your own pending/promoted handling. The
|
|
regenerate-one-net-migration rule above applies either way (it is about NOT stacking,
|
|
independent of which directory the launcher stages into).
|
|
|
|
**Checkpoint:** at most ONE session-owned migration file exists, and its SQL is the
|
|
full net delta from `session-base` (not an increment on a prior session migration).
|
|
|
|
### 4. Write idempotent SQL
|
|
|
|
Write to `supabase/migrations/<YYYYMMDDHHMMSS>_<SESSION_SHORT_ID>_migration_<slug>.sql`
|
|
(timestamp via `date -u +%Y%m%d%H%M%S`). Use `IF NOT EXISTS` / `IF EXISTS`,
|
|
correct column types matching the TS types, FKs, and RLS for new tables (RLS
|
|
enabled + policies, never `USING (true)`).
|
|
|
|
**Checkpoint:** re-running the migration on an already-migrated database is a
|
|
no-op (every statement is guarded), and every new table has RLS enabled with at least one non-`USING (true)` policy.
|
|
|
|
### 5. View-recreation rule (BLOCKING correctness)
|
|
|
|
When a migration adds a column on a table referenced by a view in
|
|
`supabase/schemas/03_views.sql`, recreate the view with **`CREATE OR REPLACE
|
|
VIEW`** and place the new column as the **very last item** in the SELECT
|
|
list — after every existing column AND after every existing computed `AS`
|
|
alias (e.g. `count(...) as nb_deals`).
|
|
|
|
PostgreSQL forbids any ordinal shift in an existing view's SELECT list
|
|
(error 42P16). "Between the raw columns and the computed aggregates" is
|
|
**still a shift** — the aggregate's position increases by one. There are no
|
|
exceptions. The mechanical rule is: positions 1..N of the new view must be
|
|
identical to positions 1..N of the old view; the new column is position N+1.
|
|
|
|
Why not `DROP VIEW … CASCADE; CREATE VIEW …`? It silently drops dependent
|
|
views/materialized views/rules, loses any explicit `REVOKE`s on the view
|
|
(re-granted by default privileges at the next deploy without warning), and
|
|
reads as a destructive operation in audit. Reserve it for cases `CREATE OR
|
|
REPLACE` genuinely can't handle (column **removal** or **rename**). If you
|
|
use it, enumerate dropped dependents in a SQL comment and re-create them in
|
|
the same migration.
|
|
|
|
PostgREST queries the view, not the table — a missing update makes the
|
|
column invisible to the app. The view's column order doesn't matter to API
|
|
consumers (they address by name), so "append at end" has no API cost.
|
|
|
|
Example — adding `importance` to `companies_summary` (the view ends with
|
|
`count(...) as nb_deals, count(...) as nb_contacts`):
|
|
|
|
```sql
|
|
create or replace view public.companies_summary with (security_invoker = on) as
|
|
select
|
|
c.id, c.created_at, c.name, …, c.logo,
|
|
count(distinct d.id) as nb_deals,
|
|
count(distinct co.id) as nb_contacts,
|
|
c.importance -- LAST, after every existing item
|
|
from public.companies c
|
|
left join public.deals d on c.id = d.company_id
|
|
left join public.contacts co on c.id = co.company_id
|
|
group by c.id;
|
|
```
|
|
|
|
Then update `supabase/schemas/03_views.sql` to the **same order** — the
|
|
declarative schema must mirror the deployed view, including the chronological
|
|
"append at end" placement of newer columns. Don't reorder the schema file to
|
|
look prettier — schema drift breaks future `supabase db diff` generations.
|
|
|
|
**Checkpoint:** for every table that gained a column AND feeds a view,
|
|
positions 1..N of the recreated view match the old view exactly, the new column is position N+1, and `03_views.sql` mirrors that order.
|
|
|
|
### 6. Commit and hand off
|
|
|
|
Commit the SQL on `<SESSION_SHORT_ID>/simple`:
|
|
|
|
```bash
|
|
cd <WORKTREE_PATH> && git add supabase/migrations && git commit -m "migration(<SESSION_SHORT_ID>): <slug>"
|
|
```
|
|
|
|
Then stop and emit the output contract as your very last line — one of:
|
|
|
|
- `DONE: branch=<SESSION_SHORT_ID>/simple migration=<filename> summary=<what the SQL does>`
|
|
- `NO_MIGRATION_NEEDED` — only after computing the diff (step 1) and confirming no schema impact.
|
|
- `FAILED: <one-line reason>`
|
|
|
|
SubagentStop hooks (typecheck/prettier/unit/e2e) run automatically; they should
|
|
pass since you only touched SQL. The orchestrator then sends you to
|
|
quality-reviewer (`MODE: migration-review`) and the merger.
|
|
|
|
For Postgres correctness you may load `Skill({skill: "supabase-postgres-best-practices"})`.
|
|
|
|
**Checkpoint:** the migration is committed on `<SESSION_SHORT_ID>/simple` and the SubagentStop validation chain is green.
|
|
|
|
## Rationalizations
|
|
|
|
| Rationalization | Reality |
|
|
|---|---|
|
|
| "I'll diff against the base branch, it's close enough." | Other sessions pollute the base branch. Only the two-dot `session-base..session` range is the true net change. |
|
|
| "This column is probably already deployed, I'll re-emit it to be safe." | Re-emitting a deployed change is drift, not safety. Guard with `IF NOT EXISTS` and emit only the delta. |
|
|
| "The view still selects all the right columns, order doesn't matter." | Postgres rejects any ordinal shift (42P16). Order is a hard correctness constraint, not cosmetics. |
|
|
| "DROP VIEW CASCADE then CREATE is simpler." | It silently drops dependents and loses REVOKEs. Use `CREATE OR REPLACE` unless you are removing/renaming a column. |
|
|
| "No schema change, but I'll write an empty migration anyway." | A no-op deploy is valid. Write nothing and report `NO_MIGRATION_NEEDED`. |
|
|
|
|
## Red Flags
|
|
|
|
- Diff computed via `git merge-base` or against the base branch.
|
|
- A migration statement without an `IF [NOT] EXISTS` guard.
|
|
- A new table with no RLS, or an RLS policy using `USING (true)`.
|
|
- A view recreated by `DROP … CASCADE` for a mere column *addition*.
|
|
- A new column inserted anywhere but the last position of an existing view.
|
|
- `03_views.sql` and the migration's view definition disagreeing on column order.
|
|
- Re-emitting SQL already present in `supabase/migrations/`.
|
|
|
|
## Verification
|
|
|
|
- [ ] Diff taken from `session-base/<SESSION_SHORT_ID>..session/<SESSION_SHORT_ID>`.
|
|
- [ ] Every emitted statement is an incremental delta not already in `supabase/migrations/`.
|
|
- [ ] All statements are idempotent (`IF [NOT] EXISTS`).
|
|
- [ ] New tables have RLS enabled with real policies (never `USING (true)`).
|
|
- [ ] Affected views recreated via `CREATE OR REPLACE`, new column last, matching `03_views.sql`.
|
|
- [ ] SQL committed on `<SESSION_SHORT_ID>/simple`; SubagentStop validation green — OR `NO_MIGRATION_NEEDED` reported with no file written.
|