docs(clickhouse): require max+1 numbering and idempotent DDL (#3633)

## Summary

Codify two rules for ClickHouse migration authors that came out of the
029/030 ordering incident on the TRI-9367 test cloud deploy:

1. **Number files to `max(existing) + 1`, never slot in below the
latest.** Goose runs in strict mode in the cloud deploy pipeline and
refuses to apply a missing version below the current version — slotting
a file in below an already-applied number blocks the next deploy.
2. **DDL must be idempotent** (`ADD COLUMN IF NOT EXISTS`, `DROP COLUMN
IF EXISTS`, `CREATE TABLE IF NOT EXISTS`, etc.) so a retry or
out-of-order apply (`goose up --allow-missing` for local recovery,
manual fixups) is a no-op rather than an error.

## Where the rules live

- `internal-packages/clickhouse/CLAUDE.md` — full rules + example for
migration authors (and AI agents writing migrations).
- `.claude/REVIEW.md` — added a 🔴 finding under "What makes a 🔴
Important finding" so PR reviewers flag either fault as blocking.

The existing migration files are left untouched; the idempotency
requirement applies going forward.

## Test plan

- [ ] Next ClickHouse migration PR uses `IF NOT EXISTS` / `IF EXISTS`
forms
- [ ] No new migration files numbered below an already-applied version
on test/prod
This commit is contained in:
Eric Allam
2026-05-15 18:25:29 +01:00
committed by GitHub
parent 032b5a117a
commit 05d3ab1059
2 changed files with 32 additions and 3 deletions
+1
View File
@@ -11,6 +11,7 @@ Reserve 🔴 for things that would page someone or block a rollback. In this cod
- A Redis data shape used by both versions changes in place. New shapes need a new key namespace.
- A migration is not backward-compatible with the prior image.
- **Schema / migration safety.** Prisma migrations must be backward-compatible with the prior deploy. Adding NOT NULL without a default, dropping a column an old image still reads, renaming a column — all 🔴.
- **ClickHouse migration ordering + idempotency.** Goose runs in strict mode in the deploy pipeline and refuses to apply a missing version below the current version — slotting a new file in below the latest already-applied version blocks the deploy. New ClickHouse migration files MUST use the next available number (`max(files in internal-packages/clickhouse/schema/) + 1`); if main has added migrations while you've been on a branch, renumber yours. DDL must also be idempotent (`ADD COLUMN IF NOT EXISTS`, `DROP COLUMN IF EXISTS`, `CREATE TABLE IF NOT EXISTS`, `ADD INDEX IF NOT EXISTS`) so a partial / `--allow-missing` apply elsewhere doesn't fail on retry. Either fault is 🔴 — both break test/prod deploys. Rules live in `internal-packages/clickhouse/CLAUDE.md`.
- **Queue / concurrency correctness.** RunQueue, MarQS (V1, legacy), redis-worker — any change to enqueue / dequeue / locking semantics. Re-derive the invariant on paper before flagging or accepting.
- **Missing index on a hot table.** New Prisma queries against `TaskRun`, `TaskRunExecutionSnapshot`, `JobRun`, `Project`, etc. must use an existing index. Check `internal-packages/database/prisma/schema.prisma` for the relevant `@@index` lines — don't guess and don't propose `EXPLAIN`.
- **Recovery-path queries.** Any `TaskRun.findFirst` / `findMany` added to a schedule, run-recovery, or restart loop. Recovery fan-outs (Redis crash, restart storms) turn "rare indexed query" into a DB incident. 🔴 even if indexed.
+31 -3
View File
@@ -4,18 +4,46 @@
## Migrations
Goose-format SQL migrations live in `schema/`. Create new numbered files:
Goose-format SQL migrations live in `schema/`. Two rules below are load-bearing — both can block a deploy.
### Rule 1: number to `max + 1`, never slot in
Goose runs in strict mode in the deploy pipeline. If a migration file numbered *below* the version currently recorded in `goose_db_version` ever shows up, goose refuses to apply it and the deploy fails:
```
goose run: error: found 1 missing migrations before current version 30:
version 29: 029_add_task_kind_to_task_runs_v2.sql
```
When adding a migration:
1. Look at `schema/` and take the largest existing number, call it `N`.
2. Name your file `0(N+1)_descriptive_name.sql`.
3. If you've been on a branch while main added migrations, **rebase and renumber** before opening the PR — a file numbered below the new max will block the next deploy after your PR merges.
### Rule 2: DDL must be idempotent
Migrations can be applied out of order in some environments (`goose up --allow-missing` for local recovery, manual fixups, etc.) and may be retried. Always use idempotent forms so a re-apply is a no-op:
```sql
-- +goose Up
ALTER TABLE trigger_dev.your_table
ADD COLUMN new_column String DEFAULT '';
ADD COLUMN IF NOT EXISTS new_column String DEFAULT '';
-- +goose Down
ALTER TABLE trigger_dev.your_table
DROP COLUMN new_column;
DROP COLUMN IF EXISTS new_column;
```
Equivalent forms for other DDL:
- `CREATE TABLE IF NOT EXISTS …`
- `DROP TABLE IF EXISTS …`
- `ADD INDEX IF NOT EXISTS …` / `DROP INDEX IF EXISTS …`
- `CREATE MATERIALIZED VIEW IF NOT EXISTS …` / `DROP VIEW IF EXISTS …`
ClickHouse supports `IF [NOT] EXISTS` on all of the above. Older migrations in this directory predate the rule and are not idempotent — leave them as-is unless you're explicitly hardening one.
## Naming Conventions
- `raw_` prefix for input tables (where data lands first)