Migration baseline + squash policy
Ce contenu n’est pas encore disponible dans votre langue.
Status note (2026-07-13): squash point #1 executed at the v0.1.0 tag (2026-07-11) — every pre-fold migration referenced below is folded into the single baseline
app/internal/db/migrations/00001_baseline_v0_1.sql(see ADR 0057).
Decision — two squash points (2026-07-11, resolves #228)
Section titled “Decision — two squash points (2026-07-11, resolves #228)”The append-only-forever trigger is v1.0.0, per SemVer’s convention that the 0.x range is beta and may break. There are two deliberate squash points:
- v0.1.0 fold — before the first tagged release, all migrations to date are folded into a single fresh baseline (
00001_baseline_v0_1.sql). v0.1.0 ships with exactly one migration file — no append migrations. This is a clean-first-release goal, not the append-only lock. - v1.0.0 fold — before the out-of-beta tag, all beta-era append migrations (0.1 → 0.9) are folded once more into a fresh v1.0.0 baseline. At the v1.0.0 tag, append-only-forever activates — no further squashes, ever.
Between v0.1.0 and v1.0.0 (the beta window): append freely. New features add normal 00002, 00003, … append migrations on top of the v0.1.0 baseline. They accumulate through beta and get folded once at the v1.0.0 squash point.
Rationale: 0.x is explicitly beta (“workshop”) per the milestone model; locking append-only at the first tag would be stricter than SemVer requires and would remove the ability to clean up schema as real usage exposes problems during beta. The clean-baseline-per-release goal (one file at each vX.Y.0 milestone tag) is satisfied by the two fold points without forbidding mid-beta iteration.
Milestones (per docs/v0_1_readiness.md §0):
- v0.1.0 — first tagged release (marker: ResourceSpace refs deleted + base feature set complete). Single-baseline fold #1.
- v1.0.0 — out of beta (marker: real production usage + soak + stable quality). Single-baseline fold #2 + append-only-forever locks.
Issue #228 is closed by this decision.
Implementation status (2026-06-15)
Section titled “Implementation status (2026-06-15)”The pre-MVP baseline squash is in progress:
- 1.49.C-1 audit ✅ shipped via PR #132 (
b3b796b). The first audit report under this ADR’s policy lives atdocs/cleanup-audit-2026-06.md— 26 findings (17 drop-recommended, 9 cosmetic, 0 critical) across 14 migrations / 61 tables / 64 FKs / 149 indexes. Closes #81. - 1.49.C-2 squash ⏭ ready to ship. Scope locked at 24 single-line schema edits (17 column drops + 2 renames + 4 FK annotations + 1 column comment) per the audit’s “Recommended C-2 net changes” section. Tracked at #82.
- Future audits. This ADR’s audit-report shape (§ “Audit-report
shape” below) is now exercised end-to-end; the
docs/ cleanup-audit-<date>.mdfilename pattern is locked. Subsequent pre-MVP audits (if any) follow the same template.
After 1.49.C-2 lands + v1.0.0 ships, this ADR’s append-only clause activates. No further squashes permitted.
Context
Section titled “Context”The Artist Alley codebase landed its first 60ish migrations during the pre-MVP development sprint. The accumulated sequence carries real signal — each migration documents a decision in time — but also accumulates noise:
- Tables added then never used (orphans), columns added then effectively abandoned (smell), indexes added then made redundant by later indexes.
- Naming inconsistencies that accreted across separate sub-phases
(
*_idUUID vs*_refBIGINT,created_by_user_refvscreated_by_user_id). - Migrations from short-lived design directions that were superseded before they shipped to a real user (strangler-fig artifacts, abandoned legacy bridges).
- Every fresh-DB-in-CI run replays the entire sequence — slow,
noisy, and a recurring source of
goose plpgsqlmarker bugs that hide on local DBs because goose skips already-applied migrations.
The first time we squash 60 migrations into a clean baseline, the move is uncontroversial — pre-MVP means no real users to migrate forward. The harder question is: when else? Without a policy, every time the migration count crosses a perceived threshold a contributor will be tempted to squash again. After v1.0 ships, that temptation must die. Real users with real data cannot tolerate “sorry, this version of Artist Alley requires a fresh DB.”
This ADR draws the line.
Decision
Section titled “Decision”Pre-MVP migrations may be squashed into a single baseline. The
squash is allowed because no production user exists who needs a
forward path. The cost is paid only by developers, who can be
trusted to docker compose down -v before pulling.
After v1.0 launch, migrations are append-only forever. No further squashes. The migration history becomes a permanent record of how the schema reached its current shape, in the order it actually reached it.
When a squash is allowed
Section titled “When a squash is allowed”A migration baseline squash is permitted only when all of the following hold:
- The project has not yet shipped a tagged
v1.0.0release to a public release channel (Homebrew, apt, dnf, GHCR:latest). - The squash is approved in an ADR with the canonical migration header (per §“Baseline migration shape” below).
- The squash is audited in a separate commit before the destructive
commit lands. The audit commit ships a written report
(
docs/cleanup-audit-<date>.md) listing every dropped table, dropped column, renamed identifier, and the reasoning for each. - The destructive commit is reviewable in isolation: it ships only
the new baseline file, the deletions of the old migrations, the
updated
app/schema.sql, and the regenerated sqlc output. No feature work in the same commit. - The branch is named
chore/migration-baseline-vN(where N starts at 1) so reviewers immediately understand the scope.
A baseline squash that fails any of these conditions is a project-policy violation and must be rejected at review.
When a squash is forbidden
Section titled “When a squash is forbidden”After v1.0.0 ships, baseline squashes are forbidden. This is
not a guideline; it is a contract with operators. Real users on
real instances cannot reset their database. Every schema change
after v1.0 must be a forward-only migration that any production
instance can apply with goose up.
Exceptions exist only with explicit ADR amendment to this one, naming the specific operational reason and a guaranteed-safe forward path for every supported prior schema version. The bar is intentionally high — easier to ship a complicated migration than to break operator trust.
Baseline migration shape
Section titled “Baseline migration shape”A baseline migration file lives at
app/internal/db/migrations/00001_baseline_vN.sql (where N
matches the branch numbering). The header is mandatory and matches
this template:
-- goose Up-- =====================================================================-- MIGRATION BASELINE v<N>-- =====================================================================-- Baseline as of <YYYY-MM-DD> — pre-v1.0 cleanup pass.-- Supersedes the prior migration sequence; no upgrade path from the-- prior sequence is supported. Developers running locally must-- `docker compose down -v && up -d` after pulling this commit.---- Authority: ADR 0046-- Audit: docs/cleanup-audit-<date>.md-- Branch: chore/migration-baseline-v<N>-- =====================================================================
-- … schema in stable ordering …
-- goose Down-- (Down is intentionally empty for a baseline migration.-- Rolling back means restoring from a backup, not running migrations.)The down section is empty by design. A baseline cannot be rolled back through migrations — restoration is a backup-restore operation.
Audit-report shape
Section titled “Audit-report shape”The cleanup-audit document (docs/cleanup-audit-<date>.md) lists:
- Dropped tables. Per-table: name, where the orphan was confirmed
(which packages searched for usage, what
git greppatterns ran), reasoning, last migration that touched it. - Dropped columns. Per-column: table, name, type, default, why it was abandoned, last sqlc-generated reference (or none).
- Renamed identifiers. Per-rename: old name, new name, reason, affected packages.
- Index consolidations. Per-index: which indexes were merged or dropped, what query patterns justified the surviving set.
- CHECK constraint changes. Per-constraint: any tightening or widening, with the Go-side typed-constant mirror per ADR 0042.
- Naming-convention decisions. The canonical convention adopted
(e.g., “FKs are
*_idUUID; legacy*_refBIGINT renamed where the column is in scope, deferred where it isn’t”).
Audit reports are kept in docs/ permanently. Each squash adds
one report.
Schema-as-source-of-truth invariant
Section titled “Schema-as-source-of-truth invariant”app/schema.sql is the canonical schema description that sqlc
type-checks Go queries against. After a baseline squash:
app/schema.sqlmirrors the baseline migration’s final state exactly.- sqlc is regenerated. The diff is reviewable.
- Both files land in the same commit as the baseline.
When new migrations land after the baseline, they extend
app/schema.sql in the same commit. The two never drift.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- One canonical baseline per pre-MVP cleanup pass instead of a 60-migration archaeology dig every fresh-DB-in-CI run.
- Audit reports are permanent. Future contributors can read “why did we drop the X table” without digging through deleted migration files. Git history preserves the migration code; the audit preserves the reasoning.
- The v1.0 commitment is documented. Operators considering Artist Alley for production can read this ADR and know that the schema-stability story changes hard at v1.0 in their favour.
- CI gets faster. A 60-migration replay drops to one. Local dev-stack-from-scratch is materially less painful.
- Catches drift. The act of squashing forces a full audit pass. Orphan tables that accumulated over six months get found and dropped instead of haunting the schema forever.
Negative
Section titled “Negative”- One-time destructive operation per squash. Anyone running
locally has to
docker compose down -vand lose their dev data. Pre-MVP this is acceptable (real users don’t exist); the policy prevents it from becoming a recurring inconvenience. - The audit pass is real work. Per-table cross-referencing, per-column usage analysis, naming-convention adjudication. 4-6 days of careful work per squash. Not a quick “regenerate schema” task.
- Squash removes commit-level granularity in
git log. A curious developer asking “when did this column appear?” gets the baseline commit as the answer, not the original migration commit. Mitigation: the audit report documents the structural decisions;git log -- app/internal/db/migrations/*of the pre-baseline range still shows the historical migration commits. - The v1.0 commitment is a real commitment. Once it’s made, any schema mistake has to be lived with through forward-only migrations. There is no “let’s just squash again” escape hatch.
Alternatives considered
Section titled “Alternatives considered”-
Keep all migrations forever, never squash. Simpler policy. Rejected because pre-MVP accumulated migrations are net-negative — they encode design directions that no longer apply, slow CI, and hide drift. The first squash is worth doing.
-
Allow squashes liberally even after v1.0. Reduces engineering toil at the cost of operator trust. Rejected because the operator experience is the load-bearing product story; “sorry, this release requires a fresh DB” breaks it.
-
Keep old migrations in a
migrations/_archive/directory. Considered. Rejected because (a) git history preserves them; (b) the archive directory creates confusion about whether those migrations are still in the apply path; (c) the audit report serves the “why did we change X” question better than the raw archived SQL. -
Auto-generate the baseline from
pg_dumpon every CI run. Considered as a continuous-squash mechanism. Rejected because the schema would diverge from the committed baseline silently, the audit step would never happen, and the policy line for “never after v1.0” would have no place to anchor.
Implementation
Section titled “Implementation”This ADR’s implementation is operational:
-
Update the developer-reference documentation to point at this ADR when discussing the migration policy. Add a “Migration policy” subsection to
site/src/content/docs/developers/reference.mdx. -
Coding-standards entry. Add a “Database migrations” rule to
site/src/content/docs/developers/coding-standards.mdxreferencing this ADR, with the short version of the rule (“pre-MVP: squashes allowed per ADR 0046; post-v1.0: forward-only forever”). -
Phase 1.49 — Pre-MVP cleanup ships the first baseline squash under this policy. The audit report from that pass lands at
docs/cleanup-audit-2026-06.md(or similar dated name) as the first reference implementation of the squash procedure. -
The next squash decision (if any) is on operators, not on us. Once v1.0 ships, the policy holds without per-release ADR work.
References
Section titled “References”- ADR 0006 — Go as target backend. sqlc is the build-time validation layer that sees the squashed schema.
- ADR 0042 — Distributed catalogs. CHECK constraints in the baseline must mirror the typed Go constants per the convention.
app/schema.sql— canonical schema mirror.app/internal/db/migrations/— migration sequence directory.- goose migration tool — the runtime that applies migrations.