TML-2768: migration list shows on-disk migrations as a graph tree#706
Conversation
Record cross-command decisions (D11 edge annotations) and the TML-2768 slice spec that routes migration list human output through the shared tree. Signed-off-by: Will Madden <madden@prisma.io>
Introduce edgeAnnotationsByHash on the tree renderer for package facts (operation count, invariants) and wire migration list TTY output through buildMigrationGraphRows → buildMigrationGraphLayout → renderMigrationGraphTree per space. JSON output and flat sort order are unchanged; command copy no longer claims "latest first". Signed-off-by: Will Madden <madden@prisma.io>
📝 WalkthroughWalkthroughThis PR refactors the migration-list CLI rendering from a row-by-row approach to a graph-tree model. Edge annotations (operation counts and invariants) are added to the tree renderer, and the rendering API changes from passing pre-computed topology to passing a ChangesMigration list graph-tree rendering with edge annotations
🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
size-limit report 📦
|
Remove unwired flat-list helpers and the Tier-2 topology classifier, drop the unused topology map parameter from the list renderer, and pin linear and multi-space tree layout with golden assertions. Signed-off-by: Will Madden <madden@prisma.io>
@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: |
Re-export abbreviateContractHash after main merge brought in log formatting that shares list hash display constants. Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/1-framework/3-tooling/cli/src/utils/formatters/migration-list-render.ts`:
- Around line 61-63: The canonicalFrom function currently only maps null to
EMPTY_CONTRACT_HASH but leaves empty-string ('') as a real hash; update
canonicalFrom to treat both null and empty-string as baseline by returning
EMPTY_CONTRACT_HASH when from is null or from === '' (otherwise return the
original from) so rendering uses the baseline symbol; reference function
canonicalFrom and constant EMPTY_CONTRACT_HASH when making the change.
🪄 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: ee947c89-f08b-4f06-bc33-5c3d8e6cee3f
⛔ Files ignored due to path filters (3)
projects/migration-graph-rendering/README.mdis excluded by!projects/**projects/migration-graph-rendering/decisions.mdis excluded by!projects/**projects/migration-graph-rendering/slices/list-renders-tree/spec.mdis excluded by!projects/**
📒 Files selected for processing (9)
packages/1-framework/3-tooling/cli/src/commands/migration-list.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-graph-tree-render.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-list-data-column.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-list-graph-topology.tspackages/1-framework/3-tooling/cli/src/utils/formatters/migration-list-render.tspackages/1-framework/3-tooling/cli/test/commands/migration-list.test.tspackages/1-framework/3-tooling/cli/test/utils/formatters/migration-graph-tree-render.test.tspackages/1-framework/3-tooling/cli/test/utils/formatters/migration-list-render.test.tspackages/1-framework/3-tooling/cli/test/utils/formatters/migration-list-styler.test.ts
💤 Files with no reviewable changes (1)
- packages/1-framework/3-tooling/cli/src/utils/formatters/migration-list-data-column.ts
| function canonicalFrom(from: string | null): string { | ||
| return from ?? EMPTY_CONTRACT_HASH; | ||
| } |
There was a problem hiding this comment.
Treat empty-string from as baseline hash.
Line 62 only canonicalizes null; '' will be treated as a real hash node and render incorrectly instead of mapping to ∅.
💡 Proposed fix
function canonicalFrom(from: string | null): string {
- return from ?? EMPTY_CONTRACT_HASH;
+ return from === null || from.length === 0 ? EMPTY_CONTRACT_HASH : from;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function canonicalFrom(from: string | null): string { | |
| return from ?? EMPTY_CONTRACT_HASH; | |
| } | |
| function canonicalFrom(from: string | null): string { | |
| return from === null || from.length === 0 ? EMPTY_CONTRACT_HASH : from; | |
| } |
🤖 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/1-framework/3-tooling/cli/src/utils/formatters/migration-list-render.ts`
around lines 61 - 63, The canonicalFrom function currently only maps null to
EMPTY_CONTRACT_HASH but leaves empty-string ('') as a real hash; update
canonicalFrom to treat both null and empty-string as baseline by returning
EMPTY_CONTRACT_HASH when from is null or from === '' (otherwise return the
original from) so rendering uses the baseline symbol; reference function
canonicalFrom and constant EMPTY_CONTRACT_HASH when making the change.
…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>
… migration tree ## At a-glance Connected database with one migration still to run: ``` $ prisma-next migration status --no-color app: ○ 3b2d98d (contract) (main) │↑ 20260305_add_avatar 73e3abe → 3b2d98d ⧗ pending ○ 73e3abe (db) │✓ 20260303_add_phone ef9de27 → 73e3abe ✓ applied ○ ef9de27 │✓ 20260301_init ∅ → ef9de27 ✓ applied ○ ∅ 1 pending — run `prisma-next migrate --to 3b2d98d` ``` Hypothetical origin (`--from` drops applied glyphs and the `(db)` marker; pending is still the shortest path from that origin to the target): ``` $ prisma-next migration status --no-color --from ef9de27 --to 3b2d98d app: ○ 3b2d98d (contract) (main) │↑ 20260305_add_avatar 73e3abe → 3b2d98d ⧗ pending ○ 73e3abe │↑ 20260303_add_phone ef9de27 → 73e3abe ⧗ pending ○ ef9de27 │↑ 20260301_init ∅ → ef9de27 ○ ∅ 2 pending — run `prisma-next migrate --to 3b2d98d` ``` *(Representative output — same tree renderer and overlay rules exercised in CLI unit tests and integration journeys.)* ## The decision `migration status` now overlays **applied** and **pending** state on the **shared migration graph tree**, giving you one picture of every on-disk migration's relationship to the database. **Applied** means a ledger row exists for that migration hash (green `✓`). **Pending** means the edge sits on the shortest path from the DB marker (or `--from` origin) to the app contract and is not yet applied (yellow `⧗`). Everything else stays plain — the full on-disk graph, not a pruned subgraph. The old summary-only view and the dagre renderer are deleted; `migration graph` defaults to this tree (the experimental `--tree` flag is gone). ## How we got here ### One tree renderer for the read family `migration graph` already renders contract topology as a condensed annotated tree (`buildMigrationGraphRows` → `buildMigrationGraphLayout` → `renderMigrationGraphTree`). `list` and `status` are meant to draw that same map with different overlays — package facts vs DB state — not maintain parallel layout code. ### `status` used to look different Previously, `migration status` fed a dagre layout and a compact summary that did not match `graph` or `list`. Applied edges were inferred from a graph walk from ∅ to the marker instead of the ledger, which could disagree with what actually ran. ### Overlay on the shared renderer Status computation builds `edgeAnnotationsByHash` (`applied` / `pending`) and passes them into the same tree renderer `graph` uses. The `(db)` marker reuses the existing `dbHash` node overlay. Multi-space projects get one tree section per space (with a `spaceId:` heading when there is more than one), matching `list` / `graph`. ### `--from` for hypotheticals `--from <hash>` overrides the path origin so you can ask offline: "what if my DB were at this contract?" Applied overlays are suppressed (they only make sense against the real DB); pending still follows the shortest path from the hypothetical origin to the target (`--to` or the app contract). ### Dagre removed; tree is the default With `status` on the shared tree, dagre had no remaining consumer. `migration graph` always renders the tree; `--ascii`, `--legend`, `--dot`, and `--json` remain. `@dagrejs/dagre` and its mapper/types/tests are removed. ## What this PR does not change - **`--json` wire shape** — still mirrors `migration list` plus per-migration `status`, with `markerHash` / `targetHash` per space (documented in the slice spec). - **`migration list`** — separate slice ([prisma#706](prisma#706)); this PR only shares the renderer hook `list` will populate later. - **`migration log`** — separate slice ([prisma#704](prisma#704)); flat ledger history, not tree-shaped. ## Linked issue Refs [TML-2748](https://linear.app/prisma-company/issue/TML-2748) (resolves applied-from-ledger [TML-2130](https://linear.app/prisma-company/issue/TML-2130)). ## Testing performed - `pnpm --filter @prisma-next/cli build` - `pnpm --filter @prisma-next/cli typecheck` - `pnpm --filter @prisma-next/cli test` (status overlay, graph default-tree, dagre removal) - `rg 'dagre|graphRenderer|migrationGraphToRenderInput' packages/1-framework/3-tooling/cli/` → empty - `pnpm lint:deps` ## 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 is `TML-2748: …` - [x] n/a — internal CLI surface; skill update not required for this slice ## Alternatives considered - **Keep dagre as a `--graph` alternative renderer** — rejected. Two renderers split the family visually; dagre never matched the tree's information density for branching histories. - **Three states (applied / pending / unreachable)** — rejected. Two states cover every real workflow; unreachable edges stay plain on the full list. - **Reimplement layout inside `status`** — rejected. Share `graph` / `list` layout via the existing engine and differ only in overlays. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added applied/pending status indicators to migration visualizations. * **Bug Fixes** * Improved migration status output with per-space reporting and clearer status summaries. * **Chores** * Removed unused dependency. * Simplified migration graph rendering pipeline. * Removed `--tree` CLI option; graph now renders in optimized format by default. <!-- 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>
At a glance
Interactive
migration list(TTY) now draws the same condensed graph tree asmigration graphandmigration status, with per-package facts on migration rows (N ops,{invariants}) and refs on contract nodes. Machine output is unchanged.Single contract space (no
app:heading when there is only one space):Multiple contract spaces (
spaceId:section headings, two-space tree indent):Piping or
--jsonstill emits the flat package array (MigrationListResult); the tree never appears in machine formats.The decision
migration listnow renders the on-disk migration graph as a tree in human (pretty / TTY) output — contracts as nodes, packages as annotated edges — using the shared renderer the read-command family already uses forgraphand (soon)status. Package facts (operationCount,providedInvariants) ride a new shared edge-annotation overlay on that renderer; refs stay on destination contract nodes.migration list --json, non-TTY stdout, and the flat sort order stay byte-identical for scripts and tooling.Why this shape
One renderer for on-disk topology
list,graph, andstatusall answer questions about on-disk contract-space history. They now share one condensed tree engine for human output and differ only in overlays:listadds package facts on edges,graphemphasizes contract topology and(contract)/(refs)node labels, andstatus(follow-up in #705) will add applied/pending markers. One code path to maintain; one mental model for operators.Flat rows were the wrong default for humans
The old human output was a flat table sorted by migration directory name. That order is lexicographic, not chronological, and it hides branching — for anything beyond a linear chain you had to run
listandgraphtogether to see relationships. The tree is the readable default now that the family shares a renderer; machine consumers still get the faithful flat package inventory in JSON.Human vs machine stays split
The tree is rendered only on the pretty/TTY path. Piping a read command already selects JSON via
resolveOutputFormat, so scripts never accidentally parse box-drawing. This PR also drops the misleading “latest first” wording in the command description; flat JSON order is unchanged (still lexicographic bydirName).What this PR does not change
migration list --json—MigrationListResultshape, field names, and flat sort order (golden tests pin byte-identical JSON).migration graph— separate command, separate{ nodes, edges }JSON contract; not modified here.migration list --graph— already removed earlier in the migration-graph-rendering project; not touched.migration statusDB overlay — applied/pending glyphs and(db)marker land in TML-2748: migration status overlays applied and pending on the shared migration tree #705 (tml-2748-migration-status-db-overlay); this PR only introduces the sharededgeAnnotationsByHashfield thatstatusextends with astatuskey.migration log— out of scope.Alternatives considered
--treeonlist. Rejected: the tree is the right human default now thatgraphandstatusshare one renderer; an opt-in flag would preserve the unreadable path most people would still hit.migration statusdrivelistrendering (status wraps list). Rejected: both commands consume the same tree library; neither owns the other.listpopulates package edge annotations;statusadds DB-state keys on the same overlay type.Linked issue
Refs TML-2768. Related: TML-2697 (original list readability ask). DB overlay follow-up: #705.
Testing performed
pnpm --filter @prisma-next/cli... buildpnpm --filter @prisma-next/cli typecheck--jsonbyte-identical,--space,--ascii, multi-space headings)pnpm lint:depsSkill update
n/a — CLI behaviour change is user-visible but covered by existing migration-command skills; no new flags.
Checklist
git commit -s) per the DCO.TML-NNNN: <sentence-case title>form.n/a — internal onlyabove).Summary by CodeRabbit
New Features
Improvements