Skip to content

TML-2761: Target-contributed DDL in query AST + marker bootstrap via adapter#672

Merged
wmadden-electric merged 17 commits into
mainfrom
tml-2253-ddl-in-query-ast
Jun 2, 2026
Merged

TML-2761: Target-contributed DDL in query AST + marker bootstrap via adapter#672
wmadden-electric merged 17 commits into
mainfrom
tml-2253-ddl-in-query-ast

Conversation

@wmadden-electric

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

Copy link
Copy Markdown
Contributor

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

import { col, fn, lit } from '@prisma-next/sql-relational-core/contract-free';
import { buildControlTableBootstrapAsts } from '@prisma-next/target-postgres/contract-free';

// Migration runner / sign() — no raw CREATE TABLE strings in the execution path
for (const ast of family.bootstrapControlTableAsts()) {
  const { sql, params } = family.lowerAst(ast, { contract });
  await driver.query(sql, params);
}

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:

  1. Family DdlNode base + isAnyDdlNode guard; Postgres CreateTable/CreateSchema, SQLite CreateTable only.
  2. Adapter-owned DDL lowering (renderLoweredDdl + per-target visitors); DML renderer switch untouched.
  3. Contract-free builders (col/lit/fn, createTable/createSchema) and per-target bootstrap AST factories.
  4. Migration runners and sign() route bootstrap through family.lowerAst() — no ensure*Statement in those paths.

Reviewer notes

  • Largest diff: adapter DDL visitors + control-bootstrap.ts column shapes. Byte-equality against statement-builders constants is the pin — see packages/3-targets/6-adapters/*/test/migrations/ddl-lowering.test.ts.
  • PG lowercase/unquoted bootstrap SQL is intentional — matches existing ensure*Statement output for idempotent bootstrap, not contract-derived DDL.
  • ensure*Statement constants remain in statement-builders.ts / sql-marker.ts for tests and golden pins; only the execution paths were rerouted.
  • Rejected PR TML-2253: SQL DDL query-AST + contract-free builder + control-table bootstrap through the adapter #661 surface (generic-core ColumnType, shared control-tables.ts) is absent; grep gate included in verification.
  • Out of scope: marker DML/SPI consolidation (next slice), planner adoption, Mongo.

How it fits together

  1. Separate hierarchyrelational-core/src/ast/ddl-types.ts defines the family DDL base; targets own concrete node classes in their packages.
  2. Double-dispatch lowering — adapters widen lower() to AnyQueryAst | TargetDdlNode, routing DDL through renderLoweredDdl and DML through the existing renderer.
  3. Contract-free construction@prisma-next/sql-relational-core/contract-free plus @prisma-next/target-{postgres,sqlite}/contract-free build nodes without a contract.
  4. Bootstrap factoriesbuildControlTableBootstrapAsts() / buildSignMarkerBootstrapAsts() on each target; exposed on SqlControlAdapter and the family instance.
  5. First consumer — PG/SQLite migration runners and family sign() lower bootstrap ASTs and execute the result.

Behavior changes & evidence

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 typecheck
  • Package tests for the five packages above (DDL lowering, contract-free, runner errors, family-sql)
  • pnpm fixtures:check (DML lowering byte-identical)
  • Slice grep gates: no ColumnType/CreateSchemaAst in relational-core; no ensure*Statement in runner/sign bootstrap paths

Follow-ups

  • Marker/ledger DML through adapter (TML-2753 slice)
  • Migration planner DDL adoption (TML-2754 slice)
  • Mongo marker/ledger (separate slice)

Alternatives considered

  • Generic-core DDL in AnyQueryAst (PR TML-2253: SQL DDL query-AST + contract-free builder + control-table bootstrap through the adapter #661) — rejected; SQLite would stub CreateSchema, and a neutral ColumnType enum obscures dialect-native types.
  • Shared family-sql control-tables surface — rejected; PG and SQLite marker DDL legitimately differ; each target owns its bootstrap shape.
  • Single global DdlVisitor with 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

  • All commits are signed off (git commit -s) per the DCO.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern (foundational DDL-in-AST slice).
  • Tests are updated.
  • The PR title is in TML-2761: … form.
  • Skill update section filled in.

Summary by CodeRabbit

  • New Features

    • Contract-free DDL helpers: createTable/createSchema plus column builders (col, lit, fn) for portable table/schema definitions.
    • Bootstrap query generation exposed for control tables and sign-marker initialization across Postgres and SQLite.
    • Adapters and migration runners now accept and lower DDL nodes so bootstrapping uses generated DDL instead of fixed statements.
  • Tests

    • Added tests validating DDL builders and lowering for Postgres and SQLite.

wmadden added 7 commits June 1, 2026 09:31
… 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>
@wmadden-electric wmadden-electric requested a review from a team as a code owner June 1, 2026 15:56
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b6bffbba-04ca-4d56-aa09-e99dda6078bf

📥 Commits

Reviewing files that changed from the base of the PR and between 8c866db and 2c59dfc.

📒 Files selected for processing (1)
  • .cursor/rules-footprint.config.json
✅ Files skipped from review due to trivial changes (1)
  • .cursor/rules-footprint.config.json

📝 Walkthrough

Walkthrough

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

Changes

DDL Bootstrap Infrastructure

Layer / File(s) Summary
DDL AST Foundation and Contract-Free Builders
packages/2-sql/4-lanes/relational-core/src/ast/ddl-types.ts, packages/2-sql/4-lanes/relational-core/src/contract-free/column.ts, packages/2-sql/4-lanes/relational-core/src/exports/ast.ts, packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts, packages/2-sql/4-lanes/relational-core/package.json, packages/2-sql/4-lanes/relational-core/tsdown.config.ts, packages/2-sql/4-lanes/relational-core/test/contract-free/column.test.ts
Defines DdlNode abstract base and DdlColumn interface; adds contract-free col(), lit(), fn() helpers; exports and tests these APIs; updates exports and build entries.
Control Adapter DDL Bootstrap Interface
packages/2-sql/9-family/src/core/control-adapter.ts
Extends SqlControlAdapter with bootstrapControlTableQueries() and bootstrapSignMarkerQueries() returning DdlNode[]; broadens lower() to accept DdlNode and AnyQueryAst.
Control Instance Bootstrap Implementation
packages/2-sql/9-family/src/core/control-instance.ts
Updates SqlControlFamilyInstance to accept `AnyQueryAst
Postgres DDL AST Nodes and Visitor Pattern
packages/3-targets/3-targets/postgres/src/core/ddl/nodes.ts, packages/3-targets/3-targets/postgres/src/exports/ddl.ts, packages/3-targets/3-targets/postgres/test/contract-free/ddl.test.ts
Implements PostgresDdlNode, PostgresCreateTable, PostgresCreateSchema, immutability helpers, and visitor dispatch; exposes types and tests node construction.
SQLite DDL AST Nodes and Visitor Pattern
packages/3-targets/3-targets/sqlite/src/core/ddl/nodes.ts, packages/3-targets/3-targets/sqlite/src/exports/ddl.ts, packages/3-targets/3-targets/sqlite/test/contract-free/ddl.test.ts
Implements SqliteDdlNode, SqliteCreateTable, immutability helpers, and visitor dispatch; exposes types and tests node construction.
Postgres Contract-Free DDL Builders
packages/3-targets/3-targets/postgres/src/contract-free/ddl.ts, packages/3-targets/3-targets/postgres/src/contract-free/control-bootstrap.ts, packages/3-targets/3-targets/postgres/src/exports/contract-free.ts, packages/3-targets/3-targets/postgres/package.json, packages/3-targets/3-targets/postgres/tsdown.config.ts
Adds createTable/createSchema factories and buildControlTableBootstrapQueries/buildSignMarkerBootstrapQueries for prisma_contract schema (marker, ledger); updates exports and build config.
SQLite Contract-Free DDL Builders
packages/3-targets/3-targets/sqlite/src/contract-free/ddl.ts, packages/3-targets/3-targets/sqlite/src/contract-free/control-bootstrap.ts, packages/3-targets/3-targets/sqlite/src/exports/contract-free.ts, packages/3-targets/3-targets/sqlite/package.json, packages/3-targets/3-targets/sqlite/tsdown.config.ts
Adds createTable factory and bootstrap query builders for _prisma_marker and _prisma_ledger; updates exports and build config.
Postgres DDL Renderer and Adapter Lowering
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/ddl-renderer.ts, packages/3-targets/6-adapters/postgres/test/ddl-create-table-lowering.test.ts, packages/3-targets/6-adapters/postgres/test/migrations/ddl-lowering.test.ts
Adds renderLoweredDdl to render Postgres DDL AST to SQL; adapters detect Postgres DDL nodes and route to DDL renderer; tests validate exact SQL and empty params.
SQLite DDL Renderer and Adapter Lowering
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/ddl-renderer.ts, packages/3-targets/6-adapters/sqlite/test/ddl-create-table-lowering.test.ts, packages/3-targets/6-adapters/sqlite/test/migrations/ddl-lowering.test.ts
Adds renderLoweredDdl to render SQLite DDL AST to SQL; adapters detect SQLite DDL nodes and route to DDL renderer; tests validate IF NOT EXISTS and exact SQL/params matching.
Postgres Migration Runner Bootstrap
packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts
Refactors ensureControlTables to accept contract, fetch bootstrap queries, lower schema and table queries with contract-scoped context, require schema query presence, and execute lowered statements sequentially.
SQLite Migration Runner Bootstrap
packages/3-targets/3-targets/sqlite/src/core/migrations/runner.ts
Refactors ensureControlTables to accept contract, fetch bootstrap queries, lower each with contract-scoped context, and execute lowered statements; retains legacy marker-shape detection.
Rule Documentation and Configuration
.agents/rules/sql-queries-not-asts.mdc
Updates rule text to standardize executable units as “queries” (not “asts”), provides naming examples and a grep quick-check that allows structural DDL nouns like DdlNode, AnyQueryAst, DdlVisitor, and accept.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • aqrln

Poem

A rabbit builds tables row by row,
With DDL nodes that gently flow,
From schema seed to lowered string,
The bootstrap runs — the adapters sing.
🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and clearly describes the main changes: introduction of target-contributed DDL in the query AST and marker bootstrap via adapter, which aligns with the core objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2253-ddl-in-query-ast

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/2-sql/4-lanes/relational-core/src/ast/ddl-types.ts Outdated
Comment thread packages/2-sql/4-lanes/relational-core/src/contract-free/column.ts Outdated
Comment thread packages/2-sql/9-family/src/core/control-adapter.ts
…-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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

38-40: 💤 Low value

Type guard is imprecise but functionally safe.

The guard claims node is SqliteDdlNode but only checks isDdlNode(node), which would be true for any DdlNode (including Postgres DDL nodes). However, the system design ensures only SQLite DDL nodes reach this adapter through bootstrapControlTableQueries() and bootstrapSignMarkerQueries(), so runtime behavior is correct.

For stronger type safety, consider checking a SQLite-specific property (e.g., node.kind starts 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 win

Consider adding a clarifying comment for the hardcoded codecId.

The function storageColumnFromDdlColumn hardcodes codecId: 'pg/text@1' for all columns regardless of their actual type (line 30), and strips primary key from 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7ff5200 and 8c866db.

⛔ Files ignored due to path filters (9)
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/design-notes.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/learnings.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/plan.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/ddl-in-query-ast/plan.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/ddl-in-query-ast/spec.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/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/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 (38)
  • .agents/rules/sql-queries-not-asts.mdc
  • packages/2-sql/4-lanes/relational-core/package.json
  • packages/2-sql/4-lanes/relational-core/src/ast/ddl-types.ts
  • packages/2-sql/4-lanes/relational-core/src/contract-free/column.ts
  • packages/2-sql/4-lanes/relational-core/src/exports/ast.ts
  • packages/2-sql/4-lanes/relational-core/src/exports/contract-free.ts
  • packages/2-sql/4-lanes/relational-core/test/contract-free/column.test.ts
  • packages/2-sql/4-lanes/relational-core/tsdown.config.ts
  • packages/2-sql/9-family/src/core/control-adapter.ts
  • packages/2-sql/9-family/src/core/control-instance.ts
  • packages/3-targets/3-targets/postgres/package.json
  • packages/3-targets/3-targets/postgres/src/contract-free/control-bootstrap.ts
  • packages/3-targets/3-targets/postgres/src/contract-free/ddl.ts
  • packages/3-targets/3-targets/postgres/src/core/ddl/nodes.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/runner.ts
  • packages/3-targets/3-targets/postgres/src/exports/contract-free.ts
  • packages/3-targets/3-targets/postgres/src/exports/ddl.ts
  • packages/3-targets/3-targets/postgres/test/contract-free/ddl.test.ts
  • packages/3-targets/3-targets/postgres/tsdown.config.ts
  • packages/3-targets/3-targets/sqlite/package.json
  • packages/3-targets/3-targets/sqlite/src/contract-free/control-bootstrap.ts
  • packages/3-targets/3-targets/sqlite/src/contract-free/ddl.ts
  • packages/3-targets/3-targets/sqlite/src/core/ddl/nodes.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/runner.ts
  • packages/3-targets/3-targets/sqlite/src/exports/contract-free.ts
  • packages/3-targets/3-targets/sqlite/src/exports/ddl.ts
  • packages/3-targets/3-targets/sqlite/test/contract-free/ddl.test.ts
  • packages/3-targets/3-targets/sqlite/tsdown.config.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/ddl-renderer.ts
  • packages/3-targets/6-adapters/postgres/test/ddl-create-table-lowering.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/ddl-lowering.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/ddl-renderer.ts
  • packages/3-targets/6-adapters/sqlite/test/ddl-create-table-lowering.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/ddl-lowering.test.ts

Comment thread packages/3-targets/6-adapters/sqlite/src/core/ddl-renderer.ts Outdated
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 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.

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

Pre-emptive approve. Address all comments, then merge.

Comment thread .agents/rules/ast-construction-frozen-classes.mdc Outdated
Comment thread packages/2-sql/4-lanes/relational-core/src/contract-free/column.ts
Comment thread packages/3-targets/6-adapters/postgres/src/core/adapter.ts Outdated
Comment thread packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts Outdated
Comment thread packages/3-targets/6-adapters/postgres/src/core/ddl-renderer.ts
Comment thread packages/3-targets/6-adapters/sqlite/src/core/adapter.ts Outdated
Resolves import conflict in postgres migrations runner (keep both
UNBOUND_NAMESPACE_ID and SqlStorage imports).

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

pkg-pr-new Bot commented Jun 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: dc00715

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 136.47 KB (+0.27% 🔺)
postgres / emit 108.81 KB (+0.28% 🔺)
mongo / no-emit 75.77 KB (0%)
mongo / emit 70.78 KB (0%)
cf-worker / no-emit 165.36 KB (+0.24% 🔺)
cf-worker / emit 134.51 KB (+0.27% 🔺)

wmadden added 3 commits June 2, 2026 16:48
…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>
@wmadden-electric wmadden-electric enabled auto-merge (squash) June 2, 2026 15:04
wmadden added 3 commits June 2, 2026 17:13
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>
@wmadden-electric wmadden-electric merged commit a04c042 into main Jun 2, 2026
21 checks passed
@wmadden-electric wmadden-electric deleted the tml-2253-ddl-in-query-ast branch June 2, 2026 16:00
medz pushed a commit to medz/prisma-next that referenced this pull request Jun 5, 2026
…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>
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