Skip to content

TML-2753: route SQL marker/ledger writes through the control adapter SPI#712

Open
wmadden-electric wants to merge 29 commits into
mainfrom
tml-2753-sql-marker-ops-through-adapter
Open

TML-2753: route SQL marker/ledger writes through the control adapter SPI#712
wmadden-electric wants to merge 29 commits into
mainfrom
tml-2753-sql-marker-ops-through-adapter

Conversation

@wmadden-electric
Copy link
Copy Markdown
Contributor

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

Linked issue

Refs TML-2753 — Slice 2 of the marker/ledger typed-query-AST project (Slice 1: #672). Related: TML-2754 (planner adoption), TML-2253 (project root).

At a glance

// Before — runner builds raw SQL strings, dialect-specific merge inline:
const { sql, params } = buildMergeMarkerStatements(driver, marker, contract, plan);
await driver.query(sql, params);

// After — runner hands intent to the family SPI; adapter lowers DML through the
// same visitor path as Slice 1's DDL:
await family.updateMarker(driver, APP_SPACE_ID, {
  storageHash: contract.storage.storageHash,
  profileHash: contract.profileHash,
  invariants: plan.providedInvariants, // ← verbatim; merge happens inside updateMarker
});
await family.writeLedgerEntry(driver, APP_SPACE_ID, ledgerEntry);

No raw INSERT/UPDATE marker SQL in production src outside adapter lowering; SQL/Mongo SqlControlAdapter / MongoControlAdapter are now signature-symmetric on the marker/ledger surface.

Decision

Migrate all SQL marker/ledger DML (init, advance, ledger append) off raw-string builders and onto a typed-AST SqlControlAdapter SPI symmetric with Mongo, with invariant-merge converged into a single TS implementation inside updateMarker and the read path consolidated onto one helper + one parser.

Four dispatches, one merge each:

  1. D1SqlControlAdapter write SPI (initMarker / updateMarker / writeLedgerEntry) + enabling surface: contract-free DML builder (insert / upsert / update), TableSource.schema? for schema-qualified DML, Postgres text[] codec for invariants.
  2. D2 — invariant-merge convergence: mergeInvariants (union + dedupe + stable sort) lives inside updateMarker, which reads current invariants internally — both dialects now accumulate-dedupe via one shared TS implementation.
  3. D3 — read + parser consolidation: one readMarkerResult(queryable, shape) helper in family-sql/verify.ts + one parseContractMarkerRow; the duplicate sql-runtime/marker.ts parser and the SQLite runner's private read are deleted.
  4. D4 (cut-over) — remove the three legacy raw-SQL write builders (buildMergeMarkerStatements, buildWriteMarkerStatements, writeContractMarker) plus dead ledger-insert builders; route PG + SQLite runners and the family sign() path through the SPI; drop the SQLite runner's client-side invariant pre-merge; collapse upsert to INSERT … ON CONFLICT (space) DO UPDATE; add MARKER_CAS_FAILURE runner code.

Reviewer notes — the consequential change

Column-set reduction (deliberate cross-family parity, not a silent drop). The legacy PG merge update refreshed core_hash / profile_hash / contract_json / canonical_version / app_tag / meta / invariants / updated_at on every advance. The new SPI updateMarker writes only core_hash / profile_hash / updated_at (+ invariants when supplied). The other columns are initialised by initMarker and then stale-after-advance — they become write-only provenance.

Verified safe before cut-over: zero production decision-paths read any dropped column.

  • verify() branches only on marker.storageHash (control-instance.ts:498) and marker.profileHash (:516); the rest of marker is passed through into the diagnostic result only.
  • The aggregate planner's ContractMarkerRecordLike exposes only storageHash / invariants / profileHash?.
  • The CLI migration-log formatter reads ledger fields the new write set covers (migrationName / migrationHash / from / to / appliedAt / operationCount).

Mongo parity is genuine: Mongo updateMarker $sets the same reduced field set; Mongo writeLedgerEntry already omits the same legacy snapshot fields (origin_profile_hash / destination_profile_hash / contract_json_before / contract_json_after). The DDL columns are retained for storage-shape compatibility — the SPI just stops writing them. If a future feature needs any of these refreshed on advance, the right move is to add it to both family SPIs (preserving DC-4 symmetry).

Other reviewer notes:

  • Net SQLite invariant behaviour is preserved, not changed. The legacy SQLite runner (runner.ts:605-607) already pre-merged invariants client-side before its overwrite statement, so today's net SQLite behaviour already accumulate-deduped — only the SQL statement overwrote. D2 relocates the merge into updateMarker; D4 drops the runner pre-merge. Convergence is at the SPI/statement layer.
  • CAS + lock/txn primitives unchanged. CAS predicate (core_hash = expectedFrom + RETURNING space row-presence) and lock primitives (pg_advisory_xact_lock(hashtext($1)), BEGIN EXCLUSIVE) are byte-identical to pre-D4.
  • MARKER_CAS_FAILURE is a clean addition to SqlMigrationRunnerErrorCode, symmetric with Mongo's existing code; no caller in tree branched on the prior generic-error path.
  • updated_at stays DB-side (now() for PG, datetime('now') for SQLite via RawExpr) — no wire-semantics change; the dispatch brief overruled an early instinct to switch to a JS Date parameter for Mongo symmetry.
  • Test seeders use a raw-SQL seedTestMarker helper — explicitly out of scope for DC-1, because fixtures hold a raw pg.Client and can't reach the SPI's ControlDriverInstance without a layer violation.

Behavior changes & evidence

Testing performed

  • pnpm build (65/65 cached, post-merge incremental)
  • pnpm typecheck (135/135)
  • pnpm test:packages (9535 passed, 4 expected fail, 3 skipped)
  • pnpm test:integration (1012/1012 under --maxWorkers=4; default parallelism shows intermittent contention on the journey / SQL-ORM suites — same class as the documented init-skill-distribution / init-journey parallelism flake, not slice-induced)
  • pnpm test:e2e (105/105)
  • pnpm fixtures:check (byte-identical)
  • pnpm lint:deps (1023 modules, clean)
  • biome on the slice diff (infos only)
  • git grep for removed symbols (writeContractMarker, buildMergeMarkerStatements, buildWriteMarkerStatements, buildLedgerInsertStatement): zero matches outside projects/

Follow-ups

Alternatives considered

  • Pass currentInvariants as a parameter to updateMarker (Option A) — rejected; would break DC-4 SQL/Mongo SPI signature symmetry. Internal read inside updateMarker (Option B) preserves the symmetric surface and hides dialect-specific merge concerns.
  • Make SqlControlAdapter.readMarker return MarkerReadResult directly — rejected; would force the control SPI to carry a 3-way result its consumers don't want and break readMarker | null symmetry with Mongo. Shared readMarkerResult helper + two thin typed projections (control returns | null, runtime returns the 3-way) keeps "one read home + one parser" without polluting either consumer's signature.
  • Switch updated_at to a JS Date parameter for Mongo symmetry — rejected; would change observable wire semantics. DB-side RawExpr (now() / datetime('now')) keeps the existing timestamps' clock-source.
  • Fold WHERE-builder into the contract-free DML builder — rejected; predicate construction already has a typed expression layer (AndExpr / BinaryExpr / ColumnRef). Reusing it keeps the DML builder focused on insert/upsert/update structure.

Skill update

n/a — internal SPI surface only; no user-facing CLI or config change.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added contract-free DML query builders for INSERT, UPDATE, UPSERT, and SELECT operations with fluent API
    • Added schema-qualified table support for PostgreSQL
    • Added raw SQL expression support in INSERT statements
    • New marker and ledger management control operations for improved state handling
  • Bug Fixes

    • Moved marker write operations to adapter-controlled layer for better consistency and control

wmadden added 16 commits June 2, 2026 18:24
Refine the dispatch plan (4 dispatches: write SPI, invariant-merge,
read/parser consolidation, raw-builder removal); pin the two slice open
questions (ON CONFLICT upsert on both dialects; per-family SPI). Record
the resumed implementer/reviewer in the registry.

Signed-off-by: Will Madden <madden@prisma.io>
D1 R1 recon surfaced that the contract-free DML builder + TableSource
schema-qualification + a Postgres text[] codec do not yet exist (slice 1
shipped DDL only). Intent-validation: these are in-project-scope enabling
work (Project DoD + OQ3), not expansion. Pin updated_at to DB-side time
(RawExpr), overriding a mirror-Mongo new Date() instinct that would change
wire semantics.

Signed-off-by: Will Madden <madden@prisma.io>
…ce for control writes (TML-2753)

Enabling surface for migrating SQL marker/ledger writes onto the adapter SPI:

- relational-core: TableSource.schema? threaded through named()/rewriteTableSource; InsertValue widened to admit RawExpr so DB-side time (now()/datetime('now')) can sit in value position; contract-free DML builder (insert/upsert/update with optional RETURNING) beside the DDL builder, no contract dependency.
- Postgres renderer qualifies "schema"."name" when a literal schema is set and no namespaceId is present; SQLite renderer asserts schema absence (control tables are unqualified).
- sql-runtime: createControlCodecRegistry + deriveParamMetadata/encodeParamsWithMetadata exports so contract-free control DML can encode params from AST-bound codecs.
- Postgres pg/text-array@1 codec for the marker invariants column.

Existing DML fixtures stay byte-identical (pnpm fixtures:check).

Signed-off-by: Will Madden <madden@prisma.io>
… SPI (TML-2753)

Add initMarker / updateMarker / writeLedgerEntry to SqlControlAdapter (interface + Postgres and SQLite implementations), mirroring MongoControlAdapter's method shapes. Each builds query-AST DML via the contract-free DML builder, lowers through adapter.lower() with no contract, and encodes params from AST-bound codecs before hitting the driver.

- Marker writes use INSERT ... ON CONFLICT (space) DO UPDATE SET ... (idempotent initMarker); updateMarker is a CAS UPDATE ... WHERE space = ? AND core_hash = expectedFrom RETURNING space, reporting the swap via row presence.
- updated_at is stamped DB-side (now() / datetime('now')) via RawExpr, not an app-side Date.
- Value codecs attached at the value site: pg jsonb for meta/contract_json/operations, pg text[] for invariants, pg timestamptz for updated_at; SQLite uses JSON-as-TEXT codecs.

Tests pin the lowered SQL on both dialects and exercise the writes end-to-end (PGlite for Postgres, node:sqlite in-memory for SQLite) with readback. The old raw-string write builders are left untouched.

Signed-off-by: Will Madden <madden@prisma.io>
…notes

D1 R1 verdict SATISFIED (DC-4 PASS). Record the review carry-forwards:
D2 computes invariant union/dedupe in the adapter (TS, uniform across
dialects); D4 reconciles the marker column set at cut-over (legacy full-row
rewrite vs new subset). F01 (trivial JSDoc) folded into D2.

Signed-off-by: Will Madden <madden@prisma.io>
… internally (Option B)

Hold the Mongo-symmetric updateMarker signature (project-DoD symmetric SPI +
DC-4) by reading the current invariant set inside updateMarker under the
existing lock (reusing the readMarker path), rather than threading it in as a
param. Negligible cost; no signature leak of the storage difference.

Signed-off-by: Will Madden <madden@prisma.io>
…753)

updateMarker now reads the current invariant set via the existing readMarker
SPI path (under the migration txn + advisory lock), unions it with the incoming
set, dedupes, and sorts ascending in TS before writing back. The merge is
uniform across dialects: SQLite — which stores invariants as JSON TEXT with no
array operators — stops overwriting wholesale and converges on the same
accumulate-dedupe behaviour as Postgres. The SPI signature is unchanged, so it
stays byte-identical to MongoControlAdapter.updateMarker; the current-invariants
source is an internal read, not a new parameter. No new locking is introduced.

Also moves the misplaced lowering-helper JSDoc above execute() (it sat above
const CONTROL_CODECS) in both marker-ledger-writes.ts files.

Signed-off-by: Will Madden <madden@prisma.io>
Legacy SQLite runner already pre-merged invariants client-side, so the
convergence is a layer relocation (runner -> updateMarker SPI) that preserves
net observable behaviour rather than changing it. PR body to state accurately.

Signed-off-by: Will Madden <madden@prisma.io>
…ns (Option 1)

Preserve MarkerReadResult (runtime) and the control readMarker | null shape
(SQL<->Mongo symmetry) by extracting one canonical read with thin typed
projections, rather than widening the control SPI return type (Option 2,
which ripples into D2/runner/CLI). Unify on the verify.ts parser superset.

Signed-off-by: Will Madden <madden@prisma.io>
Collapse the duplicate SQL marker read paths onto a single canonical read
flow and a single row parser, without changing any read semantics.

- Add readMarkerResult(queryable, shape) to @prisma-next/family-sql/verify:
  probe -> select -> optional per-dialect decode -> the verify.ts parser ->
  MarkerReadResult. The runtime AdapterProfile.readMarker returns it verbatim
  (3-way no-table/absent/present preserved); the control SqlControlAdapter
  .readMarker wraps it in withMarkerReadErrorHandling and projects
  present -> record / else -> null, keeping its ContractMarkerRecord | null
  signature (and SQL<->Mongo readMarker symmetry).
- Unify on the verify.ts parser superset and delete the sql-runtime marker.ts
  copy + its re-export; repoint both runtime reads. This decodes SQLite TEXT
  contract_json that the old runtime parser left as a string.
- Share each dialect read via postgres/sqliteMarkerReadShape builders, and
  move decodeSqliteMarkerRow to the sqlite adapter so the runtime and control
  reads spell the read (and the invariants TEXT decode) exactly once.
- Fold the SQLite runner private read into this.family.readMarker (mirrors the
  Postgres runner) and delete the now-dead readMarkerStatement read builder.

Signed-off-by: Will Madden <madden@prisma.io>
Remove the legacy raw-string marker/ledger write builders and cut every
in-scope write call site over to the D1 control-adapter SPI
(initMarker / updateMarker / writeLedgerEntry):

- Drop buildMergeMarkerStatements (postgres), buildWriteMarkerStatements
  (sqlite), their buildLedgerInsertStatement siblings, and the
  sql-runtime writeContractMarker builder, plus the exports/ re-exports
  and the tests that pinned their string shape.
- Postgres and SQLite migration runners now call the family SPI; the
  upsert collapses to INSERT ... ON CONFLICT (space) DO UPDATE in
  initMarker, and CAS failures surface as MARKER_CAS_FAILURE.
- SQLite runner no longer pre-merges invariants client-side; it passes
  the plan-provided invariants verbatim and lets updateMarker do the
  sole TS-side accumulate-dedupe (no double-merge, no overwrite). Net
  behaviour is preserved.
- Marker column-set reconciled to the SPI surface
  (core_hash/profile_hash/updated_at/invariants), matching the Mongo
  adapter; contract_json/canonical_version/meta were write-only
  provenance not read by any typed consumer. Ledger contract_json
  snapshot columns are likewise no longer written.
- Test seeders move to a raw-SQL seedTestMarker helper in sql-runtime
  test utils; ledger tests assert the now-null snapshot columns.

Signed-off-by: Will Madden <madden@prisma.io>
Signed-off-by: Will Madden <madden@prisma.io>
…ps-through-adapter

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric requested a review from a team as a code owner June 3, 2026 13:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR moves contract marker and ledger writes from runtime SQL builders to control-plane adapter methods, introduces a composable contract-free SQL query builder surface with raw-expression support, adds target-specific table sources and column helpers, and implements dialect-specific marker persistence with compare-and-swap semantics and delegated ledger recording.

Changes

SQL control-plane refactor

Layer / File(s) Summary
Core AST: raw expressions and table source refactoring
packages/2-sql/4-lanes/relational-core/src/ast/types.ts, adapter-types.ts
InsertValue now allows RawExpr in insert rows, rewriteInsertValue treats raw expressions as opaque, collectParamRefs() traverses them to collect parameters, TableSource constructor becomes protected, and static named() handles freezing; exported MarkerStatement interface removed.
Generic contract-free DML builders
packages/2-sql/4-lanes/relational-core/src/contract-free/table.ts, dml.ts, test/contract-free/table.test.ts, src/exports/contract-free.ts
table() factory returns typed TableHandle with per-column ColumnProxy metadata, fluent CfInsertQuery, CfUpsertBuilder/CfConflictClause/CfUpsertQuery, CfUpdateQuery, CfSelectQuery builders with value-to-AST conversion and comprehensive test coverage; re-exported from DML and contract-free modules.
PostgreSQL table source and column helpers
packages/3-targets/3-targets/postgres/src/core/ast/table-source.ts, src/contract-free/columns.ts, test/contract-free/columns.test.ts, src/core/sql-renderer.ts, src/exports/contract-free.ts
PostgresTableSource extends TableSource with optional schema field; SQL renderer detects schema and qualifies as "schema"."name"; typed column factories (text, int4, jsonb, textArray, timestamptz) and pgTable() wrapper return schema-typed TableHandle; renderInsertValue now accepts contract and renders raw expressions.
SQLite column helpers
packages/3-targets/3-targets/sqlite/src/contract-free/columns.ts, test/contract-free/columns.test.ts, src/exports/contract-free.ts
Typed column factories (text, integer, jsonText, datetime) and sqliteTable() factory using unqualified flat table names; full test coverage for metadata exposure and query builder output shapes.
Runtime marker cleanup and control codec
packages/2-sql/5-runtime/src/codecs/control-codec-registry.ts, src/sql-marker.ts, src/marker.ts, src/exports/index.ts, README.md, test/utils.ts
Removes write-side marker builders (WriteMarkerInput, WriteContractMarkerStatements, writeContractMarker) and parseContractMarkerRow; adds createControlCodecRegistry() for AST-driven codec resolution; refactors test marker seeding to use direct SQL insertion via seedTestMarker(); updates README to document marker writes via control adapter SPI.
Control adapter SPI and family
packages/2-sql/9-family/src/core/control-adapter.ts, control-instance.ts, migrations/types.ts
SqlControlAdapter interface adds insertMarker(), initMarker(), updateMarker() with compare-and-swap, writeLedgerEntry() methods; SqlControlFamilyInstance gains same public methods; sign() stops using runtime writeContractMarker and delegates to adapter with CAS semantics; SqlMigrationRunnerErrorCode gains MARKER_CAS_FAILURE.
PostgreSQL codec and migration runner
packages/3-targets/3-targets/postgres/src/core/codec-ids.ts, codecs.ts, migrations/runner.ts, migrations/statement-builders.ts, test/migrations/statement-builders.test.ts
Adds PgTextArrayCodec and pgTextArrayDescriptor for text[] encoding/decoding; migration runner calls family.readMarker(), upsertMarker() returns Result with CAS failure handling returning MARKER_CAS_FAILURE, recordLedgerEntries() delegates to family.writeLedgerEntry() per edge; removes buildMergeMarkerStatements export.
PostgreSQL control adapter and marker-ledger
packages/3-targets/6-adapters/postgres/src/core/adapter.ts, control-adapter.ts, marker-ledger.ts, sql-renderer.ts
Adapter delegates profile.readMarker() to shared markerLedger.readMarker(), implements insertMarker, initMarker, updateMarker, writeLedgerEntry by lowering contract-free SQL; shared module defines marker/ledger tables, implements read probes, insertion, upserts, CAS updates with invariants merging, and ledger entry recording.
PostgreSQL control tests
packages/3-targets/6-adapters/postgres/test/marker-ledger-writes.test.ts, test/migrations/marker-ledger-writes.integration.test.ts, test/adapter.test.ts, test/control-adapter.test.ts, test/migrations/runner.*.test.ts
Unit tests verify SQL lowering for marker/ledger operations (INSERT, upsert, CAS, ledger writes); integration tests validate marker idempotency, CAS enforcement on mismatch, invariants accumulation with dedup/sort, ledger persistence with operation counts; adapter and runner tests updated for quoted identifiers.
SQLite control adapter and migration runner
packages/3-targets/3-targets/sqlite/src/core/adapter.ts, migrations/runner.ts, migrations/statement-builders.ts, src/exports/statement-builders.ts
Adapter delegates profile.readMarker() to shared helper, removes inline marker probing; renderInsertValue renders raw-expr via renderExpr; migration runner delegates to family methods; statement builders simplified, exports reduced to table/schema names.
SQLite control adapter and marker-ledger
packages/3-targets/6-adapters/sqlite/src/core/adapter.ts, control-adapter.ts, marker-ledger.ts
Adapter implements insertMarker, initMarker, updateMarker, writeLedgerEntry by lowering contract-free SQL; shared module defines _prisma_marker/_prisma_ledger tables, implements probes, insertion, upserts, CAS updates with JSON invariants merging, and ledger recording with decodeSqliteMarkerRow helper.
SQLite control tests
packages/3-targets/6-adapters/sqlite/test/marker-ledger-writes.test.ts, test/migrations/marker-ledger-writes.test.ts, test/migrations/runner.*.test.ts
Unit tests verify SQL lowering with JSON-encoded invariants and conditional assignment; integration tests validate marker idempotency, CAS enforcement, invariants accumulation via union/dedup, and ledger persistence; runner tests updated for new marker setup via family methods.
Integration tests
test/integration/test/cli.db-verify.*.test.ts, test/integration/test/family.*.test.ts
Tests across CLI verify, family sign, family verify flows switch from runtime writeContractMarker to direct seedTestMarker() calls or family.initMarker() delegations, passing storage/profile hashes, contract JSON, and canonical version explicitly.
Rules and calibration
.agents/rules/no-target-branches.mdc, drive/calibration/dor.md, drive/calibration/failure-modes.md, drive/code/README.md
Rules now treat target-specific optional fields on generic AST classes as layering violations; DoR adds checklist items for architectural invariants, operation-collapse caller verification, and reviewer trace prompts; new failure modes (F16–F21) document acknowledged violations, inverted abstractions, semantic collisions, and AST construction ergonomics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prisma/prisma-next#594: Introduced the RawExpr AST node that this PR extends into the INSERT pipeline, making raw SQL expressions traversable in parameter collection.

Suggested reviewers

  • wmadden
  • saevarb
  • aqrln

🐰 A ledger filled with hashes,
Markers stored in SQL clashes,
Contract-free builders take flight,
Schema-qualified tables delight,
Control plane orchestrates right! ✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2753-sql-marker-ops-through-adapter

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 3, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 3a712f0

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

size-limit report 📦

Path Size
postgres / no-emit 138.97 KB (+1.17% 🔺)
postgres / emit 110.87 KB (+1.88% 🔺)
mongo / no-emit 75.96 KB (0%)
mongo / emit 70.78 KB (0%)
cf-worker / no-emit 168.27 KB (+1.16% 🔺)
cf-worker / emit 136.9 KB (+1.78% 🔺)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
packages/2-sql/9-family/src/core/control-adapter.ts (1)

91-95: ⚡ Quick win

Align the updateMarker contract docs with the new invariant-merge behavior.

This comment still says callers must pre-merge invariants and that the adapter writes them verbatim. The PR contract moved union/dedupe into updateMarker, so leaving this as-is will mislead future adapter implementations into dropping previously recorded invariants.

🤖 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-adapter.ts` around lines 91 - 95,
The docstring for updateMarker is inaccurate: it currently claims callers must
pre-merge invariants and that destination.invariants are written verbatim;
update it to state that updateMarker performs the union/dedupe merge of
invariants itself (so callers do not need to pre-merge), that it atomically
advances the marker via compare-and-swap on core_hash = expectedFrom, returns
true/false as described, and that when destination.invariants is omitted
existing invariants are left untouched; also mirror this clarified contract in
the comment referencing MongoControlAdapter.updateMarker to avoid misleading
future adapter implementations.
packages/3-targets/3-targets/postgres/src/core/codecs.ts (1)

188-190: 💤 Low value

Consider stricter validation in decodeJson for data integrity.

The current implementation returns an empty array for non-array JSON and coerces all entries to strings. While this provides resilience, it could mask data corruption if the marker's invariants JSON is malformed (e.g., null, {}, or an array with unexpected types).

Since this is control-plane data, consider throwing an error for non-array input to surface corruption early, rather than silently recovering with an empty array.

🔍 Stricter alternative
 decodeJson(json: JsonValue): readonly string[] {
-  return Array.isArray(json) ? json.map((entry) => String(entry)) : [];
+  if (!Array.isArray(json)) {
+    throw new Error(`PgTextArrayCodec.decodeJson: expected array, got ${typeof json}`);
+  }
+  return json.map((entry) => {
+    if (typeof entry !== 'string') {
+      throw new Error(`PgTextArrayCodec.decodeJson: expected string entry, got ${typeof entry}`);
+    }
+    return entry;
+  });
🤖 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/codecs.ts` around lines 188 -
190, The decodeJson function currently coerces any non-array input to [] and
casts entries to String, which can hide corrupted control-plane data; update
decodeJson in codecs.ts to validate that the incoming JsonValue is an array and
that every entry is a string (or explicitly convertible if you choose) and throw
a descriptive error (e.g., TypeError or a custom validation error) when the
input is not an array or when any element is not a string, returning readonly
string[] only after successful validation so corruption surfaces early.
packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts (1)

593-630: ⚡ Quick win

Add a regression test for MARKER_CAS_FAILURE.

This branch is new and safety-critical, but none of the provided Postgres runner tests exercise the updateMarker() === false path or assert that it aborts before ledger writes. A small runner test here would lock down both the failure code/metadata and the rollback behavior. As per coding guidelines, **/*.test.{ts,tsx}: Always write tests before creating or modifying implementation.

🤖 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 593 - 630, The tests are missing a regression that exercises the
updateMarker() === false branch in upsertMarker and asserts MARKER_CAS_FAILURE
and rollback behavior; add a unit/integration test (matching **/*.test.{ts,tsx})
that sets up a Postgres runner, seeds an existing ContractMarkerRecord, then
simulates a concurrent modification so family.updateMarker returns false (or
mock family.updateMarker to return false) and verifies upsertMarker returns a
runner failure with code 'MARKER_CAS_FAILURE', the failure meta contains
space/expectedStorageHash/destinationStorageHash, and that no ledger writes were
committed (i.e., migration aborted before ledger operations); target the
upsertMarker method and use family.initMarker/family.updateMarker control to
reproduce the path.
🤖 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/2-sql/4-lanes/relational-core/src/ast/types.ts`:
- Line 83: InsertValue was extended to include RawExpr but the exported alias
AnyInsertValue still omits RawExpr; update the AnyInsertValue type alias (the
exported public alias) to include RawExpr so the public type surface matches the
AST/runtime behavior—locate the AnyInsertValue alias declaration and add RawExpr
to its union alongside ColumnRef, ParamRef, PreparedParamRef, and
DefaultValueExpr.
- Around line 327-348: The TableSource constructor and its factory
TableSource.named currently allow both schema and namespaceId to be set; add a
validation in the TableSource constructor (and keep it effective for
TableSource.named which calls the constructor) that enforces the mutual
exclusivity: if both schema and namespaceId are non-empty/defined, throw an
Error (or assert) with a clear message identifying the invalid combination
(e.g., "TableSource: schema and namespaceId are mutually exclusive"). This
ensures any caller constructing a TableSource (via new TableSource(...) or
TableSource.named(...)) will fail fast on invalid input.

In `@packages/2-sql/5-runtime/README.md`:
- Line 130: Update the README to reflect the new runtime surface: change the
`readContractMarker` bullet to state it only provides marker read statements
(remove any claim that writes are provided by sql-runtime), and add entries in
the "Codecs" section for the exported functions `createControlCodecRegistry`,
`deriveParamMetadata`, and `encodeParamsWithMetadata` (briefly describe each and
note they are exported from src/exports/index.ts) so the docs match the actual
exports.

In `@packages/2-sql/9-family/src/core/control-instance.ts`:
- Around line 604-608: The sign() logic uses readMarker() then calls
controlAdapter.initMarker(...) which currently upserts and can overwrite a
concurrent signer; change the behavior so initMarker is insert-if-absent (atomic
insert) or make initMarker return a distinct result/error when a row already
exists and have sign() detect that and fall back to updateMarker()/conflict
handling instead of assuming success; update references in sign(),
existingMarker, and controlAdapter.initMarker to implement atomic insert-or-fail
semantics (or propagate “already exists” so sign() can retry/merge).

---

Nitpick comments:
In `@packages/2-sql/9-family/src/core/control-adapter.ts`:
- Around line 91-95: The docstring for updateMarker is inaccurate: it currently
claims callers must pre-merge invariants and that destination.invariants are
written verbatim; update it to state that updateMarker performs the union/dedupe
merge of invariants itself (so callers do not need to pre-merge), that it
atomically advances the marker via compare-and-swap on core_hash = expectedFrom,
returns true/false as described, and that when destination.invariants is omitted
existing invariants are left untouched; also mirror this clarified contract in
the comment referencing MongoControlAdapter.updateMarker to avoid misleading
future adapter implementations.

In `@packages/3-targets/3-targets/postgres/src/core/codecs.ts`:
- Around line 188-190: The decodeJson function currently coerces any non-array
input to [] and casts entries to String, which can hide corrupted control-plane
data; update decodeJson in codecs.ts to validate that the incoming JsonValue is
an array and that every entry is a string (or explicitly convertible if you
choose) and throw a descriptive error (e.g., TypeError or a custom validation
error) when the input is not an array or when any element is not a string,
returning readonly string[] only after successful validation so corruption
surfaces early.

In `@packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts`:
- Around line 593-630: The tests are missing a regression that exercises the
updateMarker() === false branch in upsertMarker and asserts MARKER_CAS_FAILURE
and rollback behavior; add a unit/integration test (matching **/*.test.{ts,tsx})
that sets up a Postgres runner, seeds an existing ContractMarkerRecord, then
simulates a concurrent modification so family.updateMarker returns false (or
mock family.updateMarker to return false) and verifies upsertMarker returns a
runner failure with code 'MARKER_CAS_FAILURE', the failure meta contains
space/expectedStorageHash/destinationStorageHash, and that no ledger writes were
committed (i.e., migration aborted before ledger operations); target the
upsertMarker method and use family.initMarker/family.updateMarker control to
reproduce the path.
🪄 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: 9c8b3e2a-8132-4400-80d4-90d30cbfbc81

📥 Commits

Reviewing files that changed from the base of the PR and between e4b7a7c and 04f1be4.

⛔ Files ignored due to path filters (3)
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/plan.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/spec.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/subagent-registry.md is excluded by !projects/**
📒 Files selected for processing (43)
  • packages/2-sql/4-lanes/relational-core/src/ast/types.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/dml.ts
  • packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts
  • packages/2-sql/5-runtime/README.md
  • packages/2-sql/5-runtime/src/codecs/control-codec-registry.ts
  • packages/2-sql/5-runtime/src/exports/index.ts
  • packages/2-sql/5-runtime/src/marker.ts
  • packages/2-sql/5-runtime/src/sql-marker.ts
  • packages/2-sql/5-runtime/test/sql-marker.test.ts
  • packages/2-sql/5-runtime/test/utils.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/2-sql/9-family/src/core/verify.ts
  • packages/2-sql/9-family/src/exports/verify.ts
  • packages/3-targets/3-targets/postgres/src/core/codec-ids.ts
  • packages/3-targets/3-targets/postgres/src/core/codecs.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/postgres/src/exports/statement-builders.ts
  • packages/3-targets/3-targets/postgres/test/migrations/statement-builders.test.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/3-targets/sqlite/src/exports/statement-builders.ts
  • packages/3-targets/6-adapters/postgres/src/core/adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/marker-ledger-writes.ts
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
  • packages/3-targets/6-adapters/postgres/test/marker-ledger-writes.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/marker-ledger-writes.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.ledger.integration.test.ts
  • packages/3-targets/6-adapters/sqlite/src/core/adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/marker-ledger-writes.ts
  • packages/3-targets/6-adapters/sqlite/test/marker-ledger-writes.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.errors.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/runner.ledger.test.ts
  • test/integration/test/cli.db-verify.aggregate-schema.test.ts
  • test/integration/test/cli.db-verify.e2e.test.ts
  • test/integration/test/family.sign-database.test.ts
  • test/integration/test/family.verify-database.basic.test.ts
  • test/integration/test/family.verify-database.errors.test.ts
💤 Files with no reviewable changes (6)
  • packages/2-sql/5-runtime/src/marker.ts
  • packages/3-targets/3-targets/sqlite/src/exports/statement-builders.ts
  • packages/3-targets/3-targets/postgres/src/exports/statement-builders.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/statement-builders.ts
  • packages/2-sql/5-runtime/src/sql-marker.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts

Comment thread packages/2-sql/4-lanes/relational-core/src/ast/types.ts
Comment thread packages/2-sql/4-lanes/relational-core/src/ast/types.ts Outdated
Comment thread packages/2-sql/5-runtime/README.md Outdated
Comment thread packages/2-sql/9-family/src/core/control-instance.ts
Align AnyInsertValue with InsertValue (RawExpr), enforce TableSource
schema/namespaceId exclusivity, refresh sql-runtime README, and route
sign() through insert-only insertMarker so concurrent first stamps cannot
overwrite via the migration-runner upsert path.

Signed-off-by: Will Madden <madden@prisma.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/3-targets/6-adapters/postgres/test/marker-ledger-writes.test.ts (1)

26-43: ⚡ Quick win

Verify the invariants parameter default.

The test checks the first three params but doesn't verify params[7] (the invariants parameter). Since the initMarker test verifies invariants when provided (line 66), this test should verify that invariants defaults to an empty array when omitted.

🧪 Suggested assertion to add
     expect(params[0]).toBe('app');
     expect(params[1]).toBe('sha256:core');
     expect(params[2]).toBe('sha256:prof');
+    expect(params[7]).toEqual([]);
   });
🤖 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/test/marker-ledger-writes.test.ts`
around lines 26 - 43, Add an assertion to verify that when calling
adapter.insertMarker (in the test "insertMarker lowers to a plain insert with
DB-side updated_at") the invariants parameter defaults to an empty array: after
extracting { sql, params } = driver.calls[0]!, assert that params[7] is an empty
array (e.g., []). This ensures the insertMarker invocation without provided
invariants behaves the same as the initMarker test that verifies provided
invariants.
packages/3-targets/6-adapters/sqlite/src/core/marker-ledger-writes.ts (1)

170-172: ⚡ Quick win

Prefer ifDefined for conditional spreads.

The inline ternary spread ...(mergedInvariants !== undefined ? { invariants: ... } : {}) can be replaced with ifDefined('invariants', mergedInvariants) for consistency with repo patterns.

♻️ Refactor to use ifDefined
+import { ifDefined } from '`@prisma-next/utils/defined`';

       core_hash: param(destination.storageHash, { codecId: SQLITE_TEXT_CODEC_ID }),
       profile_hash: param(destination.profileHash, { codecId: SQLITE_TEXT_CODEC_ID }),
       updated_at: NOW,
-      ...(mergedInvariants !== undefined
-        ? { invariants: param(mergedInvariants, { codecId: SQLITE_JSON_CODEC_ID }) }
-        : {}),
+      ...ifDefined(
+        'invariants',
+        mergedInvariants !== undefined
+          ? param(mergedInvariants, { codecId: SQLITE_JSON_CODEC_ID })
+          : undefined
+      ),
     },

Based on learnings: In prisma/prisma-next code, prefer ifDefined from prisma-next/utils/defined for conditional object spreads instead of inline ternary-based spreads (e.g., when building option objects for ParamRef.of or similar codec/param construction).

🤖 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/sqlite/src/core/marker-ledger-writes.ts` around
lines 170 - 172, Replace the inline ternary-based conditional spread that adds
an invariants param with the repository pattern using ifDefined: when building
the object passed to ParamRef.of (where you currently have ...(mergedInvariants
!== undefined ? { invariants: param(mergedInvariants, { codecId:
SQLITE_JSON_CODEC_ID }) } : {})), import and call ifDefined('invariants',
param(mergedInvariants, { codecId: SQLITE_JSON_CODEC_ID })) so the invariants
property is only added when mergedInvariants is defined; keep the existing
param(...) and SQLITE_JSON_CODEC_ID usage intact and remove the ternary/spread
expression.
🤖 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.

Nitpick comments:
In `@packages/3-targets/6-adapters/postgres/test/marker-ledger-writes.test.ts`:
- Around line 26-43: Add an assertion to verify that when calling
adapter.insertMarker (in the test "insertMarker lowers to a plain insert with
DB-side updated_at") the invariants parameter defaults to an empty array: after
extracting { sql, params } = driver.calls[0]!, assert that params[7] is an empty
array (e.g., []). This ensures the insertMarker invocation without provided
invariants behaves the same as the initMarker test that verifies provided
invariants.

In `@packages/3-targets/6-adapters/sqlite/src/core/marker-ledger-writes.ts`:
- Around line 170-172: Replace the inline ternary-based conditional spread that
adds an invariants param with the repository pattern using ifDefined: when
building the object passed to ParamRef.of (where you currently have
...(mergedInvariants !== undefined ? { invariants: param(mergedInvariants, {
codecId: SQLITE_JSON_CODEC_ID }) } : {})), import and call
ifDefined('invariants', param(mergedInvariants, { codecId: SQLITE_JSON_CODEC_ID
})) so the invariants property is only added when mergedInvariants is defined;
keep the existing param(...) and SQLITE_JSON_CODEC_ID usage intact and remove
the ternary/spread expression.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: a4db194f-f351-4ebf-8cc2-f0b62fc22863

📥 Commits

Reviewing files that changed from the base of the PR and between 04f1be4 and 5da812a.

📒 Files selected for processing (10)
  • packages/2-sql/4-lanes/relational-core/src/ast/types.ts
  • packages/2-sql/5-runtime/README.md
  • packages/2-sql/9-family/src/core/control-adapter.ts
  • packages/2-sql/9-family/src/core/control-instance.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/marker-ledger-writes.ts
  • packages/3-targets/6-adapters/postgres/test/marker-ledger-writes.test.ts
  • packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/marker-ledger-writes.ts
  • packages/3-targets/6-adapters/sqlite/test/marker-ledger-writes.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/3-targets/6-adapters/postgres/src/core/marker-ledger-writes.ts
  • packages/2-sql/5-runtime/README.md
  • packages/2-sql/4-lanes/relational-core/src/ast/types.ts
  • packages/3-targets/6-adapters/sqlite/test/marker-ledger-writes.test.ts
  • packages/2-sql/9-family/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts

Comment on lines +327 to +336
/**
* Literal storage schema this table lives in, used by control-plane DML
* against fixed tables (e.g. `prisma_contract.marker`) that have no contract
* namespace coordinate. When set, the Postgres renderer qualifies the table
* as `schema.name` (each part quoted independently); SQLite asserts it is
* absent, since its control tables are unqualified. `schema` and
* `namespaceId` are mutually exclusive — `namespaceId` is the contract-bound
* coordinate, `schema` the contract-free literal.
*/
readonly schema: string | undefined;
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.

This is invalid here. This is generic SQL core, Postgres-specific AST additions should be modeled as AST node subclasses, not shoved into the generic shared base like this. You even added a comment explaining the layering violation.

* control-plane tables that have no contract namespace coordinate
* (`prisma_contract.marker`, `_prisma_ledger`).
*/
export function tableRef(name: string, options?: { readonly schema?: string }): TableSource {
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.

Again, it's not valid to have schema at this layer

@@ -1,4 +1,5 @@
import type { ContractMarkerRecord } from '@prisma-next/contract/types';
import type { MarkerReadResult, MarkerStatement } from '@prisma-next/sql-relational-core/ast';
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.

What is a MarkerStatement?

wmadden added 5 commits June 3, 2026 16:21
…d for corrective D5+D6 (TML-2753)

Operator caught 3 architectural mistakes in PR #712 that escaped the build loop (implementer + subagent reviewer both SATISFIED):

1. TableSource.schema? added to generic SQL core (Postgres-specific field on shared base); implementer added a code comment acknowledging the layering violation and shipped.

2. Read path is a leaky template-method orchestrator in family-sql/verify.ts (MarkerStatement / MarkerReadShape / readMarkerResult) that takes adapter implementation-detail fragments via an interface, asymmetric with the write SPI we got right in the same slice.

3. sign()+upsert race: collapsing marker write to INSERT…ON CONFLICT DO UPDATE is correct for the migration runner but wrong for sign() (silently overwrites another signer). CodeRabbit caught; fixed at 5da812a via new insertMarker primitive.

Retro landings (per drive-run-retro):

- projects/migrate-marker-ledger-to-typed-query-ast-commands/retros.md: new retro entry.

- drive/calibration/failure-modes.md: F16 (self-acknowledged layering violation), F17 (brief frames win as mechanics), F18 (inverted abstraction over adapter fragments), F19 (single-primitive collapse changes semantics for some callers).

- drive/calibration/dor.md: 3 new Dispatch-DoR overlay items (property statement alongside mechanic; per-caller contract enumeration; generic trace-callers reviewer prompt).

- drive/code/README.md: 3 new repo-specific code-review smells.

- .agents/rules/no-target-branches.mdc: new section "AST class fields are the same violation" with worked PostgresTableSource example.

Spec revised with corrective scope section + 3 new done conditions. Plan revised with D5 (revert TableSource.schema?, introduce PostgresTableSource) + D6 (delete MarkerStatement/Shape/Queryable/readMarkerResult; each adapter owns readMarker end-to-end). Both land on the same branch; PR #712 stays open.

Signed-off-by: Will Madden <madden@prisma.io>
…ma? from generic core (TML-2753)

Signed-off-by: Will Madden <madden@prisma.io>
…footgun, TML-2753)

D5 relocated `freeze()` from the `TableSource` constructor into the `named()` static factory so `PostgresTableSource` can assign `schema` after `super()`. All current production callers go through `named()` (which freezes) or construct a subclass (which freezes itself) — but the public constructor remained, leaving unfrozen instances reachable by any future `new TableSource(...)` caller.

Making the constructor `protected` closes the footgun structurally — the only callers that need direct construction are `TableSource.named()` itself (inside the class) and `PostgresTableSource` (via `super()`); TS now rejects any external `new TableSource(...)`. The same invariant the freeze-in-constructor pattern gave us, hoisted into the type system rather than relying on runtime audit.

D5 R1 reviewer flagged this as F02 (low, recommend tag-along). No runtime change; no semantic change for existing callers; no test changes needed.

Signed-off-by: Will Madden <madden@prisma.io>
…ment/MarkerReadShape/readMarkerResult (TML-2753)

Move marker read orchestration into per-adapter marker-read.ts (probe → select → decode → parse → tag) so the family layer only shares parseContractMarkerRow. Removes the inverted MarkerReadShape/readMarkerResult abstraction from family-sql verify.

Signed-off-by: Will Madden <madden@prisma.io>
…3 close, TML-2753)

F03 close from D6 R1. readContractMarker had zero production callers — the
adapter-owned readMarker SPI is the canonical read path post-D6.
MarkerStatement was shape-identical to SqlStatement with only this one dead
consumer.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
Copy link
Copy Markdown
Contributor Author

Corrective scope landed — re-requesting review

Following the operator's unsatisfactory; replan/respec direction, the slice was driven through a retro + three corrective dispatches (D5/D6/D7) plus an F02 close-the-footgun. All three architectural mistakes raised in review are now resolved.

The three findings + their fixes

  1. TableSource.schema? was a layering violation (Postgres-specific field on generic-core class).

    • D5 2aaae5677 — reverted schema? from generic TableSource; introduced PostgresTableSource extends TableSource in @prisma-next/target-postgres with a sibling contract-free pgTableRef factory. Mirrors Slice 1's PostgresCreateTable pattern. Renderer brand-checks instanceof PostgresTableSource; generic core stays target-agnostic. Lowered SQL byte-identical.
    • F02 close 689ac151eTableSource constructor made protected so the freeze invariant (relocated from ctor into named() to enable subclass field-assignment after super()) is hoisted into the type system: external new TableSource(...) is now a compile error.
  2. Read path was a leaky/inverted abstraction (MarkerStatement / MarkerReadShape / MarkerReadQueryable / readMarkerResult in family-sql/verify.ts fragmented adapter internals through a shared template-method orchestrator).

    • D6 a36b1e3fe — deleted all four shared symbols from verify.ts. Each adapter now owns readMarker(driver, space) end-to-end inside its own package: PG's flow lives in adapter-postgres/src/core/marker-read.ts (33 LOC), SQLite's in adapter-sqlite/src/core/marker-read.ts (49 LOC). The only shared piece is parseContractMarkerRow (a pure row→record parser). The read SPI is now structurally symmetric with the write SPI (initMarker / updateMarker / writeLedgerEntry).
    • D7 e3c4c4470 (F03 close, ride-along caught by the D6 reviewer) — deleted the residual legacy readContractMarker helper in sql-runtime/src/sql-marker.ts (zero production callers; it predated the adapter-owned shape) and the now-orphan MarkerStatement interface (shape-identical to the local SqlStatement). Net: MarkerStatement no longer exists in the codebase. This residual was D6's brief being scoped too tightly — my fault as orchestrator, caught by the reviewer in the same round.
  3. sign() + upsert initMarker race (CodeRabbit-flagged; reviewers initially missed).

    • Already resolved at 5da812ac0 by introducing insertMarker (insert-only, fail-loudly on duplicate) so sign()'s contract is preserved; the migration runner's idempotent re-apply keeps using upsert initMarker. Retro lesson F19 added to drive/calibration/failure-modes.md.

Validation

  • pnpm build, pnpm typecheck, pnpm fixtures:check (byte-identical — the corrections are structural), pnpm lint:deps, biome — all green across D5/F02/D6/D7.
  • Per-package tests green: relational-core, family-sql, adapter-postgres (560), adapter-sqlite, sql-runtime, target-postgres, target-sqlite.
  • Symmetry check: adapter.readMarker(driver, space)adapter.initMarker(driver, space, dest) in shape across both PG/SQLite and the family layer just calls and forgets.
  • Grep clean: MarkerStatement, MarkerReadShape, MarkerReadQueryable, readMarkerResult, readContractMarker — all zero matches in production source. (One stale readMarkerStatement() substring remains in adapter-sqlite/README.md:25 as pre-existing doc drift, not a symbol — see carry-over below.)

Drive lessons landed durably

  • projects/migrate-marker-ledger-to-typed-query-ast-commands/retros.md — written at retro time
  • drive/calibration/failure-modes.md — F16 (self-acknowledged layering violation shipped), F17 (brief frames the win as mechanics), F18 (inverted abstraction over adapter fragments), F19 (single-primitive collapse changes semantics for some callers)
  • drive/calibration/dor.md — three new Dispatch-DoR overlay items (property statement alongside mechanic; per-caller contract enumeration on primitive-collapse; generic 'trace each public-API change through callers' reviewer prompt)
  • drive/code/README.md — three new repo-specific smells
  • .agents/rules/no-target-branches.mdc — new section "AST class fields are the same violation" with worked PostgresTableSource example

Optional carry-overs (operator's call — happy to land on this branch if you want)

  • Stale readMarkerStatement() substring in adapter-sqlite/README.md:25 — pre-existing doc drift, references a symbol that doesn't exist in code. Both D6 and D7 reviewers surfaced it but it's outside the slice's scope. Tiny doc tidy.
  • PostgresTableSource.rewrite() override in D5 is identical to the inherited base — harmless redundancy; D5 reviewer noted as informational only.

If you want either folded in before merge, say the word and I'll dispatch.

Process note

Two orchestrator-level calibration misses surfaced this round and are being recorded:

  • The D6 brief excluded sql-marker.ts from the closed file list on a wrong assumption — produced the F03 residual. Lesson: when deleting a shape interface (e.g. MarkerStatement), the brief must enumerate every consumer, not assume the cluster boundary.
  • I drifted into in-place implementation of the F03 fix instead of dispatching — caught and reverted; F03 was then dispatched properly to D7. Lesson going into failure-modes.md as F20 (orchestrator role drift on 'small' fixes).

Re-requesting review.

…tro followup)

Caught in this slice when the orchestrator started editing in-place to close F03 (a residual MarkerStatement consumer that the D6 brief had wrongly excluded), instead of dispatching an implementer. Operator interrupted with the role-reminder; edits were reverted and F03 was re-dispatched cleanly as D7 (composer-2.5-fast), landing at e3c4c44 with a proper closed-file-list brief.

F20 records the symptom, the self-justifications that license the drift, the detection signals (no implementer dispatch transcript preceding an orchestrator-authored production commit), the mitigation (the split is a role constraint, not an efficiency tradeoff; calibration docs are inside the role, production source is not), and the revert-before-redispatch protocol when caught in-flight.

Signed-off-by: Will Madden <madden@prisma.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (2)
drive/calibration/failure-modes.md (1)

285-399: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Duplicate failure-mode numbering creates ambiguous references.

### F15 appears twice (first at Line 267, then again at Line 285), so anchors and cross-references to “F15” are ambiguous. Please renumber the second occurrence and its subsequent entries to keep the catalogue strictly monotonic.
As per coding guidelines: “**/*.md: Keep docs current (READMEs, rules, links).”

🤖 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 `@drive/calibration/failure-modes.md` around lines 285 - 399, The file has a
duplicated section heading "### F15" (the Behavioural reports-all entry starting
at the second appearance) which makes references ambiguous; renumber that second
"### F15" and every subsequent "### F16"→"### F17", "### F17"→"### F18", etc.,
incrementing each failure-mode heading after the duplicate so the catalogue
numbers are strictly monotonic and update any internal cross-references that
mention those symbols to match the new numbers.
packages/3-targets/6-adapters/sqlite/src/core/adapter.ts (1)

208-236: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject Postgres schema-qualified table sources in the SQLite renderer.

PostgresTableSource still flows through this path because it inherits kind === 'table-source'. After removing the old schema check here, passing a pgTableRef({ schema: 'x', name: 'y' }) into SQLite now lowers to just "y" when namespaceId is unset, so the adapter silently drops the Postgres-only coordinate instead of failing fast.

Suggested fix
+function hasSchemaCoordinate(
+  table: Pick<TableSource, 'name' | 'namespaceId'>,
+): table is Pick<TableSource, 'name' | 'namespaceId'> & { readonly schema: string | undefined } {
+  return 'schema' in table;
+}
+
 function qualifyTableFromNamespaceCoordinate(
   table: Pick<TableSource, 'name' | 'namespaceId'>,
   contract: SqliteContract,
 ): string {
+  if (hasSchemaCoordinate(table) && table.schema !== undefined) {
+    throw new Error('SQLite renderer does not support schema-qualified table sources');
+  }
   if (table.namespaceId === undefined) {
     return quoteIdentifier(table.name);
   }
🤖 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/sqlite/src/core/adapter.ts` around lines 208 -
236, The SQLite renderer currently accepts Postgres schema-qualified table
sources (e.g. pgTableRef with a schema) because PostgresTableSource shares kind
=== 'table-source', causing qualifyTableFromNamespaceCoordinate /
renderTableSource to silently drop the schema; update the renderer to detect and
reject Postgres-style schema-qualified sources and throw a clear error instead.
Specifically, in renderTableSource (or at the start of
qualifyTableFromNamespaceCoordinate) add a check for PostgresTableSource (e.g. a
schema property or the Postgres-specific discriminant on the source) and if a
schema is present call throw new Error("Postgres schema-qualified table sources
are not supported by the SQLite renderer") (mentioning pgTableRef or
PostgresTableSource in the message), otherwise proceed with the existing logic
that uses qualifyTableFromNamespaceCoordinate and quoteIdentifier. Ensure the
new check runs before returning the unqualified table name so schema-qualified
Postgres references fail fast.
🧹 Nitpick comments (1)
packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts (1)

343-365: ⚡ Quick win

Clarify currentInvariants handling in Postgres updateMarker

markerLedgerWrites.updateMarker omits invariants from the UPDATE SET clause when destination.invariants === undefined (it sets mergedInvariants to undefined and only adds { invariants: ... } when mergedInvariants !== undefined). So passing [] for currentInvariants can’t wipe existing invariants.

Optional: add a short comment near the currentInvariants calculation in control-adapter.ts noting that currentInvariants is only used when destination.invariants is provided.

🤖 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 343 - 365, The current logic in updateMarker computes currentInvariants as
[] when destination.invariants is undefined which prevents
markerLedgerWrites.updateMarker from clearing existing invariants; change the
computation so currentInvariants is undefined when destination.invariants ===
undefined (i.e., only call await this.readMarker(...) and supply invariants when
destination.invariants is provided), and add a short inline comment by the
currentInvariants calculation in control-adapter.ts noting that
currentInvariants is only used when destination.invariants is present and must
be undefined to allow the downstream update to remove invariants.
🤖 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 `@drive/code/README.md`:
- Around line 13-20: Update the README links that point to the mirror path
".cursor/rules/as-contract-cast-smell.mdc" to use the canonical
".agents/rules/as-contract-cast-smell.mdc" path instead; locate the reference in
the paragraph that starts with "`as Contract` cast bypasses the family
`ContractSerializer` seam" and replace the .cursor link target with the
.agents/rules equivalent so the documentation follows the repo policy about
canonical rule locations.

---

Outside diff comments:
In `@drive/calibration/failure-modes.md`:
- Around line 285-399: The file has a duplicated section heading "### F15" (the
Behavioural reports-all entry starting at the second appearance) which makes
references ambiguous; renumber that second "### F15" and every subsequent "###
F16"→"### F17", "### F17"→"### F18", etc., incrementing each failure-mode
heading after the duplicate so the catalogue numbers are strictly monotonic and
update any internal cross-references that mention those symbols to match the new
numbers.

In `@packages/3-targets/6-adapters/sqlite/src/core/adapter.ts`:
- Around line 208-236: The SQLite renderer currently accepts Postgres
schema-qualified table sources (e.g. pgTableRef with a schema) because
PostgresTableSource shares kind === 'table-source', causing
qualifyTableFromNamespaceCoordinate / renderTableSource to silently drop the
schema; update the renderer to detect and reject Postgres-style schema-qualified
sources and throw a clear error instead. Specifically, in renderTableSource (or
at the start of qualifyTableFromNamespaceCoordinate) add a check for
PostgresTableSource (e.g. a schema property or the Postgres-specific
discriminant on the source) and if a schema is present call throw new
Error("Postgres schema-qualified table sources are not supported by the SQLite
renderer") (mentioning pgTableRef or PostgresTableSource in the message),
otherwise proceed with the existing logic that uses
qualifyTableFromNamespaceCoordinate and quoteIdentifier. Ensure the new check
runs before returning the unqualified table name so schema-qualified Postgres
references fail fast.

---

Nitpick comments:
In `@packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts`:
- Around line 343-365: The current logic in updateMarker computes
currentInvariants as [] when destination.invariants is undefined which prevents
markerLedgerWrites.updateMarker from clearing existing invariants; change the
computation so currentInvariants is undefined when destination.invariants ===
undefined (i.e., only call await this.readMarker(...) and supply invariants when
destination.invariants is provided), and add a short inline comment by the
currentInvariants calculation in control-adapter.ts noting that
currentInvariants is only used when destination.invariants is present and must
be undefined to allow the downstream update to remove invariants.
🪄 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: 1057064f-332b-4907-b60f-c03b333ce5b2

📥 Commits

Reviewing files that changed from the base of the PR and between 5da812a and de2aab4.

⛔ Files ignored due to path filters (3)
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/retros.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/plan.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/spec.md is excluded by !projects/**
📒 Files selected for processing (23)
  • .agents/rules/no-target-branches.mdc
  • drive/calibration/dor.md
  • drive/calibration/failure-modes.md
  • drive/code/README.md
  • packages/2-sql/4-lanes/relational-core/src/ast/adapter-types.ts
  • packages/2-sql/4-lanes/relational-core/src/ast/types.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/dml.ts
  • packages/2-sql/5-runtime/README.md
  • packages/2-sql/5-runtime/src/exports/index.ts
  • packages/2-sql/5-runtime/src/sql-marker.ts
  • packages/2-sql/5-runtime/test/sql-marker.test.ts
  • packages/3-targets/3-targets/postgres/src/contract-free/dml.ts
  • packages/3-targets/3-targets/postgres/src/core/ast/table-source.ts
  • packages/3-targets/3-targets/postgres/src/exports/contract-free.ts
  • packages/3-targets/3-targets/postgres/test/contract-free/dml.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/marker-ledger-writes.ts
  • packages/3-targets/6-adapters/postgres/src/core/marker-read.ts
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
  • packages/3-targets/6-adapters/sqlite/src/core/adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/marker-read.ts
💤 Files with no reviewable changes (3)
  • packages/2-sql/4-lanes/relational-core/src/ast/adapter-types.ts
  • packages/2-sql/5-runtime/test/sql-marker.test.ts
  • packages/2-sql/5-runtime/src/sql-marker.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/3-targets/3-targets/postgres/test/contract-free/dml.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/3-targets/6-adapters/postgres/src/core/sql-renderer.ts
  • packages/2-sql/5-runtime/src/exports/index.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/dml.ts
  • packages/3-targets/6-adapters/postgres/src/core/marker-ledger-writes.ts

Comment thread drive/code/README.md
Comment on lines 13 to +20
- **`as Contract` cast bypasses the family `ContractSerializer` seam.** Any `JSON.parse(...) as Contract` (or `as Contract<…>`) in `packages/**/src/**` outside the allowlist is a serializer-bypass smell — see [`.cursor/rules/as-contract-cast-smell.mdc`](../../.cursor/rules/as-contract-cast-smell.mdc). The replacement idiom is `familyInstance.deserializeContract(JSON.parse(raw) as unknown)`. Originally surfaced by `TML-2536`.

- **Self-acknowledged layering violation in source.** A code comment that explicitly admits to a known anti-pattern (`layering violation`, `branch on target`, `leaky abstraction`, `bypasses the seam`, `we shouldn't do this but`, `temporary`, `TODO: this is wrong`) is *itself* the must-fix finding — flag it as `must-fix` and stop reviewing the surrounding diff until the operator confirms whether the shape is correct. Reviewers should grep the diff for these phrases as part of every review pass. The implementer's acknowledgment is the symptom; the underlying structural problem is the actual finding. See [F16 in `drive/calibration/failure-modes.md`](../calibration/failure-modes.md#f16-self-acknowledged-layering-violation-shipped-through-review). Originally surfaced by `TML-2753`.

- **Inverted abstraction: shared template-method orchestrator over adapter fragments.** A type named `<Operation>Shape` / `<Operation>Statements` / `<Operation>Spec` in `family-sql` (or any shared layer) carrying SQL fragments + row decoders populated by each adapter, plus a function that takes a queryable + that shape and runs the operation — that's the adapter exporting its implementation details upstream instead of owning the operation. The adapter exists to hide those details. Right shape: each adapter owns `<operation>(driver, args)` end-to-end; only *pure* helpers (parsers, row-shape schemas) are shared. 10–20 lines of "duplicated" orchestration between two adapters is the right kind of duplication — it's what the adapter pattern is for. Symmetry check: if the *write* side or the sibling *Mongo* family owns the operation end-to-end, the read side or SQL side must too. See [F18 in `drive/calibration/failure-modes.md`](../calibration/failure-modes.md#f18-inverted-abstraction-shared-orchestrator-in-family-layer-takes-adapter-implementation-detail-fragments-via-an-interface). Originally surfaced by `TML-2753`.

- **Single-primitive collapse without per-caller contract trace.** A diff that collapses two distinct operations into one primitive (`initMarker` becomes upsert, replacing separate insert + update; `merge` replaces separate overwrite + accumulate) must explicitly enumerate every caller of either pre-collapse operation and confirm the post-collapse semantic still satisfies each caller's contract. Look for: caller A's tests pinning idempotent-re-apply (passes under upsert), caller B's tests pinning fail-loudly-on-duplicate (might still pass sequentially while failing under concurrency). The mitigation is either keeping both operations or adding an explicit variant (e.g. `insertMarker` alongside upsert-`initMarker`). See [F19 in `drive/calibration/failure-modes.md`](../calibration/failure-modes.md#f19-single-primitive-collapse-changes-semantics-for-some-callers-but-not-others). Originally surfaced by `TML-2753`.

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Link rule references to canonical .agents/rules paths, not mirror paths.

This section links to .cursor/rules/as-contract-cast-smell.mdc; per repo policy, .agents/rules/<rule>.mdc is the canonical home and mirror paths are presentation-only. Please switch the link target to the .agents/rules path.
As per coding guidelines: “Rules canonical home lives in .agents/rules/<rule-name>.mdc with symlink presentation mirrors at .cursor/rules/ and .claude/rules/.”

🤖 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 `@drive/code/README.md` around lines 13 - 20, Update the README links that
point to the mirror path ".cursor/rules/as-contract-cast-smell.mdc" to use the
canonical ".agents/rules/as-contract-cast-smell.mdc" path instead; locate the
reference in the paragraph that starts with "`as Contract` cast bypasses the
family `ContractSerializer` seam" and replace the .cursor link target with the
.agents/rules equivalent so the documentation follows the repo policy about
canonical rule locations.

Comment thread drive/calibration/dor.md Outdated
- [ ] If touching `packages/0-shared` or `packages/1-framework-core`, downstream package builds named as "done when" gates.
- [ ] If the dispatch adds a new public type, the dependent packages' typecheck is named.
- [ ] Brief's "Outcome" section includes a *property statement* alongside the mechanical instruction (*"such that <invariant the change preserves>"*). If the brief authorises adding a field / collapsing operations / consolidating implementations, it must also name the architectural property the change preserves (target-agnostic core, adapter ownership of an operation, caller's contract semantics, etc.) — not just the mechanic. See [F17 in `failure-modes.md`](./failure-modes.md#f17-dispatch-brief-frames-the-win-as-mechanics-implementer--reviewer-ship-wrong-shape-work-that-satisfies-it). (Added by `TML-2753` retro.)
- [ ] If the dispatch **collapses two distinct operations into one primitive**, brief enumerates every caller of either pre-collapse operation and the contract each caller depends on (idempotent / fail-loudly / CAS / etc.). The dispatch closes only when every contract is satisfied; otherwise the brief keeps both operations or adds a deliberate variant. See [F19 in `failure-modes.md`](./failure-modes.md#f19-single-primitive-collapse-changes-semantics-for-some-callers-but-not-others). (Added by `TML-2753` retro.)
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.

This seems over-prescriptive. Our Drive principles say we don't enumerate all files a dispatch will touch. We should be able to assert that the implementer must remove all references of the collapsed operations instead.

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.

This is incorrectly named "control". This doesn't rely on the contract but it can be in theory utilized in either plane

Comment on lines +56 to +57
import * as markerLedgerWrites from './marker-ledger-writes';
import * as markerRead from './marker-read';
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.

Why are these in separate files?

Comment on lines +48 to +51
/** A single-row `INSERT INTO <table> (…) VALUES (…)`. */
export function insert(table: TableSource, row: Readonly<Record<string, InsertValue>>): InsertAst {
return InsertAst.into(table).withRows([row]);
}
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.

Wtf is the point of this insert() function? It just hides the query builder this module is supposed to be providing

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.

This code is unreadable crap. Why isn't it using the query builder we just created

Comment on lines +15 to +32
const exists = await driver.query(
'select 1 from information_schema.tables where table_schema = $1 and table_name = $2',
['prisma_contract', 'marker'],
);
if (exists.rows.length === 0) {
return { kind: 'no-table' };
}

const result = await driver.query(
'select core_hash, profile_hash, contract_json, canonical_version, updated_at, app_tag, meta, invariants from prisma_contract.marker where space = $1',
[space],
);
const row = result.rows[0];
if (!row) {
return { kind: 'absent' };
}

return { kind: 'present', record: parseContractMarkerRow(row) };
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.

WTF is this? Why isn't this using the query builder we just wrote?:

wmadden added 2 commits June 3, 2026 17:59
…ive D8/D9/D10 (TML-2753)

After D5/D6/D7 closed SATISFIED, operator review surfaced a deeper architectural miss: the contract-free authoring surface (relational-core/src/contract-free/dml.ts) is not actually a query builder. Current insert(table, row) / update({...}) / upsert({...}) helpers are option-bag wrappers around the AST class chain that already exists; marker writes that use them thread codec IDs / table names / column refs at every leaf; marker reads use raw SQL strings. Net: the slice migrated marker/ledger to typed AST literally (the AST is used) but not in spirit (the authoring is unreadable).

Root cause: D1 brief framed the win as "use the typed AST" instead of "deliver a fluent authoring surface analogous to sql-builder in spirit". composer-2.5-fast pattern-cloned Slice 1 atom-constructors (col/lit/fn, correct for DDL) into DML (which needs a chainable builder). F17 property-statement discipline did not catch this because the property statement named the transport, not the ergonomics.

Landed: F19 DoR overlay rewritten property-based per operator dor.md:45 critique (no more file enumeration; assert "no reference to pre-collapse operations survives" instead); F21 added to failure-modes (implementer ships AST construction by hand wrapped in option-bag factories instead of building the fluent surface); retros.md round-2 entry; slice spec + plan revised with D8/D9/D10 corrective scope and 5 new done conditions; implementer slot for D8 onwards upgraded from composer-2.5-fast to claude-4.6-sonnet-high-thinking.
Signed-off-by: Will Madden <madden@prisma.io>
…ML (D8, TML-2753)

Replace the option-bag factory functions in relational-core/contract-free/dml.ts with a
genuine fluent authoring surface. The old exports (insert/update/upsert/tableRef/
excludedColumn/dbExpr) are kept as @deprecated re-exports so the build stays green
during D8; D9 will delete the call sites and remove them.

Design decisions
----------------

**Chain shape.** Each operation starts from a TableHandle and chains immutably:
  marker.update()
    .set({ core_hash: newHash })
    .where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom)))
    .returning(marker.space)
    .build()

.set/.where/.returning all return new builder instances. .build() produces the existing
InsertAst/UpdateAst/SelectAst node the lower pipeline already consumes.

**Column proxies.** ColumnProxy objects are exposed as keyed properties of TableHandle
(marker.space, marker.core_hash). Each proxy carries its codecId and nullable flag
baked in at declaration time. Expression methods (.eq, .neq, .isNull, .isNotNull)
produce CfExpr — a thin composable wrapper over AnyExpression with .and/.or/.not —
without any per-call-site codec or column-name threading.

**Value encoding.** Plain JS values passed to .set() or .insert() are automatically
wrapped in ParamRef with the column's known codec. Existing AnyExpression values
(ColumnRef, RawExpr) are detected via a toExpr() type guard and passed through without
re-wrapping.

**Table declaration.** The generic table(source, schema) factory in relational-core
accepts a TableSource and a ColumnSchema (Record<string, ColumnDescriptor>). Target
packages (postgres, sqlite) provide dialect-specific column-type helpers (text(),
int4(), timestamptz() etc.) that return ColumnDescriptor objects with the correct
codec ID, plus pgTable()/sqliteTable() wrappers that supply the right TableSource
subclass.

**ExcludedProxy.** The .onConflict().doUpdate(callback) overload passes an
ExcludedProxy<Schema> to the callback, giving type-safe access to excluded.<column>
ColumnRefs for DO UPDATE SET clauses.

**Spirit vs literal alignment.** sql-builder uses a JS Proxy for table resolution
because the table set is only known at contract-bind time. Here the table is declared
once as a module-level const; a plain object spread with a blindCast at the return
boundary is correct by construction, no Proxy needed.

What survived from old dml.ts
-------------------------------
param() — re-exported unchanged as a non-deprecated primitive.
tableRef/excludedColumn/dbExpr/insert/upsert/update — re-exported with @deprecated
JSDoc; D9 removes their call sites.

Signed-off-by: Will Madden <madden@prisma.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/2-sql/4-lanes/relational-core/src/contract-free/table.ts`:
- Around line 255-256: The bug is that table() always passes source.name into
makeColumnProxy so aliased tables (e.g., pgTable(..., { alias })) produce
ColumnRef entries with the base table name; update the proxies creation in
table() (and the similar block around the 278-296 range) to use the source alias
when present (e.g., source.alias or similar property) instead of source.name
when calling makeColumnProxy so generated ColumnRef qualifiers reflect the
alias; locate the loop that assigns proxies[col] = makeColumnProxy(source.name,
col, desc) and change the first argument to the effective qualifier (alias if
defined, otherwise source.name) consistently wherever column proxies are
created.
- Around line 289-290: The eq and neq branches currently convert null into a
ParamRef via toSetExpression and produce BinaryExpr.eq/ref, which yields SQL
like "column = NULL" instead of proper IS NULL semantics; update the eq and neq
handlers in the table contract so that when value is null (or explicitly a
nullable sentinel) they return a CfExpr wrapping an IsNullExpression or
IsNotNullExpression (rather than BinaryExpr.eq/neq), otherwise keep the existing
new CfExpr(BinaryExpr.eq(ref, toSetExpression(...))) behaviour; reference the
eq, neq functions, CfExpr, BinaryExpr, ref and toSetExpression symbols when
making this change.

In `@packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts`:
- Around line 13-24: Remove the deprecated DML shims from this module's public
export list: delete dbExpr, excludedColumn, insert, tableRef, update, and upsert
from the exported identifiers in contract-free.ts so they are no longer
re-exported from this package; leave the other type exports (ExcludedProxy,
TableHandle, TableInsertRow, TableSetValues) and helpers (param, table) intact
and do not add new backwards-compat exports pointing at ../contract-free/dml.
🪄 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: 0d70b7f8-149f-4dfb-ad79-89ac76a5c189

📥 Commits

Reviewing files that changed from the base of the PR and between de2aab4 and 313c3e7.

⛔ Files ignored due to path filters (3)
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/retros.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/plan.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/spec.md is excluded by !projects/**
📒 Files selected for processing (12)
  • drive/calibration/dor.md
  • drive/calibration/failure-modes.md
  • packages/2-sql/4-lanes/relational-core/src/contract-free/dml.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/table.ts
  • packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts
  • packages/2-sql/4-lanes/relational-core/test/contract-free/table.test.ts
  • packages/3-targets/3-targets/postgres/src/contract-free/columns.ts
  • packages/3-targets/3-targets/postgres/src/exports/contract-free.ts
  • packages/3-targets/3-targets/postgres/test/contract-free/columns.test.ts
  • packages/3-targets/3-targets/sqlite/src/contract-free/columns.ts
  • packages/3-targets/3-targets/sqlite/src/exports/contract-free.ts
  • packages/3-targets/3-targets/sqlite/test/contract-free/columns.test.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/3-targets/3-targets/sqlite/src/exports/contract-free.ts
  • packages/3-targets/3-targets/sqlite/test/contract-free/columns.test.ts
  • drive/calibration/dor.md
  • drive/calibration/failure-modes.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/3-targets/3-targets/postgres/src/exports/contract-free.ts

Comment on lines +255 to +256
for (const [col, desc] of Object.entries(schema)) {
proxies[col] = makeColumnProxy(source.name, col, desc);
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve aliases when creating column proxies.

table() always feeds source.name into makeColumnProxy, so an aliased source still emits ColumnRefs against the base table name. This is reachable through pgTable(..., { alias }), and any WHERE / RETURNING built from those proxies will reference the wrong qualifier.

Suggested fix
 export function table<Schema extends ColumnSchema>(
   source: TableSource,
   schema: Schema,
 ): TableHandle<Schema> {
   const proxies: Record<string, ColumnProxy> = {};
+  const refTableName = source.alias ?? source.name;
   for (const [col, desc] of Object.entries(schema)) {
-    proxies[col] = makeColumnProxy(source.name, col, desc);
+    proxies[col] = makeColumnProxy(source.name, refTableName, col, desc);
   }
@@
 function makeColumnProxy(
   tableName: string,
+  refTableName: string,
   columnName: string,
   desc: ColumnDescriptor,
 ): ColumnProxy {
-  const ref = ColumnRef.of(tableName, columnName);
+  const ref = ColumnRef.of(refTableName, columnName);
   return {
     codecId: desc.codecId,
     nullable: desc.nullable,

Also applies to: 278-296

🤖 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/4-lanes/relational-core/src/contract-free/table.ts` around
lines 255 - 256, The bug is that table() always passes source.name into
makeColumnProxy so aliased tables (e.g., pgTable(..., { alias })) produce
ColumnRef entries with the base table name; update the proxies creation in
table() (and the similar block around the 278-296 range) to use the source alias
when present (e.g., source.alias or similar property) instead of source.name
when calling makeColumnProxy so generated ColumnRef qualifiers reflect the
alias; locate the loop that assigns proxies[col] = makeColumnProxy(source.name,
col, desc) and change the first argument to the effective qualifier (alias if
defined, otherwise source.name) consistently wherever column proxies are
created.

Comment on lines +289 to +290
eq: (value) => new CfExpr(BinaryExpr.eq(ref, toSetExpression(value, desc))),
neq: (value) => new CfExpr(BinaryExpr.neq(ref, toSetExpression(value, desc))),
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't lower eq(null) and neq(null) to binary operators.

These branches currently turn null into a ParamRef, so the AST becomes column = NULL / column <> NULL. In SQL that does not behave like IS NULL / IS NOT NULL, so nullable filters will quietly return the wrong rows.

Suggested fix
-    eq: (value) => new CfExpr(BinaryExpr.eq(ref, toSetExpression(value, desc))),
-    neq: (value) => new CfExpr(BinaryExpr.neq(ref, toSetExpression(value, desc))),
+    eq: (value) =>
+      value === null
+        ? new CfExpr(NullCheckExpr.isNull(ref))
+        : new CfExpr(BinaryExpr.eq(ref, toSetExpression(value, desc))),
+    neq: (value) =>
+      value === null
+        ? new CfExpr(NullCheckExpr.isNotNull(ref))
+        : new CfExpr(BinaryExpr.neq(ref, toSetExpression(value, desc))),
📝 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.

Suggested change
eq: (value) => new CfExpr(BinaryExpr.eq(ref, toSetExpression(value, desc))),
neq: (value) => new CfExpr(BinaryExpr.neq(ref, toSetExpression(value, desc))),
eq: (value) =>
value === null
? new CfExpr(NullCheckExpr.isNull(ref))
: new CfExpr(BinaryExpr.eq(ref, toSetExpression(value, desc))),
neq: (value) =>
value === null
? new CfExpr(NullCheckExpr.isNotNull(ref))
: new CfExpr(BinaryExpr.neq(ref, toSetExpression(value, desc))),
🤖 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/4-lanes/relational-core/src/contract-free/table.ts` around
lines 289 - 290, The eq and neq branches currently convert null into a ParamRef
via toSetExpression and produce BinaryExpr.eq/ref, which yields SQL like "column
= NULL" instead of proper IS NULL semantics; update the eq and neq handlers in
the table contract so that when value is null (or explicitly a nullable
sentinel) they return a CfExpr wrapping an IsNullExpression or
IsNotNullExpression (rather than BinaryExpr.eq/neq), otherwise keep the existing
new CfExpr(BinaryExpr.eq(ref, toSetExpression(...))) behaviour; reference the
eq, neq functions, CfExpr, BinaryExpr, ref and toSetExpression symbols when
making this change.

Comment thread packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts Outdated
…se files; delete deprecated shims (D9, TML-2753)

Call-site delta: all six marker/ledger functions (insertMarker, initMarker, updateMarker,
writeLedgerEntry, readMarker x2 — probe + row fetch) are now expressed entirely through the
D8 pgTable()/sqliteTable() API. Every codec, table reference, and column reference lives at
the table declaration; leaves hold plain values and column-proxy expressions only. The
information_schema.tables and sqlite_master catalog probes are routed through the same
execute() helper as the DML, so there are zero raw driver.query() strings in the new files.

File collapse: marker-ledger-writes.ts + marker-read.ts merged into marker-ledger.ts per
adapter. The split was a round-1 dispatch boundary artefact, not a real seam.

Shim deletion: tableRef, excludedColumn, dbExpr, insert, upsert, update, param removed from
relational-core/src/contract-free/dml.ts and its contract-free export; pgTableRef and its
postgres/contract-free/dml.ts wrapper deleted. Nothing outside the files being rewritten
imported these symbols.

Signed-off-by: Will Madden <madden@prisma.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/3-targets/6-adapters/sqlite/src/core/marker-ledger.ts (1)

251-265: 💤 Low value

Replace bare as cast with utility function.

Line 255 uses a bare as cast which violates the coding guideline requiring blindCast<T, 'Reason'> or castAs<T> from @prisma-next/utils/casts for production code.

♻️ Suggested fix
+import { castAs } from '`@prisma-next/utils/casts`';
+
 export function decodeSqliteMarkerRow(row: unknown): unknown {
   if (typeof row !== 'object' || row === null || !('invariants' in row)) {
     return row;
   }
-  const record = row as { invariants: unknown };
+  const record = castAs<{ invariants: unknown }>(row);
   if (typeof record.invariants !== 'string') return row;

As per coding guidelines: "No bare as in production code. Use blindCast<T, 'Reason'> or castAs from @prisma-next/utils/casts."

🤖 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/sqlite/src/core/marker-ledger.ts` around lines
251 - 265, In decodeSqliteMarkerRow replace the bare TypeScript cast "as {
invariants: unknown }" with the project's casting utility: import and use either
blindCast<{ invariants: unknown }, 'DecodeSqliteMarkerRow'> or castAs<{
invariants: unknown }> from '`@prisma-next/utils/casts`' and apply it to the row
before accessing record.invariants so the cast follows the coding guideline and
preserves the same runtime behavior.
packages/3-targets/6-adapters/postgres/src/core/marker-ledger.ts (1)

105-127: ⚡ Quick win

Use table.columns.* for these contract-free column refs.

This file is reading columns directly off the table handle (infoSchemaTables.table_schema, marker.space, etc.) instead of table.columns.*. Please switch the pattern consistently here and in the later DML builders so it stays aligned with the contract-free API convention and avoids future table-property collisions.

Suggested change
   const probe = infoSchemaTables
-    .select(infoSchemaTables.table_schema)
+    .select(infoSchemaTables.columns.table_schema)
     .where(
-      infoSchemaTables.table_schema
+      infoSchemaTables.columns.table_schema
         .eq('prisma_contract')
-        .and(infoSchemaTables.table_name.eq('marker')),
+        .and(infoSchemaTables.columns.table_name.eq('marker')),
     )
     .build();
@@
   const fetch = marker
     .select(
-      marker.core_hash,
-      marker.profile_hash,
-      marker.contract_json,
-      marker.canonical_version,
-      marker.updated_at,
-      marker.app_tag,
-      marker.meta,
-      marker.invariants,
+      marker.columns.core_hash,
+      marker.columns.profile_hash,
+      marker.columns.contract_json,
+      marker.columns.canonical_version,
+      marker.columns.updated_at,
+      marker.columns.app_tag,
+      marker.columns.meta,
+      marker.columns.invariants,
     )
-    .where(marker.space.eq(space))
+    .where(marker.columns.space.eq(space))
     .build();

As per coding guidelines, "Column access: Use table.columns.fieldName to avoid conflicts with table properties."

🤖 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/marker-ledger.ts` around
lines 105 - 127, The SQL builders are accessing columns directly off table
handles (e.g., infoSchemaTables.table_schema, infoSchemaTables.table_name,
marker.space, marker.core_hash, marker.profile_hash, marker.contract_json,
marker.canonical_version, marker.updated_at, marker.app_tag, marker.meta,
marker.invariants); update all such references to use the contract-free API by
replacing them with the table.columns.* form (e.g.,
infoSchemaTables.columns.table_schema, marker.columns.space,
marker.columns.core_hash, etc.) throughout the probe, fetch and any later DML
builders so column access consistently uses table.columns.fieldName.
🤖 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.

Nitpick comments:
In `@packages/3-targets/6-adapters/postgres/src/core/marker-ledger.ts`:
- Around line 105-127: The SQL builders are accessing columns directly off table
handles (e.g., infoSchemaTables.table_schema, infoSchemaTables.table_name,
marker.space, marker.core_hash, marker.profile_hash, marker.contract_json,
marker.canonical_version, marker.updated_at, marker.app_tag, marker.meta,
marker.invariants); update all such references to use the contract-free API by
replacing them with the table.columns.* form (e.g.,
infoSchemaTables.columns.table_schema, marker.columns.space,
marker.columns.core_hash, etc.) throughout the probe, fetch and any later DML
builders so column access consistently uses table.columns.fieldName.

In `@packages/3-targets/6-adapters/sqlite/src/core/marker-ledger.ts`:
- Around line 251-265: In decodeSqliteMarkerRow replace the bare TypeScript cast
"as { invariants: unknown }" with the project's casting utility: import and use
either blindCast<{ invariants: unknown }, 'DecodeSqliteMarkerRow'> or castAs<{
invariants: unknown }> from '`@prisma-next/utils/casts`' and apply it to the row
before accessing record.invariants so the cast follows the coding guideline and
preserves the same runtime behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: d957d7f6-9620-466b-8914-07038c44d2c2

📥 Commits

Reviewing files that changed from the base of the PR and between 313c3e7 and 90fa434.

📒 Files selected for processing (12)
  • packages/2-sql/4-lanes/relational-core/src/contract-free/dml.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/table.ts
  • packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts
  • packages/3-targets/3-targets/postgres/src/exports/contract-free.ts
  • packages/3-targets/6-adapters/postgres/src/core/adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/marker-ledger.ts
  • packages/3-targets/6-adapters/postgres/test/adapter.test.ts
  • packages/3-targets/6-adapters/postgres/test/control-adapter.test.ts
  • packages/3-targets/6-adapters/sqlite/src/core/adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/marker-ledger.ts
💤 Files with no reviewable changes (3)
  • packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/dml.ts
  • packages/3-targets/3-targets/postgres/src/exports/contract-free.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/src/core/adapter.ts
  • packages/3-targets/6-adapters/sqlite/src/core/adapter.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/table.ts

wmadden added 3 commits June 3, 2026 20:11
The registry dispatches codecs purely from AST-supplied CodecRefs; it
has no contract dependency and is not specific to the control plane.
Naming it "control" leaked the intent of its first caller into the
registry's identity.

Rename to ast-codec-registry / createAstCodecRegistry to match the
adjacent ast-codec-resolver it wraps and to name the mechanism (AST
CodecRef dispatch) rather than the caller.

Files changed:
- packages/2-sql/5-runtime/src/codecs/control-codec-registry.ts
  → ast-codec-registry.ts  (git mv + symbol rename + JSDoc rewrite)
- packages/2-sql/5-runtime/src/exports/index.ts  (re-export updated)
- packages/3-targets/6-adapters/postgres/src/core/marker-ledger.ts
  (import + call site updated)
- packages/3-targets/6-adapters/sqlite/src/core/marker-ledger.ts
  (import + call site updated)
- packages/2-sql/5-runtime/README.md  (Exports section updated)

No behaviour change; same parameters, return type, and internal logic.

Signed-off-by: Will Madden <madden@prisma.io>
…ules-footprint cap (TML-2753)

Lands two calibration deltas surfaced during slice 2 round-2 reviews:

- F22 added to drive/calibration/failure-modes.md. D8 R1 reviewer ran
  `git stash` to isolate a stale-`dist` typecheck red and popped an
  unrelated worktree`s stash entry (stash is repository-global, not
  worktree-scoped). The mitigation is to use `git worktree add` for
  any base-ref / pre-existing-flake verification; reviewer briefs must
  carry the no-stash rule explicitly. D9 R1 reviewer honored the new
  rule via a sibling-worktree verification with no incident.

- .cursor/rules-footprint.config.json totalRulesBytes raised from 200000
  to 205000. The legitimate growth came from round-1 F16 tightening of
  `.agents/rules/no-target-branches.mdc` (+48 lines, +2 KB) to forbid
  cross-layer field additions on shared AST classes. The bump leaves
  ~4 KB of headroom; trim before the next bump.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
Copy link
Copy Markdown
Contributor Author

Round-2 corrective scope landed — re-requesting review

Operator review on round-1 (D5/D6/D7) cleared the architectural-mistake set but surfaced a deeper miss: the "contract-free" module was an option-bag wrapper around AST construction, marker writes threaded codec IDs at every leaf, marker reads went out as raw SQL strings, the per-adapter marker files were split, and the control-codec-registry was misnamed. The slice's name promised "typed query AST commands" — the disk did not deliver it. Implementer slot was upgraded from composer-2.5-fastclaude-4.6-sonnet-high-thinking for the corrective work.

Round 2 — three corrective dispatches

# Commit What landed
D8 313c3e714 Built a real fluent contract-free authoring surface (relational-core/src/contract-free/table.ts + per-target columns.ts). Typed table proxies, typed column proxies that carry codec at declaration, fluent chain depth ≥ 2, leaves never thread codec IDs / table names / column refs. Spirit reference: sql-builder (contract-bound), simpler shape (no ExecutionContext).
D9 90fa43431 Rewrote insertMarker / initMarker / updateMarker / writeLedgerEntry / readMarker against the D8 surface. Catalog-existence probe (information_schema.tables, sqlite_master) routes through D8 — no driver.query('select…', […]) strings left. Collapsed marker-read.ts + marker-ledger-writes.ts → one marker-ledger.ts per adapter. Deleted the deprecated dml.ts shims (tableRef / excludedColumn / dbExpr / insert / upsert / update / param) and pgTableRef.
D10 940e3d902 Renamed control-codec-registryast-codec-registry (the registry's mechanism, not the first caller's intent). git mv preserved history; JSDoc rewritten to describe what the registry IS.

Representative call site (post-round-2 updateMarker)

const marker = pgTable(
  { name: 'marker', schema: 'prisma_contract' },
  { space: text(), core_hash: text(), updated_at: timestamptz(), invariants: textArray(), ... },
);
const NOW = new RawExpr({ parts: ['now()'], returns: { codecId: PG_TIMESTAMPTZ_CODEC_ID, nullable: false } });

const query = marker
  .update()
  .set({ core_hash: destination.storageHash, updated_at: NOW, ...(merged !== undefined ? { invariants: merged } : {}) })
  .where(marker.space.eq(space).and(marker.core_hash.eq(expectedFrom)))
  .returning(marker.space)
  .build();

Chain depth 5. Codecs bound at declaration. Zero BinaryExpr.eq(...), zero ColumnRef.of(...), zero param(value, { codecId: X }) at leaves.

Scoreboard

11 PASS / 0 FAIL / 0 PENDING. Validation: pnpm build, pnpm typecheck, pnpm fixtures:check byte-identical (load-bearing — behaviour preserved), targeted package tests, pnpm lint:deps, biome — all green. Integration suite has the same pre-existing init-journey.e2e parallelism flake (passes on retry) documented since round-1 reviews; verified pre-existing via sibling-worktree (not git stash — see F22 below).

Calibration deltas (ride-along, 745c63a98)

  • F21 ("Implementer ships AST construction by hand wrapped in option-bag factories instead of building the fluent authoring surface the slice exists to deliver") added to drive/calibration/failure-modes.md — root cause of the round-1 miss; the D1 brief framed the win as "use the typed AST" instead of "deliver fluent authoring ergonomics."
  • F22 ("Reviewer runs git stash to debug a worktree-local red and pops an unrelated worktree's stash entry") — the D8 reviewer popped your d5-wip-2 stash from the tml-2812-… worktree by accident. Recovery: git fsck --no-reflogs --unreachable | rg 'dangling commit' in that worktree. Mitigation: review briefs now require git worktree add for any base-ref verification; D9 R1 reviewer honored it with no incident.
  • F19 DoR overlay rewritten property-form (was over-prescriptive file enumeration).
  • F20 ("Orchestrator role drift on small fixes") added — caught + corrected mid-round-1 after your YOU ARE AN ORCHESTRATOR interrupt.
  • rules-footprint cap bumped 200 KB → 205 KB to accommodate round-1's F16 tightening of no-target-branches.mdc (+48 lines).

Out-of-scope follow-up (operator decision)

  • F05 (D9 reviewer) — runtime adapter's readMarker wiring threads blindCast<TargetContract, 'Catalog probe has no contract'>(undefined) into renderLoweredSql at two sites. Clean close is widening Lower<T extends Contract | undefined> / renderLoweredSql's contract parameter to Contract | undefined. Recommend a separate small PR after this slice merges; happy to file a Linear ticket if you confirm.
  • D9 bare as cast in decodeSqliteMarkerRow (biome info-level; pre-existing on the new file). D10 correctly didn't touch it under pure-rename discipline. Suggest landing alongside the F05 close-out PR.

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