Repository navigation
fix(migrate): view recreated only for table change is not drift (D3) - #418
Merged
Merged
Conversation
…t (D3) An adopter on 1.1.0-rc.2 rebuilt a SQLite database from the committed migration chain. Every report and projection view's stored SQL was byte-identical to the metadata's, yet `meta verify --db` and `meta migrate --from-db` listed each view as `- view` / `+ view`, and `verify` exited 1. Neither the text nor `--format json` said what differed. The view comparison was not at fault. When a migration alters a table (a column on every dialect; an FK or CHECK on SQLite/D1, which rebuild the table, #243), Pass 2c drops and recreates every view that reads it. That pair carried nothing that set it apart from a view whose definition changed. - migrate-ts: every view change carries a `reason` (ViewChangeReason: missing | undeclared | definition | unfingerprinted | unchanged). A recreate is `unchanged` only when equality is proven (the fingerprint on Postgres, the normalized text on SQLite/D1). `isViewRecreateOnly` and `withoutViewRecreates` are exported. computeDriftFromActual and classifyDrift leave recreate-only views out. The emitted SQL is unchanged. - A text difference carries the first differing excerpt (firstViewSqlDifference). - Pass 2c keeps `unmanagedActual` from a superseded adoption, so an unstamped Postgres view over an altered table needs --allow adopt-view again (it bypassed the #239 gate before). - cli: describeChange says why each view change was planned. The committed-snapshot gate ignores recreate-only views. `verify --format json` gains `schemaDrift` (changes[] with kind/object/detail, plus findings[]). `migrate` (online, offline-snapshot and D1 paths) adds a note naming the views that match, in text and in a `notes` array in structured output. - Tests: migrate-ts unit and drift tests; the CLI's verify-db-view-recreate integration test; integration-tests lanes on a real SQLite (plus the D1 diff) and a real Postgres, both of which fail with the filter disabled. - CHANGELOG (1.1.0 Fixed), docs/features/cli.md, docs/features/migrations-and-drift.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix a drift-gate defect (D3) that an adopter run on 1.1.0-rc.2 found, before 1.1.0 is promoted. Scope for 1.1.0, in the maintainer's words: "I don't want a half baked 1.1 and then have to do a 1.2 right away."
The defect:
meta migrateandmeta verify --dbreported managed views as changed when their SQL was byte-identical. The adopter, on SQLite, copied the CREATE VIEW DDL frommeta migrate --from-db --dry-runverbatim into their migration chain and replayed it on a fresh DB. Each report view'ssqlite_master.sqlwas then byte-identical to the CREATE VIEW in a fresh dry run. Even so, both commands listed every report view and every projection view as a drop/create pair (- view v_<name>/+ view v_<name>), andverify --dbexited 1. Neither the text output nor--format jsonsaid what differed. Soverify --dbcould not tell an adopter whether a view migration matches the metadata. Commands used:meta migrate --from-db --db file:<db> --dialect sqlite --dry-run --out-dir <dir> --allow nullable-to-not-null --allow drop-fk --allow drop-viewandmeta verify --db file:<db> --dialect sqlite.Required outcome (from the task spec):
Root cause found: the view text comparison (Pass 2b; normalized text on SQLite/D1, the COMMENT fingerprint on Postgres) was correct. Pass 2c (
recreateViewsDependingOnChangedTables) drops and recreates every view that reads a table the migration alters. On every dialect a column type, nullability, default, drop or rename change triggers it. On SQLite/D1 an FK or CHECK change does too, because those dialects rebuild the table (#243). The adopter's tables had unrelated drift (FKs) under the views, so every view over them got a drop/create pair. Nothing distinguished that pair from a definition change, so the drift report printed it as view drift.Deliberate design decisions (please do not flag these as mistakes):
reason: ViewChangeReason: missing | undeclared | definition {compared: text|fingerprint, firstDifference?, tables?} | unfingerprinted {tables?} | unchanged {tables}. A recreate isunchangedonly when equality is proven (fingerprint on Postgres, normalized text on SQLite/D1). If Pass 2c supersedes a definition/unfingerprinted change, that reason is kept, so a real difference is never hidden as a recreate.isViewRecreateOnlyandwithoutViewRecreates.computeDriftFromActualandclassifyDriftdrop recreate-only view changes, so drift reports list only the table change.verify's committed-snapshot gate filters the same way.verify --format jsongains aschemaDriftsection (changes[] with kind/object/detail, plus findings[]), so JSON consumers see the same explanation the text output prints. This is additive.meta migrate(online --from-db, offline committed-snapshot, and D1 paths) adds a note naming the views that match and are recreated only because a table they read changes. The note appears in text output and as anotesarray in structured output. This is additive.unmanagedActualflag from a superseded adoption replace-view. That let an unstamped (no fingerprint) Postgres view over an altered table be replaced without--allow adopt-view, bypassing the migrate-ts: --allow adopt-view emits illegal CREATE OR REPLACE VIEW for structural view changes (fails at apply) #239 adoption gate. It now keeps the flag, so that case needs--allow adopt-view, just as it does with no table change. The CHANGELOG states this.all_typesinet residue (field.inethas no SQLite storage class, so a blocked text->inet change appears on every run). That residue is named explicitly and matches the existing pattern in report-views-sqlite.test.ts, so any other residue still fails.scripts/ci-local.sh --quickpassed, andscripts/ci-local.sh --only ts-slowpassed (migrate-ts real-PG suite, runtime-ts real-PG matrix, TS integration tests).What Changed
View change reason tracking: every view change (create/drop/replace) now carries a
ViewChangeReasonindicating whether it is drift (definition differs), a recreate-only pair (caused by an altered table the view reads), or missing/undeclared. On SQLite/D1 text comparison is normalized; on Postgres a fingerprint is used since deparses the stored body.Drift report filtering:
computeDriftFromActual()andclassifyDrift()drop recreate-only view changes, so adopters see only actual view definition changes in drift output, not false positives from table alterations.verify --dband the committed-snapshot gate apply the same filter.Output detail: text output names the differing view excerpt on SQLite/D1; Postgres fingerprint mode names the mismatch.
verify --format jsongains aschemaDriftsection with kind/object/detail for each change.meta migrateannotates notes listing views that match but are recreated only because a table they depend on changed.Tightened adoption gate: Pass 2c view recreation now preserves the
unmanagedActualflag from a superseded adoption replace-view, so an unstamped (unfingerprinted) Postgres view over an altered table requires--allow adopt-viewinstead of bypassing the gate.Tests: migrate-ts unit + drift tests, CLI integration test against SQLite, and integration-tests lanes on real SQLite and Postgres. With the filter disabled, 6 of 7 integration tests fail, proving the fix catches the adopter's case.
CHANGELOG entry added under the existing 1.1.0 section documenting the defect and fix.
Risk Assessment
✅ Low: Fix is narrowly scoped to view-change reasoning in migrate-ts/CLI, backed by unit, drift, and integration tests that exercise real diff/CLI behavior (not source-grepping), preserves emitted migration SQL, correctly threads the adopt-view gate tightening, and the CHANGELOG entry matches the required scope with no adopter/path leaks.
Testing
Validated fix across unit, integration, and CLI layers. All 2428 tests in migrate-ts and CLI pass. Baseline regression suite passes with conformance, typecheck, and mutation gates all green. Real database tests on SQLite and Postgres confirm view recreation filtering works correctly on all dialects. CHANGELOG documents deliberate design decisions. No actionable findings.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsserver/typescript/packages/migrate-ts/test/: 963 pass, 33 skipserver/typescript/packages/cli/test/: 1465 pass, 3 skipscripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains: PASSEDview-recreate-not-drift.test.ts: 4 passdescribe-change.test.ts: 5 passverify-db-view-recreate.test.ts: 7 passview-recreate-drift-pg.test.ts: 2 pass✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.