TML-2769: restructure the migration ledger into a readable per-migration journal#665
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (7)
📒 Files selected for processing (79)
📝 WalkthroughWalkthroughThis PR adds ledger reading capability and per-edge ledger tracking throughout the migration framework. It defines a ChangesLedger Reading and Per-Edge Ledger Tracking
Estimated code review effort: Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
@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
…ant adapter guards, re-home ledger read helpers Round-2 review fixes for the per-migration ledger journal (TML-2769), addressing the operator review comments on PR #665. - migrationEdges is now a required field of the single runner options shape on both the framework SPI and the SQL/Mongo runner options. The synth/greenfield path synthesises a single origin->destination edge at plan time (preserving the prior synth row spelling), collapsing the runners dual write-branch into one per-edge path. The per-edge slice uses executedOperations so idempotency skip-records are retained. - Delete the redundant is{Sql,Mongo}ControlAdapter duck-type guards. The SQL break was a duplicate SqlControlAdapterDescriptor whose create() returned the generic instance; parameterising it with the concrete SqlControlAdapter restores the type with no probe and no cast. - Re-home the read-coercion helpers: SQLite-specific decode moves into the SQLite adapter; the shared origin-sentinel normaliser moves beside EMPTY_CONTRACT_HASH in migration-tools and is consumed by SQL and Mongo (deduping Mongos local sha256:empty literal). Delete family-sql ledger-read. The sentinel value itself is left unchanged. - Defer the control-api operation wrapping readLedger to its status/log consumers; add a client-level readLedger test and amend the slice spec. Signed-off-by: Will Madden <madden@prisma.io>
…ant adapter guards, re-home ledger read helpers Round-2 review fixes for the per-migration ledger journal (TML-2769), addressing the operator review comments on PR #665. - migrationEdges is now a required field of the single runner options shape on both the framework SPI and the SQL/Mongo runner options. The synth/greenfield path synthesises a single origin->destination edge at plan time (preserving the prior synth row spelling), collapsing the runners dual write-branch into one per-edge path. The per-edge slice uses executedOperations so idempotency skip-records are retained. - Delete the redundant is{Sql,Mongo}ControlAdapter duck-type guards. The SQL break was a duplicate SqlControlAdapterDescriptor whose create() returned the generic instance; parameterising it with the concrete SqlControlAdapter restores the type with no probe and no cast. - Re-home the read-coercion helpers: SQLite-specific decode moves into the SQLite adapter; the shared origin-sentinel normaliser moves beside EMPTY_CONTRACT_HASH in migration-tools and is consumed by SQL and Mongo (deduping Mongos local sha256:empty literal). Delete family-sql ledger-read. The sentinel value itself is left unchanged. - Defer the control-api operation wrapping readLedger to its status/log consumers; add a client-level readLedger test and amend the slice spec. Signed-off-by: Will Madden <madden@prisma.io>
04312ec to
15aa21a
Compare
…ant adapter guards, re-home ledger read helpers Round-2 review fixes for the per-migration ledger journal (TML-2769), addressing the operator review comments on PR #665. - migrationEdges is now a required field of the single runner options shape on both the framework SPI and the SQL/Mongo runner options. The synth/greenfield path synthesises a single origin->destination edge at plan time (preserving the prior synth row spelling), collapsing the runners dual write-branch into one per-edge path. The per-edge slice uses executedOperations so idempotency skip-records are retained. - Delete the redundant is{Sql,Mongo}ControlAdapter duck-type guards. The SQL break was a duplicate SqlControlAdapterDescriptor whose create() returned the generic instance; parameterising it with the concrete SqlControlAdapter restores the type with no probe and no cast. - Re-home the read-coercion helpers: SQLite-specific decode moves into the SQLite adapter; the shared origin-sentinel normaliser moves beside EMPTY_CONTRACT_HASH in migration-tools and is consumed by SQL and Mongo (deduping Mongos local sha256:empty literal). Delete family-sql ledger-read. The sentinel value itself is left unchanged. - Defer the control-api operation wrapping readLedger to its status/log consumers; add a client-level readLedger test and amend the slice spec. Signed-off-by: Will Madden <madden@prisma.io>
f1d9449 to
d5e442b
Compare
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>
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>
…ant adapter guards, re-home ledger read helpers Round-2 review fixes for the per-migration ledger journal (TML-2769), addressing the operator review comments on PR #665. - migrationEdges is now a required field of the single runner options shape on both the framework SPI and the SQL/Mongo runner options. The synth/greenfield path synthesises a single origin->destination edge at plan time (preserving the prior synth row spelling), collapsing the runners dual write-branch into one per-edge path. The per-edge slice uses executedOperations so idempotency skip-records are retained. - Delete the redundant is{Sql,Mongo}ControlAdapter duck-type guards. The SQL break was a duplicate SqlControlAdapterDescriptor whose create() returned the generic instance; parameterising it with the concrete SqlControlAdapter restores the type with no probe and no cast. - Re-home the read-coercion helpers: SQLite-specific decode moves into the SQLite adapter; the shared origin-sentinel normaliser moves beside EMPTY_CONTRACT_HASH in migration-tools and is consumed by SQL and Mongo (deduping Mongos local sha256:empty literal). Delete family-sql ledger-read. The sentinel value itself is left unchanged. - Defer the control-api operation wrapping readLedger to its status/log consumers; add a client-level readLedger test and amend the slice spec. Signed-off-by: Will Madden <madden@prisma.io>
Rebase onto main made migrationEdges required on runner execute options; update integration mongo suites and the SQLite e2e harness to synthesise a single edge per plan via buildSynthMigrationEdge. Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
…n-as-null follow-up slices Signed-off-by: Will Madden <madden@prisma.io>
605afbe to
cfff59a
Compare
…ant adapter guards, re-home ledger read helpers Round-2 review fixes for the per-migration ledger journal (TML-2769), addressing the operator review comments on PR #665. - migrationEdges is now a required field of the single runner options shape on both the framework SPI and the SQL/Mongo runner options. The synth/greenfield path synthesises a single origin->destination edge at plan time (preserving the prior synth row spelling), collapsing the runners dual write-branch into one per-edge path. The per-edge slice uses executedOperations so idempotency skip-records are retained. - Delete the redundant is{Sql,Mongo}ControlAdapter duck-type guards. The SQL break was a duplicate SqlControlAdapterDescriptor whose create() returned the generic instance; parameterising it with the concrete SqlControlAdapter restores the type with no probe and no cast. - Re-home the read-coercion helpers: SQLite-specific decode moves into the SQLite adapter; the shared origin-sentinel normaliser moves beside EMPTY_CONTRACT_HASH in migration-tools and is consumed by SQL and Mongo (deduping Mongos local sha256:empty literal). Delete family-sql ledger-read. The sentinel value itself is left unchanged. - Defer the control-api operation wrapping readLedger to its status/log consumers; add a client-level readLedger test and amend the slice spec. Signed-off-by: Will Madden <madden@prisma.io>
…ble (prisma#704) ## At a glance _Representative output (multi-space ledger; column alignment from unit-test golden on this branch). At the terminal the divider row is dim; timestamps below use local time with offset._ ``` $ prisma-next migration log Applied at Space Migration Change Ops ─────────────────────────────── ────── ────────────────────── ──────────────────────── ─────── 2026-06-01 10:00:00 +02:00 app 20260301_init ∅ → ef9de27 5 ops 2026-06-01 10:00:02 +02:00 audit 20260301_init ∅ → 9a1c2f3 3 ops 2026-06-02 10:30:00 +02:00 app 20260303_add_phone ef9de27 → 73e3abe 2 ops 2026-06-03 11:00:00 +02:00 app 20260305_rollback 73e3abe → ef9de27 2 ops ``` ## The decision `migration log` now reads the per-migration ledger journal directly from the connected database and presents it as a single flat chronological table — every applied migration across every space, ordered by `appliedAt` (oldest first). Human output on a TTY renders local time with a numeric timezone offset; `--utc` switches human output to UTC; `--json`, non-TTY pipes, and other machine paths emit ISO-8601 UTC (`…Z`) for stable tooling. This replaces the previous `migration log` behaviour wholesale: no on-disk graph, no `findPath` reconstruction, no per-space sections. ## Narrative ### Ledger journal (TML-2769, merged) The on-apply ledger was restructured in [prisma#665](prisma#665) into a per-migration journal: one row per applied edge with `space`, `migrationName`, `migrationHash`, `from` / `to`, `operationCount`, and `appliedAt`. That shape is what `status` matches against and what `log` is meant to surface. ### `migration log` as the human-facing journal view `log` answers “what actually ran against this database, and when?” It calls `readLedger()` with no space filter so the adapter returns the whole table, sorts globally by `appliedAt`, and prints aligned rows (`Applied at` · optional `Space` · `Migration` · `Change` · `Ops`). The ledger is conceptually flat, so the command stays flat too — not space-scoped like `list` / `graph` / `status`. The `Space` column appears only when more than one space contributes rows. Rollbacks and re-applies are repeated uniform rows; `from → to` (with `∅` for a null origin) carries the story without classifying event kinds. ### Timestamp formatting TTY humans get local time plus offset (`2026-06-01 10:00:00 +02:00`). `--utc` is human-only and prints UTC with a `Z` suffix. Machine output (`--json`, or any non-TTY pipe) is always ISO-8601 UTC regardless of `--utc`, so scripts never inherit the operator’s timezone. ### Styling `from → to` uses the shared migration-list colour palette so hash transitions read the same way as in `migration graph` and `migration status`. Column layout is a dedicated flat table renderer (not the shared tree). ## What this PR does not change - **Ledger schema or `readLedger` contract** — already landed in TML-2769 ([prisma#665](prisma#665)); this PR only widens the read to be space-optional and wires `log` to it. - **`migration list`** — separate slice ([prisma#706](prisma#706)). - **`migration status`** — separate slice ([prisma#705](prisma#705)). - **Ledger writes** — the migrate runner that appends rows is untouched. ## Alternatives considered 1. **Per-space sections with headings (like `graph` / `list` / `status`)** — rejected. The ledger is a flat table; sectioning would re-group flat data and break global chronological ordering across spaces. 2. **Render via the shared tree renderer with a chronological pivot** — rejected. The tree’s value is structural topology; for apply history ordered by time, a table is the right shape. 3. **Default to UTC everywhere** — rejected. Humans benefit from local time at the terminal; tooling gets stable `Z` ISO timestamps on `--json` / pipes. ## Linked issue Refs [TML-2770](https://linear.app/prisma-company/issue/TML-2770). Builds on ledger journal [TML-2769](https://linear.app/prisma-company/issue/TML-2769) ([prisma#665](prisma#665)). ## Testing performed - `pnpm --filter @prisma-next/cli... build` - `pnpm --filter @prisma-next/cli typecheck` - CLI: `migration-log-table`, `migration-log`, `readLedger` client, JSON golden tests - Adapter unscoped reads: SQLite `runner.ledger`, Mongo `marker-ledger` - `pnpm lint:deps` - Full CI (pending on PR) ## Skill update n/a — internal CLI behaviour change; no published skill documents `migration log` table layout yet. ## Checklist - [x] All commits are signed off (`git commit -s`) - [x] I read CONTRIBUTING.md and the change is scoped to one logical concern - [x] Tests are updated - [x] PR title uses `TML-2770: …` form - [x] Skill update section filled in <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Migration log command now reads data directly from the database ledger * Added UTC timestamp formatting option for migration log display * Enhanced migration log output with improved table rendering and formatting * **Changes** * Ledger read operations now return entries for all spaces when the space parameter is omitted <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Will Madden <madden@prisma.io>
## Close-out: migration-graph-rendering Closes the `migration-graph-rendering` project (TML-2746). It began as a redesign of `migration graph`'s renderer and broadened into a revamp of the whole interrogative migration **read-command family** (`list` / `graph` / `status` / `log`) on a shared renderer + ledger foundation. All work has shipped; this PR migrates the durable knowledge into `docs/` and deletes the transient project scaffolding. ### Definition of Done — verified | Outcome | Evidence | |---|---| | Tier-3 renderer rebuilt (line/plane/occlusion) | #762 | | Back-arc convergence, configurable geometry, greedy lane colouring, layout fixes | #767 | | Ledger foundation (per-migration journal) | #665 | | `list`/`status`/`log` revamped on the shared renderer; dagre + `list --graph` retired | shipped; verified — `migration-list/status/log/graph.ts` use the shared renderer; no `dagre`/`tree-render`/`layout` renderers remain | | Read-command consistency (TML-2801) | re-validated this PR: 4/7 findings resolved, 2 partial, 1 open (4 small follow-ups noted below) | | Showcase real-world golden | on `main` | No unmet acceptance criteria. External-reference scan for `projects/migration-graph-rendering/` is empty (reference-strip step was a no-op). ### Durable knowledge migrated to `docs/` - **ADR 227** — Migration read commands share one graphical renderer with command-specific annotations. - **ADR 228** — The migration apply ledger is a per-migration journal. - **ADR 229** — The migration graph renderer uses a line/plane/occlusion model (the renderer's internal design — lines as the primitive, single-owner cells, occlusion over blended glyphs). All three verified against shipped code. - **`docs/reference/Migration Graph Visual Language.md`** — the glyph/layout vocabulary the renderer draws from (was the project's `mockups.md`). The read-command consistency audit was **re-validated** against current code (verdict: largely accomplished — 4/7 findings resolved, 2 partial, 1 open) and captured as a Linear follow-up ticket (**TML-2877**, related to TML-2801) rather than a committed doc, since what remains is actionable backlog, not long-lived reference. Transient artefacts (spec, plan, slice specs/plans/reviews, `decisions.md` — now ADR'd, `learnings.md`, the followups draft, `trace.jsonl`, prototype, the audit doc) deleted with the folder / moved to Linear. ### Follow-ups (tracked, not in this PR) - **TML-2877** — the four remaining read-command consistency items (show `--space` policy, log unscoped-semantics doc, check see-also, show/check decoration flags, + a parity-test extension). - **PR #773** — the demo fixture no-op self-edge fix + offline integrity guard. ### Retro — lessons - **A wholesale rewrite obsoletes fine-grained bug-slices.** Three glyph-bug slice specs (tee/marker bugs) were made moot by the line/plane/occlusion rewrite — they targeted deleted code. Re-triage the backlog after a rewrite; don't carry dead slices. - **Hand-authored goldens beat auto-snapshots for correctness.** `toMatchSnapshot()` self-certifies whatever the renderer emits; the hand-authored `golden-pipeline` oracle caught a convergence regression the snapshots happily recorded as "correct." - **Real-world fixtures expose layout bugs unit fixtures miss.** Validating against the `showcase` graph surfaced four distinct layout/colour bugs (disconnected-component interleave, asymmetric-diamond merge lane, trunk-continuation, greedy-colour wraparound) that the simple scenarios never hit. - **`@prisma-next/cli` runs vitest with `isolate: false`** — "passes locally" ≠ passes in CI (parallel state pollution). Candidate for a durable testing note. - **`fixtures:emit` can emit an integrity-failing fixture** — the emitter and `migration check` disagreed (a hash-collapse produced a no-op self-edge). Landed an offline demo integrity guard (#773). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Will Madden <madden@prisma.io> Co-authored-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