Skip to content

TML-2769: restructure the migration ledger into a readable per-migration journal#665

Merged
wmadden merged 14 commits into
mainfrom
tml-2769-make-the-migration-ledger-readable
Jun 2, 2026
Merged

TML-2769: restructure the migration ledger into a readable per-migration journal#665
wmadden merged 14 commits into
mainfrom
tml-2769-make-the-migration-ledger-readable

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Linked issue

Refs TML-2769. Control-plane foundation for migration status (TML-2748) and migration 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 readLedger API that returns the same shape on every target:

readLedger(options: {
  readonly driver: ControlDriverInstance<TFamilyId, string>;
  readonly space: string;
}): Promise<readonly LedgerEntryRecord[]>; // append (apply) order

interface LedgerEntryRecord {
  readonly space: string;
  readonly migrationName: string;
  readonly migrationHash: string;
  readonly from: string | null; // null only for the ∅ origin
  readonly to: string;
  readonly appliedAt: Date;
  readonly operationCount: number;
}

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:

  1. Per-edge write. A space-apply walks a path of edges; we now write one ledger row per applied edge (inside the per-space transaction, in walk order) instead of one collapsed row. Each row records the edge's space, migration_name (dirName), migration_hash (the exact-match key status will use), per-edge from/to core hashes, and the edge's authored operations (sliced from plan.operations by operationCount).
  2. Schema convergence. Postgres/SQLite gain space + migration_name + migration_hash columns; Mongo gains migrationName + migrationHash + operations on its ledger docs. contract_json_before/after is retained but materialised only at the apply's endpoints (first/last edge) — interior edges store null.
  3. readLedger read API on ControlFamilyInstance, both family instances, all three adapters, and the CLI control client (a thin pass-through mirroring readMarker). 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) under projects/migration-graph-rendering/ — the spec and plan this slice was cut from.

How it fits together

  1. Thread the per-edge breakdown to the runner. apply.ts already computes PerSpacePlan.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.
  2. Write one row per edge. The SQL runners (postgres/sqlite) and the Mongo runner replace the single ledger write with a loop over the edges, slicing plan.operations by each edge's operationCount to attribute ops in walk order, asserting sum(operationCount) === plan.operations.length. Synth (greenfield db init) plans have no authored edges, so they keep writing a single synthesised row keyed by the plan destination (from = null, empty migration name).
  3. Read it back with parity. Each adapter's readLedger probes for the ledger store (returning [] when absent, mirroring readMarker), selects the space's rows in apply order, and projects them to LedgerEntryRecord. The ∅ origin reads as from: null on every target (normalising SQL null/sha256:empty and Mongo's ''), and operationCount is derived from the stored ops rather than returning the ops array — so the three targets yield an identical record.

Behavior changes & evidence

  • SQL ledger writes one row per applied edge with space/migration_name/migration_hash and per-edge fromto chaining; endpoint-only contract_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.
  • Mongo ledger writes one _prisma_migrations doc per applied edge carrying migrationName/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

  • migrationEdges reaches the Mongo runner structurally, not by a declared framework type. The framework MigrationRunnerPerSpaceOptions can't import AggregateMigrationEdgeRef (it lives in migration-tools, a higher layer than framework-components, and lint:deps forbids 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.
  • The ∅-origin sentinel 'sha256:empty' + its from→null normalisation helper are duplicated across the three adapters rather than sharing the real EMPTY_CONTRACT_HASH constant. Deliberate for now (the shared home is a cross-layer move); tracked as item 1 of TML-2774.
  • Synth op-count parity is unconditional only because greenfield init has no idempotency skips. SQL synth counts skip records, Mongo synth doesn't; latent divergence tracked as item 3 of TML-2774.
  • Largest surface is the read-side commit (8d6f2bba6) — it touches the SPI, both families, three adapters, and the client, but every layer is a mechanical mirror of the existing readMarker wiring.
  • No back-compat / migration of existing ledger rows — this is a prototype; old-shaped rows are not read.

Verification

Run on the post-merge HEAD (branch merged up to origin/main):

  • @prisma-next/cli typecheck — 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 passed
  • biome check on touched files — clean (pre-existing no-bare-cast infos only)

Follow-ups

  • TML-2774 — harden the journal: share the ∅-origin sentinel, make Mongo's migrationEdges threading type-safe, align synth op-count parity and read robustness, add throw-path + wrapper-threading tests.
  • TML-2748 (status) and TML-2770 (log) consume this read API.

Alternatives considered

  • Add two columns, keep the collapsed-per-apply row. Rejected: a per-apply row can't answer "is this edge applied?" (status) or "one row per apply event" (log) — both need per-edge granularity, so the restructure was the honest first step.
  • Materialise intermediate contract_json for 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 store null.
  • Return the operations array from readLedger. Rejected in favour of operationCount — 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/log commands ship separately).

Checklist

  • All commits are signed off (git commit -s).
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated.
  • The PR title is in TML-NNNN: <sentence-case title> form.
  • The Skill update section above is filled in.

Summary by CodeRabbit

  • New Features
    • Migration ledger tracking: records applied migrations with space, name, hash, boundaries, timestamp, and operation counts.
    • readLedger(): new API to retrieve per-space migration history in apply order.
    • Per-edge ledger entries: runner writes one ledger row per migration edge for finer visibility.
    • Planner/tooling now synthesizes and surfaces migration-edge metadata for execution and reporting.

@wmadden-electric wmadden-electric requested a review from a team as a code owner June 1, 2026 13:02
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 36815ab5-a5c5-4f03-b737-05a191768d60

📥 Commits

Reviewing files that changed from the base of the PR and between d5e442b and cfff59a.

⛔ Files ignored due to path filters (7)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/migration-graph-rendering/README.md is excluded by !projects/**
  • projects/migration-graph-rendering/decisions.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/edges-on-plan/spec.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/empty-origin-as-null/spec.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/ledger-foundation/plan.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/ledger-foundation/spec.md is excluded by !projects/**
📒 Files selected for processing (79)
  • examples/mongo-demo/test/manual-migration.test.ts
  • examples/retail-store/test/manual-migration.test.ts
  • examples/retail-store/test/migration-chain.test.ts
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/1-framework/0-foundation/contract/src/types.ts
  • packages/1-framework/1-core/framework-components/src/control/control-instances.ts
  • packages/1-framework/1-core/framework-components/src/control/control-migration-types.ts
  • packages/1-framework/3-tooling/cli/src/commands/migration-status.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/apply.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/migration-apply.ts
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/apply.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/client.test.ts
  • packages/1-framework/3-tooling/migration/package.json
  • packages/1-framework/3-tooling/migration/src/aggregate/planner-types.ts
  • packages/1-framework/3-tooling/migration/src/aggregate/planner.ts
  • packages/1-framework/3-tooling/migration/src/aggregate/strategies/synth.ts
  • packages/1-framework/3-tooling/migration/src/aggregate/synth-migration-edge.ts
  • packages/1-framework/3-tooling/migration/src/exports/aggregate.ts
  • packages/1-framework/3-tooling/migration/src/exports/ledger-origin.ts
  • packages/1-framework/3-tooling/migration/src/ledger-origin.ts
  • packages/1-framework/3-tooling/migration/test/aggregate/strategies/synth.test.ts
  • packages/1-framework/3-tooling/migration/test/ledger-origin.test.ts
  • packages/1-framework/3-tooling/migration/tsdown.config.ts
  • packages/2-mongo-family/9-family/src/core/control-adapter.ts
  • packages/2-mongo-family/9-family/src/core/control-instance.ts
  • packages/2-sql/9-family/src/core/control-adapter.ts
  • packages/2-sql/9-family/src/core/control-instance.ts
  • packages/2-sql/9-family/src/core/migrations/types.ts
  • packages/3-mongo-target/1-mongo-target/src/core/mongo-runner.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner-integration.test.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner.polymorphism.integration.test.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner.schema-verify.integration.test.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner.test.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner.validator-closed.integration.test.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner.validator-widen.integration.test.ts
  • packages/3-mongo-target/2-mongo-adapter/package.json
  • packages/3-mongo-target/2-mongo-adapter/src/core/marker-ledger.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/mongo-control-adapter.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/runner-deps.ts
  • packages/3-mongo-target/2-mongo-adapter/src/exports/control.ts
  • packages/3-mongo-target/2-mongo-adapter/test/marker-ledger.test.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/statement-builders.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/runner.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/fixtures/runner-fixtures.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/planner.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/planner.reconciliation.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/planner.storage-types.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.across-spaces.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.basic.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.errors.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.execution-checks.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.idempotency.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.ledger.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.policy.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/schema-verify.after-runner.integration.test.ts
  • packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/ledger-decode.ts
  • packages/3-targets/6-adapters/sqlite/test/ledger-decode.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/fixtures/runner-fixtures.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.across-spaces.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.basic.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.errors.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.idempotency.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.ledger.test.ts
  • test/e2e/framework/test/sqlite/migrations/harness.ts
  • test/integration/test/mongo/aggregate-e2e.test.ts
  • test/integration/test/mongo/codec-rehydration-guardrail.test.ts
  • test/integration/test/mongo/migration-authoring-e2e.test.ts
  • test/integration/test/mongo/migration-e2e.test.ts
  • test/integration/test/mongo/migration-m2-vocabulary.test.ts
  • test/integration/test/mongo/migration-psl-authoring.test.ts
  • test/integration/test/mongo/runner.test.ts
  • test/integration/test/mongo/synth-migration-edges.ts

📝 Walkthrough

Walkthrough

This PR adds ledger reading capability and per-edge ledger tracking throughout the migration framework. It defines a LedgerEntryRecord contract type, extends control APIs with readLedger methods, enables migration runners to emit one ledger entry per migration edge instead of a single collapsed row, and implements the full reading/writing flow across MongoDB and SQL targets (Postgres/SQLite).

Changes

Ledger Reading and Per-Edge Ledger Tracking

Layer / File(s) Summary
Contract types and exports
packages/1-framework/0-foundation/contract/src/types.ts, packages/1-framework/0-foundation/contract/src/exports/types.ts
Adds LedgerEntryRecord type describing per-space ledger rows (space, migrationName, migrationHash, from
Control API surface & CLI client
packages/1-framework/1-core/framework-components/src/control/control-instances.ts, packages/1-framework/3-tooling/cli/src/control-api/types.ts, packages/1-framework/3-tooling/cli/src/control-api/client.ts
Adds readLedger(space?) to public control client and family interfaces; updates type-only imports to include LedgerEntryRecord; client delegates to family instance.
Migration-runner per-space options & synth helpers
packages/1-framework/1-core/framework-components/src/control/control-migration-types.ts, packages/1-framework/3-tooling/migration/src/aggregate/planner-types.ts, packages/1-framework/3-tooling/migration/src/aggregate/strategies/synth.ts, packages/1-framework/3-tooling/migration/src/aggregate/synth-migration-edge.ts
Makes migrationEdges required on per-space plans/options; adds buildSynthMigrationEdge and wires currentMarker into synthesis so synth/at-head plans emit a synthesized edge.
Threading migrationEdges through apply flows
packages/1-framework/3-tooling/cli/src/control-api/operations/apply.ts, packages/1-framework/3-tooling/cli/src/control-api/operations/migration-apply.ts
Passes migrationEdges from per-space plans into runner execute inputs and uses synthesized edges for at-head/no-op scenarios.
Mongo target runner & adapter
packages/3-mongo-target/1-mongo-target/src/core/mongo-runner.ts, packages/3-mongo-target/2-mongo-adapter/src/core/marker-ledger.ts, packages/3-mongo-target/2-mongo-adapter/src/core/mongo-control-adapter.ts, packages/3-mongo-target/2-mongo-adapter/src/core/runner-deps.ts, packages/3-mongo-target/2-mongo-adapter/src/exports/control.ts
Runner now requires migrationEdges and records one ledger entry per edge (validates operationCount sums, slices operations per edge). Adapter adds readLedger(db, space) and expands writeLedgerEntry payload with migration metadata and operations; control adapter exposes readLedger.
SQL targets (Postgres & SQLite) runner, schema, statement builders, adapters
packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts, packages/3-targets/3-targets/postgres/src/core/migrations/statement-builders.ts, packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts, packages/3-targets/3-targets/sqlite/src/core/migrations/runner.ts, packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts, packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
Runners refactored to recordLedgerEntries emitting one row per edge; ledger table schemas extended with space, migration_name, migration_hash; Postgres/SQLite adapters add readLedger with normalization and operationCount derivation.
Ledger decoding helpers (SQLite) and ledger-origin normalization
packages/3-targets/6-adapters/sqlite/src/core/ledger-decode.ts, packages/1-framework/3-tooling/migration/src/ledger-origin.ts
Adds coerceLedgerAppliedAt and operationCountFromStored for SQLite decoding; adds ledgerOriginFromStored to normalize empty/empty-sentinel origins.
Tests, fixtures, examples, and test harnesses
packages/3-mongo-target/1-mongo-target/test/*, packages/3-mongo-target/2-mongo-adapter/test/marker-ledger.test.ts, packages/3-targets/6-adapters/*/test/migrations/*, test/integration/test/mongo/*, test/e2e/*, examples/*, packages/1-framework/3-tooling/cli/test/*
Extensive test updates to supply migrationEdges to runner.execute across suites, new per-edge ledger tests for Mongo/Postgres/SQLite, ledger-decode tests, ledger-origin tests, and example test updates.
Packaging/exports config
packages/1-framework/3-tooling/migration/package.json, packages/1-framework/3-tooling/migration/src/exports/*, packages/1-framework/3-tooling/migration/tsdown.config.ts
Adds exports/ledger-origin subpath and re-exports for ledger-origin and synth edge helper.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prisma/prisma-next#655: Refactors the MigrationRunner API to unified execute({ driver, perSpaceOptions }) shape that this PR extends with migrationEdges for per-edge ledger emission.

Suggested reviewers

  • wmadden
  • aqrln

Poem

🐰 In ledger rows I hop and write,

One edge at once, in order bright.
Mongo, Postgres, SQLite sing,
Each migration edge leaves a ring.
Hoppity tests keep all in sight.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2769-make-the-migration-ledger-readable

@pkg-pr-new

pkg-pr-new Bot commented Jun 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@665

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@665

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@665

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@665

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@665

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@665

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@665

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@665

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@665

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@665

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@665

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@665

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@665

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@665

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@665

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@665

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@665

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@665

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@665

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@665

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@665

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@665

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@665

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@665

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@665

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@665

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@665

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@665

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@665

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@665

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@665

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@665

prisma-next

npm i https://pkg.pr.new/prisma-next@665

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@665

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@665

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@665

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@665

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@665

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@665

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@665

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@665

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@665

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@665

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@665

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@665

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@665

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@665

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@665

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@665

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@665

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@665

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@665

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@665

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@665

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@665

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@665

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@665

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@665

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@665

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@665

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@665

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@665

commit: 605afbe

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 135.67 KB (0%)
postgres / emit 108.02 KB (0%)
mongo / no-emit 75.76 KB (0%)
mongo / emit 70.75 KB (0%)
cf-worker / no-emit 164.62 KB (0%)
cf-worker / emit 133.79 KB (0%)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Update 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 win

Update 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 win

Guard or migrate the legacy _prisma_ledger shape.

CREATE TABLE IF NOT EXISTS leaves an existing ledger table untouched, so upgraded databases keep the old column set and the new reads/inserts in this PR will fail on space, migration_name, or migration_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 win

Use structured error for validation failure.

Line 280 throws a generic Error when edge operation counts don't sum to plan length. Consider using errorRunnerFailed (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 tradeoff

Error handling inconsistency: direct throw vs runner failure pattern.

The validation at lines 637-640 throws an Error directly, but other runner errors return runnerFailure(...) (e.g., lines 321-332, 525-534). Consider wrapping this in a runnerFailure for 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 recordLedgerEntries return type to Promise<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 win

Hardcoded EMPTY_ORIGIN_CORE_HASH matches the canonical EMPTY_CONTRACT_HASH ('sha256:empty'), so there’s no correctness mismatch risk. Keeping the local constant is fine; optionally import and compare against EMPTY_CONTRACT_HASH for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 82216b8 and 860c52c.

⛔ Files ignored due to path filters (7)
  • projects/migration-graph-rendering/README.md is excluded by !projects/**
  • projects/migration-graph-rendering/decisions.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/ledger-foundation/plan.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/ledger-foundation/spec.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/list-renders-tree/spec.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/migration-graph-space-flag/spec.md is excluded by !projects/**
  • projects/migration-graph-rendering/slices/remove-list-graph-renderer/spec.md is excluded by !projects/**
📒 Files selected for processing (29)
  • packages/1-framework/0-foundation/contract/src/exports/types.ts
  • packages/1-framework/0-foundation/contract/src/types.ts
  • packages/1-framework/1-core/framework-components/src/control/control-instances.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/apply.ts
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/2-mongo-family/9-family/src/core/control-adapter.ts
  • packages/2-mongo-family/9-family/src/core/control-instance.ts
  • packages/2-sql/9-family/src/core/control-adapter.ts
  • packages/2-sql/9-family/src/core/control-instance.ts
  • packages/2-sql/9-family/src/core/migrations/types.ts
  • packages/3-mongo-target/1-mongo-target/src/core/mongo-runner.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner.test.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/marker-ledger.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/mongo-control-adapter.ts
  • packages/3-mongo-target/2-mongo-adapter/src/core/runner-deps.ts
  • packages/3-mongo-target/2-mongo-adapter/src/exports/control.ts
  • packages/3-mongo-target/2-mongo-adapter/test/marker-ledger.test.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/statement-builders.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/runner.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/fixtures/runner-fixtures.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/runner.ledger.integration.test.ts
  • packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/fixtures/runner-fixtures.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.ledger.test.ts

Comment thread packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts Outdated
Comment thread packages/2-sql/9-family/src/core/control-instance.ts Outdated
Comment thread packages/2-sql/9-family/src/core/migrations/types.ts Outdated
Comment thread packages/3-targets/6-adapters/sqlite/src/core/ledger-decode.ts
Comment thread packages/3-targets/6-adapters/sqlite/src/core/ledger-decode.ts
Comment thread packages/3-mongo-target/2-mongo-adapter/src/core/marker-ledger.ts Outdated
wmadden-electric pushed a commit that referenced this pull request Jun 2, 2026
…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>
Comment thread packages/1-framework/0-foundation/contract/src/types.ts
wmadden-electric pushed a commit that referenced this pull request Jun 2, 2026
…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>
@wmadden-electric wmadden-electric force-pushed the tml-2769-make-the-migration-ledger-readable branch from 04312ec to 15aa21a Compare June 2, 2026 13:50
wmadden-electric pushed a commit that referenced this pull request Jun 2, 2026
…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>
@wmadden-electric wmadden-electric force-pushed the tml-2769-make-the-migration-ledger-readable branch from f1d9449 to d5e442b Compare June 2, 2026 14:17
wmadden added 14 commits June 2, 2026 16:37
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>
@wmadden wmadden force-pushed the tml-2769-make-the-migration-ledger-readable branch from 605afbe to cfff59a Compare June 2, 2026 14:37
@wmadden wmadden merged commit 3ed2569 into main Jun 2, 2026
7 of 8 checks passed
wmadden added a commit that referenced this pull request Jun 2, 2026
…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>
@wmadden wmadden deleted the tml-2769-make-the-migration-ledger-readable branch June 2, 2026 14:37
medz pushed a commit to medz/prisma-next that referenced this pull request Jun 5, 2026
…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>
wmadden-electric added a commit that referenced this pull request Jun 9, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants