Skip to content

Commit 3262910

Browse files
dmealingclaude
andcommitted
docs(#241): document the D1 FK-cascade; remove the refusal known-limitation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TJRi8FtEW24z9HKGUL1xh8
1 parent 4dcd551 commit 3262910

2 files changed

Lines changed: 50 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,33 @@ here. The format follows [Keep a Changelog](https://keepachangelog.com/), and
55
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
66
(pre-1.0; MINOR bumps may introduce breaking changes with notice).
77

8+
## [Unreleased]
9+
10+
**npm-only** — the changed code is all in `migrate-ts` (D1 is a TS-only dialect);
11+
PyPI / NuGet / Maven Central are unchanged.
12+
13+
### Added — D1 auto-cascade for rebuilding foreign-key-referenced tables (#241)
14+
15+
npm-only (`migrate-ts` — D1 is a TS-only dialect). `meta migrate --dialect d1` no
16+
longer refuses when a change would rebuild a table that another table's foreign key
17+
references ([#226](https://github.com/metaobjectsdev/metaobjects/issues/226)) — it now
18+
**auto-generates an appliable cascade** instead. The emitter rebuilds the referenced
19+
table together with every table that transitively references it, in one pass: the
20+
affected tables are dropped referrers-first and recreated parents-first, under `PRAGMA
21+
defer_foreign_keys = ON` so every foreign-key check defers to the end of D1's implicit
22+
transaction rather than firing mid-rebuild. The cascade is built over the **union of
23+
the actual (live) and expected (target) schemas' foreign-key graphs**, closing #226's
24+
residual under-refuse gap: a single migration that both rebuilds a referenced table
25+
*and* drops the referencing foreign key in the same run is now detected and handled
26+
correctly (a target-schema-only check missed this). A **multi-table foreign-key cycle**
27+
(two or more tables referencing each other in a loop) has no parents-first rebuild
28+
order and is still refused at generation time with actionable guidance; a
29+
self-referencing table is not a cycle and is rebuilt by the cascade like any other
30+
table. Migrations that do not rebuild a foreign-key-referenced table are
31+
**byte-identical**. Gated by a real-libSQL-engine convergence round-trip across every
32+
cascade topology (linear chain, diamond, self-reference), the #226 gap scenario, and
33+
the cycle-refusal case.
34+
835
## [0.20.7] — 2026-07-28
936

1037
**npm-only** — the changed code is all in `cli` / `migrate-ts`; PyPI / NuGet / Maven

docs/features/migrations-and-drift.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,28 +63,36 @@ new migration from the metadata-vs-DB diff before applying it. `apply-pending` j
6363
the pending already-committed files, making it idempotent; `--dry-run` lists what would
6464
run. postgres/sqlite only — on D1 use `wrangler d1 migrations apply`.
6565

66-
#### D1 limitation: rebuilding a foreign-key-referenced table
66+
#### D1: rebuilding a foreign-key-referenced table
6767

6868
Cloudflare D1 applies each migration inside its own implicit transaction, and SQLite
6969
ignores `PRAGMA foreign_keys` while a transaction is open. The SQLite table-rebuild
7070
recipe (used for a `CHECK`, column type/nullability/default, foreign-key, or
7171
`field.enum` values change) relies on `PRAGMA foreign_keys = OFF` taking effect before
7272
it drops and recreates the table — which does not happen on remote D1.
7373

74-
To avoid emitting a migration that would fail against a populated production database
75-
with `FOREIGN KEY constraint failed`, `meta migrate --dialect d1` **refuses at
76-
generation time** when a change would rebuild a table that another table's foreign key
77-
references. Apply such a change by hand-writing the migration: rebuild the referencing
78-
table to temporarily drop its foreign key, rebuild the referenced table, then restore
79-
the foreign key — or make the change on an unreferenced table. (Auto-generating this
80-
cascade is tracked as a follow-up, [#241](https://github.com/metaobjectsdev/metaobjects/issues/241).)
81-
82-
**Known limitation of the current refusal:** it is computed from the *target* schema's
83-
foreign keys. A single migration that **both** rebuilds a referenced table **and** drops
84-
the referencing foreign key in the same run is therefore not detected, and can still fail
85-
on remote D1 (the parent's `DROP TABLE` can be emitted before the referencing table is
86-
rebuilt). Split such a change into two migrations, or hand-write it. Closing this gap
87-
requires the actual-schema foreign-key graph and rides the same follow-up (#241).
74+
`meta migrate --dialect d1` handles this by **auto-generating a cascade**
75+
([#241](https://github.com/metaobjectsdev/metaobjects/issues/241), closing
76+
[#226](https://github.com/metaobjectsdev/metaobjects/issues/226)'s residual
77+
under-refuse gap below) instead of refusing outright. When a change would rebuild a
78+
table that another table's foreign key references, the emitter rebuilds that table
79+
together with every table that transitively references it, in one pass: the affected
80+
tables are dropped referrers-first and recreated parents-first, under `PRAGMA
81+
defer_foreign_keys = ON` so every foreign-key check defers to the end of D1's implicit
82+
transaction instead of firing mid-rebuild. The result applies cleanly against a
83+
populated production database and re-converges — a follow-up `meta verify`/`meta
84+
migrate` sees no drift. The cascade is built over the **union of the actual (live) and
85+
expected (target) schemas' foreign-key graphs**, so it also covers the case a
86+
target-schema-only check would miss: a single migration that both rebuilds a
87+
referenced table *and* drops the referencing foreign key in the same run.
88+
89+
The one case still hand-written: a **multi-table foreign-key cycle** (table A
90+
references B references … references A, two or more tables). A cycle has no
91+
parents-first rebuild order, so `meta migrate --dialect d1` still **refuses at
92+
generation time** — hand-write the migration (drop the foreign key on one side of the
93+
cycle, rebuild the tables, then restore it) or break the cycle in your metadata. A
94+
self-referencing table (a table whose own foreign key targets itself) is not a cycle
95+
in this sense and is handled by the cascade like any other rebuild.
8896

8997
### Java
9098

0 commit comments

Comments
 (0)