TML-2761: Target-contributed DDL in query AST + marker bootstrap via adapter#672
Conversation
… DDL in the query AST Start the re-scoped project fresh off main after closing #661 (which built the rejected generic-core DDL approach). Carries the respec artifacts: design-notes/spec/plan reframed around "expand the SQL query AST to represent DDL as a target-contributed surface the adapter lowers" (target owns shape, adapter owns lowering), the foundational ddl-in-query-ast slice (spike folded into its first dispatch), and the sql-marker slice rebased onto it. Signed-off-by: Will Madden <madden@prisma.io>
Add a parallel DdlNode/DdlVisitor hierarchy beside DML query ASTs and route adapter.lower() through dialect-specific DDL visitors without touching the existing renderLoweredSql switch or AnyQueryAst membership. Signed-off-by: Will Madden <madden@prisma.io>
Trim the family DdlNode base (no accept/visitor), add DdlColumn.default from contract ColumnDefault, and move CreateTable/CreateSchema concretes into target-postgres and target-sqlite with adapter-owned lowering. Signed-off-by: Will Madden <madden@prisma.io>
Harden Postgres and SQLite DDL renderers so lowered CreateSchema/CreateTable ASTs match the existing ensure*Statement bootstrap constants exactly, add ifNotExists on CreateTable, and tighten lower() to accept target DDL nodes. Pin byte-equality in adapter migration tests; fixtures:check stays green. Signed-off-by: Will Madden <madden@prisma.io>
Record per-target visitor decision and slice plan corrections as the foundational dispatches land. Signed-off-by: Will Madden <madden@prisma.io>
Expose col/lit/fn helpers on sql-relational-core and per-target createTable/createSchema factories so bootstrap and planner callers can build DDL nodes without a contract; refactor byte-equality tests to use them. Signed-off-by: Will Madden <madden@prisma.io>
Control adapters now expose bootstrap AST builders and lower DDL nodes; migration runners and sign() execute lowered bootstrap SQL instead of raw ensure*Statement constants, closing the first consumer path for the target-contributed DDL surface. Signed-off-by: Will Madden <madden@prisma.io>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds DDL AST primitives, contract-free DDL builders, Postgres/SQLite DDL node implementations and renderers, updates adapters and family interfaces to lower DDL nodes, and refactors migration runners to bootstrap control tables by lowering adapter-provided DDL queries. ChangesDDL Bootstrap Infrastructure
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…-asts naming Incorporates PR #672 review feedback: - Replace the dead, central kind-set DDL guard (isAnyDdlNode/ddlAstKinds) with a base-class isDdlNode() brand so any DdlNode subclass is recognised without a registry; adapter guards narrow via the brand instead of re-enumerating kinds. - col() uses the ifDefined helper instead of hand-rolled conditional spreads. - Rename bootstrap*Asts / build*BootstrapAsts to *Queries: the built-and-lowered unit is a query, not an AST. Adds .agents/rules/sql-queries-not-asts.mdc to stop the recurring naming drift. Signed-off-by: Will Madden <madden@prisma.io>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts (1)
38-40: 💤 Low valueType guard is imprecise but functionally safe.
The guard claims
node is SqliteDdlNodebut only checksisDdlNode(node), which would be true for anyDdlNode(including Postgres DDL nodes). However, the system design ensures only SQLite DDL nodes reach this adapter throughbootstrapControlTableQueries()andbootstrapSignMarkerQueries(), so runtime behavior is correct.For stronger type safety, consider checking a SQLite-specific property (e.g.,
node.kindstarts with 'sqlite_') or adding a target discriminator to DDL nodes.🤖 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/control-adapter.ts` around lines 38 - 40, The isSqliteDdlNode type guard currently returns isDdlNode(node) which is too broad; update isSqliteDdlNode to assert a SQLite-specific discriminator (for example check node.kind startsWith('sqlite_') or a target property) so it truly narrows to SqliteDdlNode. Locate the guard function isSqliteDdlNode(AnyQueryAst | DdlNode) and modify its predicate to include the SQLite-specific check (or add a target discriminator on DdlNode and check that) while keeping bootstrapControlTableQueries() and bootstrapSignMarkerQueries() callers unchanged.packages/3-targets/6-adapters/postgres/src/core/ddl-renderer.ts (1)
27-34: ⚡ Quick winConsider adding a clarifying comment for the hardcoded codecId.
The function
storageColumnFromDdlColumnhardcodescodecId: 'pg/text@1'for all columns regardless of their actual type (line 30), and stripsprimary keyfrom the type string (line 28). While the tests validate this works correctly for bootstrap DDL, the intent isn't clear from the code alone. A brief comment explaining why this simplified approach is safe for bootstrap-only scenarios would help future maintainers.🤖 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/ddl-renderer.ts` around lines 27 - 34, Add a brief clarifying comment inside the storageColumnFromDdlColumn function above the codecId assignment explaining that codecId is intentionally hardcoded to 'pg/text@1' (and that primary-key suffix is stripped from column.type) because this module only needs a minimal, type-agnostic codec for bootstrap DDL processing; mention that this is a deliberate simplification safe for bootstrap-only scenarios and note that any future full-type mapping must replace this with proper type-to-codec resolution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/3-targets/6-adapters/sqlite/src/core/ddl-renderer.ts`:
- Around line 19-43: In renderColumnDefault, when defaultValue.kind ===
'literal' and the value is a string (inside the branch handling typeof value ===
'string'), escape any single quotes by doubling them before interpolating into
the returned SQL (so "O'Reilly" becomes "O''Reilly"); implement this by
replacing all occurrences of "'" with "''" on the string prior to building the
`DEFAULT '...'` result so generated SQLite SQL is valid.
---
Nitpick comments:
In `@packages/3-targets/6-adapters/postgres/src/core/ddl-renderer.ts`:
- Around line 27-34: Add a brief clarifying comment inside the
storageColumnFromDdlColumn function above the codecId assignment explaining that
codecId is intentionally hardcoded to 'pg/text@1' (and that primary-key suffix
is stripped from column.type) because this module only needs a minimal,
type-agnostic codec for bootstrap DDL processing; mention that this is a
deliberate simplification safe for bootstrap-only scenarios and note that any
future full-type mapping must replace this with proper type-to-codec resolution.
In `@packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts`:
- Around line 38-40: The isSqliteDdlNode type guard currently returns
isDdlNode(node) which is too broad; update isSqliteDdlNode to assert a
SQLite-specific discriminator (for example check node.kind startsWith('sqlite_')
or a target property) so it truly narrows to SqliteDdlNode. Locate the guard
function isSqliteDdlNode(AnyQueryAst | DdlNode) and modify its predicate to
include the SQLite-specific check (or add a target discriminator on DdlNode and
check that) while keeping bootstrapControlTableQueries() and
bootstrapSignMarkerQueries() callers unchanged.
🪄 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: edf22dcf-90ff-436f-8573-cba60708d9d0
⛔ Files ignored due to path filters (9)
projects/migrate-marker-ledger-to-typed-query-ast-commands/design-notes.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/learnings.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/plan.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/ddl-in-query-ast/plan.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/ddl-in-query-ast/spec.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/plan.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sql-marker-ops-through-adapter/spec.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/spec.mdis excluded by!projects/**projects/migrate-marker-ledger-to-typed-query-ast-commands/subagent-registry.mdis excluded by!projects/**
📒 Files selected for processing (38)
.agents/rules/sql-queries-not-asts.mdcpackages/2-sql/4-lanes/relational-core/package.jsonpackages/2-sql/4-lanes/relational-core/src/ast/ddl-types.tspackages/2-sql/4-lanes/relational-core/src/contract-free/column.tspackages/2-sql/4-lanes/relational-core/src/exports/ast.tspackages/2-sql/4-lanes/relational-core/src/exports/contract-free.tspackages/2-sql/4-lanes/relational-core/test/contract-free/column.test.tspackages/2-sql/4-lanes/relational-core/tsdown.config.tspackages/2-sql/9-family/src/core/control-adapter.tspackages/2-sql/9-family/src/core/control-instance.tspackages/3-targets/3-targets/postgres/package.jsonpackages/3-targets/3-targets/postgres/src/contract-free/control-bootstrap.tspackages/3-targets/3-targets/postgres/src/contract-free/ddl.tspackages/3-targets/3-targets/postgres/src/core/ddl/nodes.tspackages/3-targets/3-targets/postgres/src/core/migrations/runner.tspackages/3-targets/3-targets/postgres/src/exports/contract-free.tspackages/3-targets/3-targets/postgres/src/exports/ddl.tspackages/3-targets/3-targets/postgres/test/contract-free/ddl.test.tspackages/3-targets/3-targets/postgres/tsdown.config.tspackages/3-targets/3-targets/sqlite/package.jsonpackages/3-targets/3-targets/sqlite/src/contract-free/control-bootstrap.tspackages/3-targets/3-targets/sqlite/src/contract-free/ddl.tspackages/3-targets/3-targets/sqlite/src/core/ddl/nodes.tspackages/3-targets/3-targets/sqlite/src/core/migrations/runner.tspackages/3-targets/3-targets/sqlite/src/exports/contract-free.tspackages/3-targets/3-targets/sqlite/src/exports/ddl.tspackages/3-targets/3-targets/sqlite/test/contract-free/ddl.test.tspackages/3-targets/3-targets/sqlite/tsdown.config.tspackages/3-targets/6-adapters/postgres/src/core/adapter.tspackages/3-targets/6-adapters/postgres/src/core/control-adapter.tspackages/3-targets/6-adapters/postgres/src/core/ddl-renderer.tspackages/3-targets/6-adapters/postgres/test/ddl-create-table-lowering.test.tspackages/3-targets/6-adapters/postgres/test/migrations/ddl-lowering.test.tspackages/3-targets/6-adapters/sqlite/src/core/adapter.tspackages/3-targets/6-adapters/sqlite/src/core/control-adapter.tspackages/3-targets/6-adapters/sqlite/src/core/ddl-renderer.tspackages/3-targets/6-adapters/sqlite/test/ddl-create-table-lowering.test.tspackages/3-targets/6-adapters/sqlite/test/migrations/ddl-lowering.test.ts
Review follow-up (PR #672, findings F05/F06/F07/F09): - Postgres renderColumnDefault now mirrors the SQLite renderer: one direct ColumnDefault->clause function. Removes the dead buildColumnDefaultSql reuse (against a fabricated pg/text@1 StorageColumn), the duplicate renderBootstrapDefault fallback, and the .toLowerCase() that would corrupt uppercase literal content. Bootstrap output stays byte-identical. - Document the contract-free DDL precondition: createTable/createSchema emit identifiers and string-literal defaults verbatim (no quoting/escaping); real quoting is deferred to the planner-adoption slice where untrusted identifiers flow. Recorded in JSDoc and design-notes. - Add per-shape default lowering tests (string/number/boolean/null literal, now(), arbitrary function, autoincrement-elision) for both targets. Signed-off-by: Will Madden <madden@prisma.io>
wmadden
left a comment
There was a problem hiding this comment.
AST nodes should use inheritance, not frozen objects. That's the whole reason for the base abstract class just introduced
… object literals col/lit/fn and DdlColumn returned frozen plain objects, diverging from the frozen-class AST hierarchy every other DDL node follows. Introduce a local DdlColumnDefault visitor hierarchy (LiteralColumnDefault / FunctionColumnDefault) and promote DdlColumn to a frozen class in relational-core; the factories now return instances and both adapters render defaults through a DdlColumnDefaultVisitor. The foundational ColumnDefault union and its planner consumers are left untouched (local hierarchy only). Also fixes freezeDdlColumns, which shallow-copied each column into a prototype-less object and would have stripped class identity from columns and their nested defaults. Adds an always-on rule (ast-construction-frozen-classes) so AST/IR construction surfaces do not regress to frozen object-literal factories. Signed-off-by: Will Madden <madden@prisma.io>
wmadden
left a comment
There was a problem hiding this comment.
Pre-emptive approve. Address all comments, then merge.
Resolves import conflict in postgres migrations runner (keep both UNBOUND_NAMESPACE_ID and SqlStorage imports). Signed-off-by: Will Madden <madden@prisma.io>
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
…l defaults (TML-2761)
Address PR review:
- Delete the four is{Postgres,Sqlite}DdlNode wrappers in the adapters and
control-adapters. They wrapped isDdlNode() but asserted node is <Target>DdlNode,
a claim isDdlNode never proved. Each lower() already takes a target-specific
union (AnyQueryAst | <Target>DdlNode), so calling isDdlNode(ast) directly
narrows the true branch to the target node soundly, with no cast.
- Escape single quotes in string-literal column defaults via the shared
escapeLiteral in both DDL renderers. Byte-identical for the quote-free
control-plane defaults; injection-safe for quote-bearing values. Identifier
quoting stays deferred. JSDoc and design notes updated to match.
- Remove the redundant ast-construction-frozen-classes rule (covered by the
existing use-ast-factories rule); fold its one distinct point (production
factories must return class instances, not frozen object literals) into the
ast-visitor-pattern skill.
Signed-off-by: Will Madden <madden@prisma.io>
…-ast Signed-off-by: Will Madden <madden@prisma.io> # Conflicts: # packages/3-targets/6-adapters/sqlite/src/core/control-adapter.ts
…trap DDL (TML-2761) Reconcile the typed-DDL ledger bootstrap with the per-space ledger schema that landed on main while this branch was open. main added space, migration_name, and migration_hash columns to the ledger (and switched the SQLite created_at default to strftime ISO-8601), updating the raw-string ensureLedgerTableStatement goldens. The textual merge left the contract-free DDL builders on the old shape, so the bootstrap created a ledger without the space column while the ledger INSERT referenced it. Mirror the new columns and SQLite created_at default in both targets DDL builders and the matching ddl-lowering byte-equality tests. Signed-off-by: Will Madden <madden@prisma.io>
The new sql-queries-not-asts.mdc alwaysApply rule adds ~1.7 KB to the always-loaded context, pushing the total to 25.9 KB (26,522 bytes). Raise the threshold from 26000 to 27000 to reflect this intentional addition. Signed-off-by: Will Madden <madden@prisma.io>
…-ast Signed-off-by: Will Madden <madden@prisma.io>
…-ast Signed-off-by: Will Madden <madden@prisma.io>
…SPI (prisma#712) ## Linked issue Refs [TML-2753](https://linear.app/prisma-company/issue/TML-2753) — Slice 2 of the marker/ledger typed-query-AST project (Slice 1: prisma#672). Related: TML-2754 (planner adoption), TML-2253 (project root). ## At a glance ```ts // 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. **D1** — `SqlControlAdapter` 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` `$set`s 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 - **Marker writes go through `family.initMarker` / `family.updateMarker`** — [`packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts`](packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts), [`packages/3-targets/3-targets/sqlite/src/core/migrations/runner.ts`](packages/3-targets/3-targets/sqlite/src/core/migrations/runner.ts), [`packages/2-sql/9-family/src/core/control-instance.ts`](packages/2-sql/9-family/src/core/control-instance.ts). Evidence: [`packages/3-targets/6-adapters/postgres/test/marker-ledger-writes.test.ts`](packages/3-targets/6-adapters/postgres/test/marker-ledger-writes.test.ts), sqlite sibling, and the PGlite + node:sqlite integration tests. - **Ledger writes go through `family.writeLedgerEntry`** — same runner files. Evidence: `runner.ledger.test.ts` + `runner.ledger.integration.test.ts` (snapshot columns assert `toBeNull()`). - **Invariant merge is a single TS implementation** — [`packages/3-targets/6-adapters/postgres/src/core/marker-ledger-writes.ts`](packages/3-targets/6-adapters/postgres/src/core/marker-ledger-writes.ts), sqlite sibling. Evidence: D2 unit + integration tests pinning `union({a,b},{b,c}) → [a,b,c]` on both dialects. - **One read helper + one parser** — [`packages/2-sql/9-family/src/core/verify.ts`](packages/2-sql/9-family/src/core/verify.ts) (`readMarkerResult`). The duplicate `sql-runtime/marker.ts` parser and the SQLite runner's private read are deleted, not wrapped. - **DDL columns retained, SPI write set reduced** — see "Reviewer notes — the consequential change" above. ## 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 - Migration planner DDL adoption (TML-2754 slice). - Mongo marker/ledger via typed AST (separate slice). - README enumerations of `SqlMigrationRunnerErrorCode` in [`packages/2-sql/9-family/README.md`](packages/2-sql/9-family/README.md) and [`packages/3-targets/3-targets/postgres/README.md`](packages/3-targets/3-targets/postgres/README.md) don't yet list `MARKER_CAS_FAILURE` — small follow-up tidy ticket; not gating this PR. ## 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. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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 <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Will Madden <madden@prisma.io>
Linked issue
Refs TML-2761 — foundational slice for marker/ledger via typed query AST commands (related: TML-2753, TML-2754, TML-2253).
At a glance
Before this PR, marker/ledger bootstrap executed pre-built SQL strings from
statement-builders/sql-marker. After it, each target builds frozen DDL nodes and the control adapter lowers them through the same visitor path as the runtime adapter.Decision
This PR ships a target-contributed DDL surface in the SQL query AST — separate from DML
AnyQueryAst— with per-target visitors in the Postgres and SQLite adapters, contract-free constructors, and marker/ledger bootstrap as the first real consumer.Deliverables:
DdlNodebase +isAnyDdlNodeguard; PostgresCreateTable/CreateSchema, SQLiteCreateTableonly.renderLoweredDdl+ per-target visitors); DML renderer switch untouched.col/lit/fn,createTable/createSchema) and per-target bootstrap AST factories.sign()route bootstrap throughfamily.lowerAst()— noensure*Statementin those paths.Reviewer notes
control-bootstrap.tscolumn shapes. Byte-equality againststatement-buildersconstants is the pin — seepackages/3-targets/6-adapters/*/test/migrations/ddl-lowering.test.ts.ensure*Statementoutput for idempotent bootstrap, not contract-derived DDL.ensure*Statementconstants remain instatement-builders.ts/sql-marker.tsfor tests and golden pins; only the execution paths were rerouted.ColumnType, sharedcontrol-tables.ts) is absent; grep gate included in verification.How it fits together
relational-core/src/ast/ddl-types.tsdefines the family DDL base; targets own concrete node classes in their packages.lower()toAnyQueryAst | TargetDdlNode, routing DDL throughrenderLoweredDdland DML through the existing renderer.@prisma-next/sql-relational-core/contract-freeplus@prisma-next/target-{postgres,sqlite}/contract-freebuild nodes without a contract.buildControlTableBootstrapAsts()/buildSignMarkerBootstrapAsts()on each target; exposed onSqlControlAdapterand the family instance.sign()lower bootstrap ASTs and execute the result.Behavior changes & evidence
packages/3-targets/6-adapters/postgres/src/core/ddl-renderer.ts,packages/3-targets/6-adapters/sqlite/src/core/ddl-renderer.ts. Evidence:packages/3-targets/6-adapters/postgres/test/migrations/ddl-lowering.test.ts, sqlite sibling.packages/2-sql/4-lanes/relational-core/src/contract-free/column.ts, targetcontract-free/ddl.ts. Evidence:test/contract-free/in relational-core and target packages.packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts,packages/2-sql/9-family/src/core/control-instance.ts. Evidence: runner error integration tests, family-sql test suite.sign()is target-correct for SQLite — no longer calls PG-flavoredensureSchemaStatement/ensureTableStatementfromsql-marker.ts.Testing performed
pnpm --filter @prisma-next/target-postgres --filter @prisma-next/target-sqlite --filter @prisma-next/adapter-postgres --filter @prisma-next/adapter-sqlite --filter @prisma-next/family-sql typecheckpnpm fixtures:check(DML lowering byte-identical)ColumnType/CreateSchemaAstin relational-core; noensure*Statementin runner/sign bootstrap pathsFollow-ups
Alternatives considered
AnyQueryAst(PR TML-2253: SQL DDL query-AST + contract-free builder + control-table bootstrap through the adapter #661) — rejected; SQLite would stubCreateSchema, and a neutralColumnTypeenum obscures dialect-native types.family-sqlcontrol-tables surface — rejected; PG and SQLite marker DDL legitimately differ; each target owns its bootstrap shape.DdlVisitorwith SQLite no-ops — rejected; per-target visitor interfaces keep SQLite from stubbing schema operations it doesn't have.Skill update
n/a — internal only (framework/target adapter surface; no user-facing CLI or config change).
Checklist
git commit -s) per the DCO.TML-2761: …form.Summary by CodeRabbit
New Features
Tests