Skip to content

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

Merged
wmadden-electric merged 45 commits into
mainfrom
tml-2753-sql-marker-ops-through-adapter
Jun 4, 2026
Merged

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

Conversation

@wmadden-electric

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

Copy link
Copy Markdown
Contributor

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

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

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

pkg-pr-new Bot commented Jun 3, 2026

Copy link
Copy Markdown

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: 8642738

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 145.22 KB (+5.71% 🔺)
postgres / emit 117.02 KB (+7.54% 🔺)
mongo / no-emit 75.97 KB (0%)
mongo / emit 70.78 KB (0%)
cf-worker / no-emit 174.92 KB (+5.14% 🔺)
cf-worker / emit 143.68 KB (+6.82% 🔺)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 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 thread packages/2-sql/4-lanes/relational-core/src/ast/types.ts Outdated
Comment thread packages/2-sql/4-lanes/relational-core/src/contract-free/dml.ts Outdated
Comment thread packages/2-sql/9-family/src/core/verify.ts Outdated
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.

…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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 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 thread packages/2-sql/4-lanes/relational-core/src/contract-free/table.ts Outdated
Comment thread packages/2-sql/4-lanes/relational-core/src/contract-free/table.ts Outdated
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 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.

wmadden added 2 commits June 4, 2026 07:23
…roxies; fix docs link

- table(): feed source.alias ?? source.name into makeColumnProxy so ColumnRef
  and tableName use the alias when one is set.
- ColumnProxy.eq/neq: route null to NullCheckExpr (IS NULL / IS NOT NULL)
  instead of BinaryExpr (= NULL / <> NULL); the latter always evaluates to
  unknown in SQL.
- Add unit tests covering both bug paths.
- drive/code/README.md: update .cursor/rules link to canonical .agents/rules.

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

@wmadden wmadden left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Big improvement

Comment thread .cursor/rules-footprint.config.json Outdated
Comment thread packages/2-sql/4-lanes/relational-core/test/contract-free/table.test.ts Outdated
Comment thread packages/2-sql/4-lanes/relational-core/test/contract-free/table.test.ts Outdated
Comment thread packages/2-sql/5-runtime/src/codecs/ast-codec-registry.ts
Comment thread packages/2-sql/5-runtime/src/sql-marker.ts Outdated
Comment thread packages/3-targets/3-targets/sqlite/src/core/migrations/statement-builders.ts Outdated
Comment thread packages/3-targets/6-adapters/postgres/src/core/adapter.ts
Comment thread packages/3-targets/6-adapters/postgres/src/core/marker-ledger.ts
Comment thread packages/3-targets/6-adapters/sqlite/src/core/marker-ledger.ts
wmadden added 10 commits June 4, 2026 07:33
TML-2791 (remove docs/drive, now upstream in ignite) expanded AGENTS.md
by ~37 bytes updating the Drive-methodology link to the ignite tree. The
lint:rules:footprint CI job was skipped on that docs-only commit; the cap
was not adjusted there. Raise agentsBytes 11200 → 11500 to accommodate.

Signed-off-by: Will Madden <madden@prisma.io>
…20 follow-up

D8/D9/D10 closed SATISFIED but operator review surfaced three further findings
on the round-2 work that should land on the same branch before merge:

1. Marker/ledger ops become methods on the control adapter class, not
   module functions + thin class delegation (Postgres + SQLite + Mongo).
2. Bootstrap-DDL residue (sql-marker.ts, both statement-builders.ts,
   SqlStatement type) deleted from production source; test helpers
   rewritten in test/utils.ts against the contract-free DDL builders.
3. Bare `as` casts in slice 2 test files cleaned up (~32 sites across 4
   files), with README L26 prose fix + column-helper test simplification
   as ride-alongs.

Driver-bound control SPI + common MarkerReader abstraction filed as
TML-2820 (out of slice scope; surfaced from operator's symmetry
critique on PostgresAdapterImpl.profile.readMarker wiring).

Spec round-3 corrective scope; plan D11/D12/D13 dispatches.

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

Move MARKER_TABLE_NAME, LEDGER_TABLE_NAME, and CONTROL_TABLE_NAMES into a
production module so statement-builders can be deleted without breaking
issue-planner. statement-builders re-exports the constants until the
bootstrap-DDL residue is removed in a follow-up commit.

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

setupTestDatabase and new bootstrapPostgresSignMarkerTables lower
buildSignMarkerBootstrapQueries via the postgres control adapter instead
of raw ensureSchemaStatement / ensureTableStatement strings.

Signed-off-by: Will Madden <madden@prisma.io>
…6 + column-helper test simplification (TML-2753)

Replace bare as T casts with typed assignments and castAs in slice-2 test
files, collapse static column-helper assertions, and update runtime README
marker prose to reflect reads going through the control adapter SPI.

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

Move readMarker, insertMarker, initMarker, updateMarker, and writeLedgerEntry
implementations onto PostgresControlAdapter, SqliteControlAdapter, and
MongoControlAdapterImpl. Each SQL adapter method lowers via this.lower(...)
instead of a threaded lower callback.

marker-ledger.ts in each SQL adapter package now holds contract-free table
declarations, execute(), mergeInvariants (and decodeSqliteMarkerRow for
SQLite) only. Mongo marker-ledger.ts keeps wire helpers (executeAggregate,
parseMongoMarkerDoc, …) shared by the class.

Runtime adapter profiles call readMarkerDiscriminated on a control adapter
instance. Mongo package exports keep db-shaped entry points that delegate
through a minimal control-driver shim into the default adapter instance.

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

Remove sql-marker.ts, target statement-builders modules, and SqlStatement.
Migration runners and test helpers use LoweredStatement from the relational
core AST; postgres integration bootstrap lives in test/integration postgres-bootstrap.ts.

Signed-off-by: Will Madden <madden@prisma.io>
- bootstrap fixtures: cast DdlNode -> {Sqlite,Postgres}DdlNode at adapter.lower call sites
  (buildControlTableBootstrapQueries returns family-generic DdlNode[], but lower expects
  the target-specific subclass with accept())
- control-adapter.test.ts: replace castAs<Row[]> / castAs<CliStructuredError> with
  as unknown as ... — castAs<T>(value: T) cannot validate when T is a generic or the
  source is unknown
- runner.errors test files: drop unused executeStatement imports left behind by the
  bootstrap-DDL deletion
- sqlite/package.json: sort exports map (biome formatter)

Signed-off-by: Will Madden <madden@prisma.io>
…ytes cap to 11200

The canonical-vs-symlink explanation grew over time into 7+ paragraphs that
re-stated the same pattern three ways. Collapsed to: name the canonical paths,
state the one rule (edit there, not through the symlink), point at the CI
checks. Drive-skills-live-upstream paragraph kept (it's genuinely new info).

AGENTS.md 11,237 -> 9,149 bytes (-2,088). Reverts the cap bump in c58f7d3
that absorbed TML-2791's ~37-byte growth; the bumped cap is no longer needed.

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

Slice 2 promised that the control adapter speaks typed AST end-to-end:
"Zero driver.query(rawMarkerSql) outside adapter lowering for in-scope
SQL marker/ledger ops (git grep clean)." readMarker / readMarkerResult
were converted in the earlier round, but readAllMarkers and readLedger
still issued raw SELECT strings against information_schema.tables /
sqlite_master and the marker / ledger tables. Operator review caught the
hole; converting them here closes the done-condition for Slice 2.

Both methods now route through the contract-free fluent surface and the
shared execute(lower, driver, query) helper, mirroring readMarkerResult.

To support readLedger.orderBy(id), the relational-core CfSelectQuery
gains an .orderBy(column, dir?) method that constructs an OrderByItem
from the column proxy and threads it through the immutable builder.

To carry the DB-generated id (bigserial / INTEGER PK) and created_at
(default now() / strftime) without polluting the writeable insert shape,
each marker-ledger.ts declares a second handle `ledgerReadShape` with
the full column set; the original `ledger` keeps the writeable subset
the insert path needs. (TableInsertRow has no concept of "defaulted"
columns yet, so two handles is the simplest correct fix; a proper
defaulted-column flag is a separate concern.)

postgres needed an int8() column helper (added alongside int4 in
target-postgres/contract-free/columns.ts and the contract-free export
barrel). sqlite reuses the existing integer() helper.

control-adapter.test.ts: the readAllMarkers corrupt-row test matched
unquoted raw SQL (`information_schema.tables`, `from prisma_contract.marker`);
updated to quoted-identifier matchers consistent with the other lowered-
SQL matchers already in the file.

Done-condition check after this change:
  rg "driver\\.query" packages/3-targets/6-adapters/{postgres,sqlite}/src/core/{control-adapter,marker-ledger}.ts | rg -i "marker|ledger|prisma_contract|information_schema|sqlite_master"
  → only the two execute() helper sites that take a *lowered* AST.

Validation: adapter-postgres and adapter-sqlite typecheck + test suites
pass locally (561/561 + 174/174).

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

@wmadden wmadden left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Merge once my comments are addressed

wmadden added 4 commits June 4, 2026 09:36
… bump (TML-2753)

Operator review (PR #712, .cursor/rules-footprint.config.json:9) flagged
the totalRulesBytes bump from 200000 to 205000 in 745c63a and asked for
no-target-branches.mdc to be condensed instead.

Trimmed the "AST class fields are the same violation" section (the round-1
addition that triggered the bump) by ~1 KB. Kept the rule statement, one
WRONG example, and one CORRECT example; dropped the renderer-dilemma
intermediate example and tightened the why-this-matters prose. The pattern
reference to the three-layer polymorphic IR survives.

Result: no-target-branches.mdc 5759 → 4747 B. Total rules 196.1 KB → 195.3 KB,
fits under the restored 200000 cap with ~3.9 KB headroom.

Signed-off-by: Will Madden <madden@prisma.io>
Resolve AGENTS.md conflict by keeping main's expanded skills/rules
section; auto-merge brings in control-policy PSL authoring work.

Signed-off-by: Will Madden <madden@prisma.io>
Import PostgresContract from adapter-postgres/types, narrow bootstrap
queries to PostgresDdlNode for lower(), and drop stale executeStatement
imports after bootstrap helpers took over statement execution.

Signed-off-by: Will Madden <madden@prisma.io>
Probe information_schema.columns when the marker table exists so db init
surfaces PN-RUN-3020 instead of PN-RUN-3006 on pre-space marker tables.
Skip the probe when a driver returns non-catalog rows (unit-test mocks).

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric merged commit c159cd4 into main Jun 4, 2026
21 checks passed
@wmadden-electric wmadden-electric deleted the tml-2753-sql-marker-ops-through-adapter branch June 4, 2026 11:08
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