TML-2769: restructure the migration ledger into a readable per-migration journal#665
TML-2769: restructure the migration ledger into a readable per-migration journal#665wmadden-electric wants to merge 15 commits into
Conversation
Folds in the cross-cutting design decisions for the migration read-command family (list / graph / status / log) and the ledger foundation that status and log depend on: - decisions.md: D1-D10 (command-family model, shared renderer, space policy B, list/graph split, list->tree, ledger foundation, log, status reframe with --from/--to, and where path/invariants live). - New slice specs: migration-graph-space-flag (TML-2767), list-renders-tree (TML-2768). - README: full slice list incl. ledger (TML-2769), status (TML-2748), log (TML-2770) and the future siblings (TML-2771, TML-2772). Signed-off-by: Will Madden <madden@prisma.io>
Investigation found the on-apply ledger records one collapsed row per space-apply (not per migration) and the three target schemas have diverged (PG/SQLite lack a space column; Mongo lacks operations). status and log both need one row per migration edge, so the foundation slice is a restructure into a per-edge journal (space + name + hash per row), not an additive field bump. - New slice spec: slices/ledger-foundation/spec.md. - decisions.md D7 rewritten to the per-migration-journal restructure. Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
…t future (D7) Signed-off-by: Will Madden <madden@prisma.io>
The SQL ledger recorded one collapsed row per space-apply, spanning the whole
walked path, on a schema with no space column — the wrong shape for status
("is this migration applied?") and log ("one row per apply event"). Restructure
it into a per-migration journal.
Postgres + SQLite ledger tables gain space, migration_name, and migration_hash.
The per-edge breakdown (PerSpacePlan.migrationEdges) is threaded from the apply
layer into the SQL runner execute options; the runner writes one row per applied
edge inside the per-space transaction, attributing ops by slicing plan.operations
by each edge's operationCount in walk order. contract_json is materialised only
at the apply's endpoints (interiors null). synth plans (no authored edges) keep a
single row keyed by the plan destination, now carrying the space id.
Mongo gains an optional migrationEdges field on its runner options so the apply
layer typechecks; its write behaviour is unchanged (per-edge parity is the next
dispatch).
Signed-off-by: Will Madden <madden@prisma.io>
Bring Mongo to parity with the SQL per-migration journal. Each applied edge now writes one _prisma_migrations ledger doc carrying migration_name, migration_hash, and that edge's operations alongside the existing space/from/to/appliedAt. The runner threads the per-edge breakdown (migrationEdges) through a per-edge loop, attributing ops by slicing plan.operations by operationCount in walk order; synth applies (no authored edges) write a single doc keyed by the plan destination with an empty migration name. Mongo's stricter no-op skip is left intact. Signed-off-by: Will Madden <madden@prisma.io>
Complete the ledger read side. A new LedgerEntryRecord (space, migrationName, migrationHash, from, to, appliedAt, operationCount) sits beside ContractMarkerRecord, and readLedger is added to the ControlFamilyInstance SPI, the SQL and Mongo family instances, the Postgres/SQLite/Mongo adapters, and the CLI control client as a thin pass-through mirroring readMarker. Reads return a space's entries in apply order with cross-target parity: the ∅ origin reads as from: null on every target (normalising SQL null/sha256:empty and Mongo's empty-string), and operationCount is derived from the stored operations rather than returning the ops themselves. Round-trip and parity tests cover single-edge, multi-edge, and missing-table reads on all three targets. Signed-off-by: Will Madden <madden@prisma.io>
…one ref Close the read-side coverage gap the review flagged: the Postgres and SQLite synth-apply tests now read the synthesised row back through readLedger and assert its from→null normalisation and operationCount projection, not just the raw stored row. Also drop a stale milestone label from a runner-deps comment. Signed-off-by: Will Madden <madden@prisma.io>
…-2769-make-the-migration-ledger-readable
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (18)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (9)
📝 WalkthroughWalkthroughAdds a LedgerEntryRecord contract and readLedger APIs across control clients, family instances, and storage adapters; threads optional migrationEdges into runner options; runners emit per-edge or synth ledger rows; SQL/SQLite/Postgres schemas and builders updated; comprehensive tests added for per-edge behavior. ChangesLedger Reading and Per-Edge Ledger Tracking
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/2-mongo-family/9-family/src/core/control-instance.ts (1)
174-177:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate error message to mention
readLedger.The type guard now requires
readLedger(lines 91-92), but the error message still only mentions "missing readMarker, readAllMarkers, or introspectSchema".📝 Proposed fix
throw new Error( - 'Adapter does not implement MongoControlAdapter (missing readMarker, readAllMarkers, or introspectSchema)', + 'Adapter does not implement MongoControlAdapter (missing readMarker, readAllMarkers, readLedger, or introspectSchema)', );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/2-mongo-family/9-family/src/core/control-instance.ts` around lines 174 - 177, The thrown Error when the isMongoControlAdapter type guard fails should mention the newly required method readLedger; update the error message in the control-adapter check (the block that tests isMongoControlAdapter(controlAdapter) and throws) so it lists "missing readMarker, readAllMarkers, readLedger, or introspectSchema" (or equivalent) to reflect the updated type guard requirement.packages/2-sql/9-family/src/core/control-instance.ts (1)
376-379:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate error message to mention
readLedger.The type guard now requires
readLedger(lines 267-268), but the error message still only mentions "missing introspect, readMarker, or readAllMarkers".📝 Proposed fix
throw new Error( - 'Adapter does not implement SqlControlAdapter (missing introspect, readMarker, or readAllMarkers)', + 'Adapter does not implement SqlControlAdapter (missing introspect, readMarker, readAllMarkers, or readLedger)', );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/2-sql/9-family/src/core/control-instance.ts` around lines 376 - 379, The error thrown when !isSqlControlAdapter(controlAdapter) references missing methods but omits the newly required readLedger; update the Error message in the throw inside control-instance.ts to list readLedger alongside introspect, readMarker, and readAllMarkers (i.e., mention that the adapter is missing introspect, readMarker, readAllMarkers, or readLedger) so the message matches the isSqlControlAdapter type guard and helps identify the missing method on controlAdapter.packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts (1)
46-60:⚠️ Potential issue | 🟠 Major | ⚡ Quick winGuard or migrate the legacy
_prisma_ledgershape.
CREATE TABLE IF NOT EXISTSleaves an existing ledger table untouched, so upgraded databases keep the old column set and the new reads/inserts in this PR will fail onspace,migration_name, ormigration_hash. Please either converge the table here or add an explicit legacy-shape failure before the runner/adapter touches it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts` around lines 46 - 60, The CREATE TABLE IF NOT EXISTS in ensureLedgerTableStatement leaves older _prisma_ledger schemas intact, causing missing columns (e.g., space, migration_name, migration_hash, origin_*/destination_* etc.) to break new reads/inserts; update the migration to either (A) detect and migrate legacy shape by querying PRAGMA table_info('_prisma_ledger') and issuing ALTER TABLE ... ADD COLUMN for any missing columns (ensuring NOT NULL columns get safe defaults or backfilled values), or (B) explicitly fail fast by checking the schema shape before the runner/adapter touches the table and throwing a clear error advising migration; implement this check/migration in the same module that defines ensureLedgerTableStatement so startup will converge or abort before using _prisma_ledger.
🧹 Nitpick comments (3)
packages/3-mongo-target/1-mongo-target/src/core/mongo-runner.ts (1)
279-282: ⚡ Quick winUse structured error for validation failure.
Line 280 throws a generic
Errorwhen edge operation counts don't sum to plan length. Consider usingerrorRunnerFailed(imported at line 3) for consistency with other runner failures and to provide structured error context.🔧 Proposed fix using structured error
- throw new Error( - `Ledger write: plan.operations length (${plan.operations.length}) does not match sum of migrationEdges operationCount (${totalEdgeOps})`, - ); + throw errorRunnerFailed( + `Ledger write: plan.operations length (${plan.operations.length}) does not match sum of migrationEdges operationCount (${totalEdgeOps})`, + { + why: 'The migration edges provided to the runner must account for all planned operations.', + meta: { planOperationsLength: plan.operations.length, totalEdgeOps }, + }, + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/3-mongo-target/1-mongo-target/src/core/mongo-runner.ts` around lines 279 - 282, Replace the generic throw new Error in mongo-runner.ts (the check that compares totalEdgeOps and plan.operations.length) with a call to the existing errorRunnerFailed helper so the failure is structured; call errorRunnerFailed(...) passing a clear message and include contextual fields (e.g., reason, planOperations: plan.operations.length, totalEdgeOps) to match how other runner failures are reported and keep the import of errorRunnerFailed used consistently.packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts (1)
635-640: ⚖️ Poor tradeoffError handling inconsistency: direct throw vs runner failure pattern.
The validation at lines 637-640 throws an
Errordirectly, but other runner errors returnrunnerFailure(...)(e.g., lines 321-332, 525-534). Consider wrapping this in arunnerFailurefor consistency:if (totalEdgeOps !== plan.operations.length) { - throw new Error( - `Ledger write: plan.operations length (${plan.operations.length}) does not match sum of migrationEdges operationCount (${totalEdgeOps})`, - ); + return runnerFailure( + 'LEDGER_EDGE_MISMATCH', + `Ledger write: plan.operations length (${plan.operations.length}) does not match sum of migrationEdges operationCount (${totalEdgeOps})`, + { + meta: { + planOperationsLength: plan.operations.length, + edgeOperationsSum: totalEdgeOps, + }, + }, + ); }Note: This would require changing
recordLedgerEntriesreturn type toPromise<Result<void, SqlMigrationRunnerFailure>>and handling the result at the call site (line 169).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts` around lines 635 - 640, The validation in recordLedgerEntries currently throws an Error when plan.operations.length !== sum of edge.operationCount; change this to return runnerFailure(...) using the SqlMigrationRunnerFailure variant used elsewhere (match the pattern from runnerFailure calls around lines with runnerFailure usages) so recordLedgerEntries returns Promise<Result<void, SqlMigrationRunnerFailure>> instead of throwing; update the function signature (recordLedgerEntries) and replace the throw with a runnerFailure(...) call carrying the same diagnostic message, then update the caller at the call site that invokes recordLedgerEntries (the code noted around line 169) to handle the Result (unwrap or propagate failure consistently) instead of expecting an exception.packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts (1)
50-62: ⚡ Quick winHardcoded
EMPTY_ORIGIN_CORE_HASHmatches the canonicalEMPTY_CONTRACT_HASH('sha256:empty'), so there’s no correctness mismatch risk. Keeping the local constant is fine; optionally import and compare againstEMPTY_CONTRACT_HASHfor consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts` around lines 50 - 62, The code defines a local constant EMPTY_ORIGIN_CORE_HASH and compares it in ledgerOriginFromStored; to keep consistency with the canonical name, remove the local EMPTY_ORIGIN_CORE_HASH and import EMPTY_CONTRACT_HASH (or rename to match) instead, then update ledgerOriginFromStored to compare originCoreHash against EMPTY_CONTRACT_HASH; ensure the imported symbol (EMPTY_CONTRACT_HASH) is referenced where EMPTY_ORIGIN_CORE_HASH was used and that ledgerOriginFromStored still returns null for null, empty string, or the empty-contract hash.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts`:
- Around line 45-47: coerceLedgerAppliedAt currently uses new Date(value) which
treats timezone-less SQL timestamps like "YYYY-MM-DD HH:MM:SS" as local time;
update coerceLedgerAppliedAt to parse such SQLite datetime strings as UTC by
normalizing the string before constructing the Date (for example convert
"YYYY-MM-DD HH:MM:SS" to an ISO-like "YYYY-MM-DDTHH:MM:SSZ" or otherwise append
a 'Z' so Date parses it as UTC) and keep the existing behavior when value is
already a Date; locate and modify the coerceLedgerAppliedAt function to
implement this normalization.
---
Outside diff comments:
In `@packages/2-mongo-family/9-family/src/core/control-instance.ts`:
- Around line 174-177: The thrown Error when the isMongoControlAdapter type
guard fails should mention the newly required method readLedger; update the
error message in the control-adapter check (the block that tests
isMongoControlAdapter(controlAdapter) and throws) so it lists "missing
readMarker, readAllMarkers, readLedger, or introspectSchema" (or equivalent) to
reflect the updated type guard requirement.
In `@packages/2-sql/9-family/src/core/control-instance.ts`:
- Around line 376-379: The error thrown when
!isSqlControlAdapter(controlAdapter) references missing methods but omits the
newly required readLedger; update the Error message in the throw inside
control-instance.ts to list readLedger alongside introspect, readMarker, and
readAllMarkers (i.e., mention that the adapter is missing introspect,
readMarker, readAllMarkers, or readLedger) so the message matches the
isSqlControlAdapter type guard and helps identify the missing method on
controlAdapter.
In
`@packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts`:
- Around line 46-60: The CREATE TABLE IF NOT EXISTS in
ensureLedgerTableStatement leaves older _prisma_ledger schemas intact, causing
missing columns (e.g., space, migration_name, migration_hash,
origin_*/destination_* etc.) to break new reads/inserts; update the migration to
either (A) detect and migrate legacy shape by querying PRAGMA
table_info('_prisma_ledger') and issuing ALTER TABLE ... ADD COLUMN for any
missing columns (ensuring NOT NULL columns get safe defaults or backfilled
values), or (B) explicitly fail fast by checking the schema shape before the
runner/adapter touches the table and throwing a clear error advising migration;
implement this check/migration in the same module that defines
ensureLedgerTableStatement so startup will converge or abort before using
_prisma_ledger.
---
Nitpick comments:
In `@packages/3-mongo-target/1-mongo-target/src/core/mongo-runner.ts`:
- Around line 279-282: Replace the generic throw new Error in mongo-runner.ts
(the check that compares totalEdgeOps and plan.operations.length) with a call to
the existing errorRunnerFailed helper so the failure is structured; call
errorRunnerFailed(...) passing a clear message and include contextual fields
(e.g., reason, planOperations: plan.operations.length, totalEdgeOps) to match
how other runner failures are reported and keep the import of errorRunnerFailed
used consistently.
In `@packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts`:
- Around line 635-640: The validation in recordLedgerEntries currently throws an
Error when plan.operations.length !== sum of edge.operationCount; change this to
return runnerFailure(...) using the SqlMigrationRunnerFailure variant used
elsewhere (match the pattern from runnerFailure calls around lines with
runnerFailure usages) so recordLedgerEntries returns Promise<Result<void,
SqlMigrationRunnerFailure>> instead of throwing; update the function signature
(recordLedgerEntries) and replace the throw with a runnerFailure(...) call
carrying the same diagnostic message, then update the caller at the call site
that invokes recordLedgerEntries (the code noted around line 169) to handle the
Result (unwrap or propagate failure consistently) instead of expecting an
exception.
In `@packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts`:
- Around line 50-62: The code defines a local constant EMPTY_ORIGIN_CORE_HASH
and compares it in ledgerOriginFromStored; to keep consistency with the
canonical name, remove the local EMPTY_ORIGIN_CORE_HASH and import
EMPTY_CONTRACT_HASH (or rename to match) instead, then update
ledgerOriginFromStored to compare originCoreHash against EMPTY_CONTRACT_HASH;
ensure the imported symbol (EMPTY_CONTRACT_HASH) is referenced where
EMPTY_ORIGIN_CORE_HASH was used and that ledgerOriginFromStored still returns
null for null, empty string, or the empty-contract hash.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: f8251f5a-abe4-451b-b739-98af66722508
⛔ Files ignored due to path filters (7)
projects/migration-graph-rendering/README.mdis excluded by!projects/**projects/migration-graph-rendering/decisions.mdis excluded by!projects/**projects/migration-graph-rendering/slices/ledger-foundation/plan.mdis excluded by!projects/**projects/migration-graph-rendering/slices/ledger-foundation/spec.mdis excluded by!projects/**projects/migration-graph-rendering/slices/list-renders-tree/spec.mdis excluded by!projects/**projects/migration-graph-rendering/slices/migration-graph-space-flag/spec.mdis excluded by!projects/**projects/migration-graph-rendering/slices/remove-list-graph-renderer/spec.mdis excluded by!projects/**
📒 Files selected for processing (29)
packages/1-framework/0-foundation/contract/src/exports/types.tspackages/1-framework/0-foundation/contract/src/types.tspackages/1-framework/1-core/framework-components/src/control/control-instances.tspackages/1-framework/3-tooling/cli/src/control-api/client.tspackages/1-framework/3-tooling/cli/src/control-api/operations/apply.tspackages/1-framework/3-tooling/cli/src/control-api/types.tspackages/1-framework/3-tooling/cli/test/config-types.test.tspackages/2-mongo-family/9-family/src/core/control-adapter.tspackages/2-mongo-family/9-family/src/core/control-instance.tspackages/2-sql/9-family/src/core/control-adapter.tspackages/2-sql/9-family/src/core/control-instance.tspackages/2-sql/9-family/src/core/migrations/types.tspackages/3-mongo-target/1-mongo-target/src/core/mongo-runner.tspackages/3-mongo-target/1-mongo-target/test/mongo-runner.test.tspackages/3-mongo-target/2-mongo-adapter/src/core/marker-ledger.tspackages/3-mongo-target/2-mongo-adapter/src/core/mongo-control-adapter.tspackages/3-mongo-target/2-mongo-adapter/src/core/runner-deps.tspackages/3-mongo-target/2-mongo-adapter/src/exports/control.tspackages/3-mongo-target/2-mongo-adapter/test/marker-ledger.test.tspackages/3-targets/3-targets/postgres/src/core/migrations/runner.tspackages/3-targets/3-targets/postgres/src/core/migrations/statement-builders.tspackages/3-targets/3-targets/sqlite/src/core/migrations/runner.tspackages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.tspackages/3-targets/6-adapters/postgres/src/core/control-adapter.tspackages/3-targets/6-adapters/postgres/test/migrations/fixtures/runner-fixtures.tspackages/3-targets/6-adapters/postgres/test/migrations/runner.ledger.integration.test.tspackages/3-targets/6-adapters/sqlite/src/core/control-adapter.tspackages/3-targets/6-adapters/sqlite/test/migrations/fixtures/runner-fixtures.tspackages/3-targets/6-adapters/sqlite/test/migrations/runner.ledger.test.ts
Consolidate the SQL ledger read-coercion helpers into a shared `@prisma-next/family-sql/ledger-read` module so both SQL adapters stop duplicating `ledgerOriginFromStored` / `coerceLedgerAppliedAt` / `operationCountFromStored` and the `'sha256:empty'` sentinel (now sourced from `EMPTY_CONTRACT_HASH`). Fix the SQLite ledger timestamp round-trip: `created_at` now defaults to a Z-suffixed ISO string and `coerceLedgerAppliedAt` interprets any remaining designator-less datetime as UTC, so `appliedAt` no longer shifts by the local offset. Align Mongo `readLedger` with the SQL adapters' lenient read policy — it now skips legacy/malformed ledger docs instead of throwing. Refresh the stale `isSqlControlAdapter` / `isMongoControlAdapter` diagnostics to list `readLedger`, and cover the previously untested op-count-mismatch guard, the Mongo wrapper-level `migrationEdges` threading, and the `appliedAt` parsed value. Signed-off-by: Will Madden <madden@prisma.io>
…ions apply already forwards migrationEdges into runner.execute() per-space options; add the field to MigrationRunnerPerSpaceOptions (with undefined for exactOptionalPropertyTypes) and fix the mongo wrapper integration test family cast. Signed-off-by: Will Madden <madden@prisma.io>
…onalPropertyTypes Runner execute options must accept explicit undefined for migrationEdges so framework per-space options spread cleanly into mongo and SQL runners. Signed-off-by: Will Madden <madden@prisma.io>
Use blindCast for the readLedger probe so the cast ratchet stays flat when extending the existing isSqlControlAdapter / isMongoControlAdapter checks. Signed-off-by: Will Madden <madden@prisma.io>
Linked issue
Refs TML-2769. Control-plane foundation for
migration status(TML-2748) andmigration log(TML-2770). Review follow-ups tracked in TML-2774.At a glance
The migration ledger is now a readable per-migration journal — one row per applied migration edge — with a
readLedgerAPI that returns the same shape on every target:Before this PR the ledger was write-only, its three target schemas had diverged, and it recorded one collapsed row per space-apply (origin→destination spanning the whole walked path) — a shape that can't answer "is this migration applied?" or "what ran, in what order?".
Decision
Restructure the on-apply ledger into a per-migration journal and add a read API:
space,migration_name(dirName),migration_hash(the exact-match keystatuswill use), per-edgefrom/tocore hashes, and the edge's authoredoperations(sliced fromplan.operationsbyoperationCount).space+migration_name+migration_hashcolumns; Mongo gainsmigrationName+migrationHash+operationson its ledger docs.contract_json_before/afteris retained but materialised only at the apply's endpoints (first/last edge) — interior edges storenull.readLedgerread API onControlFamilyInstance, both family instances, all three adapters, and the CLI control client (a thin pass-through mirroringreadMarker). It returns a space's entries in apply order with cross-target parity.The branch also lands the design-of-record for the wider migration read-command family (
list/graph/status/log/ ledger) underprojects/migration-graph-rendering/— the spec and plan this slice was cut from.How it fits together
apply.tsalready computesPerSpacePlan.migrationEdges({migrationHash, dirName, from, to, operationCount}) during graph-walk planning; it now passes them into each per-space runner execute option. The SQL runner options type carries the field; Mongo receives it structurally.postgres/sqlite) and the Mongo runner replace the single ledger write with a loop over the edges, slicingplan.operationsby each edge'soperationCountto attribute ops in walk order, assertingsum(operationCount) === plan.operations.length. Synth (greenfielddb init) plans have no authored edges, so they keep writing a single synthesised row keyed by the plan destination (from= null, empty migration name).readLedgerprobes for the ledger store (returning[]when absent, mirroringreadMarker), selects the space's rows in apply order, and projects them toLedgerEntryRecord. The ∅ origin reads asfrom: nullon every target (normalising SQLnull/sha256:emptyand Mongo's''), andoperationCountis derived from the stored ops rather than returning the ops array — so the three targets yield an identical record.Behavior changes & evidence
space/migration_name/migration_hashand per-edgefrom→tochaining; endpoint-onlycontract_json. —packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts,packages/3-targets/3-targets/postgres/src/core/migrations/statement-builders.ts. Evidence:packages/3-targets/6-adapters/postgres/test/migrations/runner.ledger.integration.test.ts,packages/3-targets/6-adapters/sqlite/test/migrations/runner.ledger.test.ts._prisma_migrationsdoc per applied edge carryingmigrationName/migrationHash/operations, with a synth fallback; the stricter Mongo no-op skip is unchanged. —packages/3-mongo-target/1-mongo-target/src/core/mongo-runner.ts,packages/3-mongo-target/2-mongo-adapter/src/core/marker-ledger.ts. Evidence:packages/3-mongo-target/1-mongo-target/test/mongo-runner.test.ts,packages/3-mongo-target/2-mongo-adapter/test/marker-ledger.test.ts.readLedger(space)returns a space's journal in apply order with cross-target parity. —packages/1-framework/1-core/framework-components/src/control/control-instances.ts,packages/1-framework/3-tooling/cli/src/control-api/client.ts. Evidence: the round-trip + cross-target shape parity assertions in all three ledger test files above.Reviewer notes
migrationEdgesreaches the Mongo runner structurally, not by a declared framework type. The frameworkMigrationRunnerPerSpaceOptionscan't importAggregateMigrationEdgeRef(it lives inmigration-tools, a higher layer thanframework-components, andlint:depsforbids the upward dependency), so Mongo's descriptor passes the field through via object-spread. It works and is tested at the runner level, but a refactor that reconstructs the options object explicitly would silently drop per-edge Mongo writes. Tracked as item 2 of TML-2774.'sha256:empty'+ itsfrom→null normalisation helper are duplicated across the three adapters rather than sharing the realEMPTY_CONTRACT_HASHconstant. Deliberate for now (the shared home is a cross-layer move); tracked as item 1 of TML-2774.8d6f2bba6) — it touches the SPI, both families, three adapters, and the client, but every layer is a mechanical mirror of the existingreadMarkerwiring.Verification
Run on the post-merge HEAD (branch merged up to
origin/main):@prisma-next/clitypecheck — pass@prisma-next/adapter-postgres— 541 passed | 4 expected-fail (incl. ledger round-trip)@prisma-next/adapter-sqlite— 154 passed (incl. ledger round-trip)@prisma-next/adapter-mongo— 287 passed@prisma-next/target-mongo— 400 passedbiome checkon touched files — clean (pre-existingno-bare-castinfos only)Follow-ups
migrationEdgesthreading type-safe, align synth op-count parity and read robustness, add throw-path + wrapper-threading tests.status) and TML-2770 (log) consume this read API.Alternatives considered
contract_jsonfor every edge of a multi-edge apply. Rejected as out of scope: no consumer reads interior snapshots, and synthesising them is non-trivial. Endpoints are materialised; interiors storenull.operationsarray fromreadLedger. Rejected in favour ofoperationCount— the count gives cross-target parity (SQL stores ops as JSON, Mongo as a BSON array) and is all the read consumers need; the full ops remain on disk as an audit record.Skill update
n/a — internal control-plane API; no user-facing CLI/contract surface changes in this PR (the consuming
status/logcommands ship separately).Checklist
git commit -s).TML-NNNN: <sentence-case title>form.Summary by CodeRabbit
New Features
Tests
Chores