Skip to content

TML-2859: SQLite migration planner builds CREATE TABLE as typed DDL through the adapter#768

Merged
wmadden-electric merged 14 commits into
mainfrom
tml-2859-sqlite-create-table-adoption
Jun 10, 2026
Merged

TML-2859: SQLite migration planner builds CREATE TABLE as typed DDL through the adapter#768
wmadden-electric merged 14 commits into
mainfrom
tml-2859-sqlite-create-table-adoption

Conversation

@wmadden-electric

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

Copy link
Copy Markdown
Contributor

At a glance

The SQLite migration planner's CREATE TABLE no longer concatenates raw SQL — it builds a typed DDL-AST node and lowers it through the adapter at plan time, the same shape Postgres adopted in #751.

before:  SqliteCreateTableCall.toOp()        ─▶ renderCreateTableSql(name, spec)        → "CREATE TABLE …"  (raw string, in the planner)
after:   SqliteCreateTableCall.toOp(lower)   ─▶ createTable({ columns, constraints })   ─lower()─▶ adapter renders the SQL

The user-facing authoring form on the SQLite side becomes this.createTable({ ... }) on SqliteMigration — same shape Postgres already has.

What changed

  • SqliteCreateTableCall refactored (packages/3-targets/3-targets/sqlite/src/core/migrations/op-factory-call.ts). The class held a flat SqliteTableSpec (string defaultSql fragments); now it holds readonly DdlColumn[] + readonly DdlTableConstraint[]?, mirroring PostgresCreateTableCall. toOp(lowerer) builds the contract-free createTable({...}) node and lowers via lowerer.lower(node, { contract: {} }). The planner's upstream construction site at issue-planner.ts builds the structured node list from the same source data, with a new sqliteDefaultToDdlColumnDefault helper mirroring the Postgres analogue (autoincrement() short-circuits to undefined; literal defaults wrap in LiteralColumnDefault; expression defaults wrap in FunctionColumnDefault).
  • SqliteMigration authoring API. A protected createTable({ table, columns, constraints? }) method on the base class instantiates the call and lowers via the held controlAdapter. The free createTable(tableName, spec) re-export is dropped from @prisma-next/sqlite/migration; the structural constructors col, lit, fn, primaryKey, foreignKey, unique are re-exported instead. The user-facing breaking change is recorded via the upgrade-instructions skill.
  • Adapter renderer convention reconciled (packages/3-targets/6-adapters/sqlite/src/core/ddl-renderer.ts). The SQLite DDL renderer didn't quote identifiers and used a different indent than the planner's canonical format. Fixed to quote identifiers and use the 2-space indent the planner emits, so the lowered SQL is byte-identical to the pre-slice output. quoteIdentifier / quoteQualifiedIdentifier helpers added.

The substrate-fix decision — adapter renderer convention

The first dispatch attempted the call-class refactor on top of the existing SQLite renderer. The byte-parity test failed immediately: the renderer didn't quote identifiers, used the wrong indent, and pre-rendered literal defaults inline rather than as a structured node. Three options surfaced:

  1. Fix the renderer first. Reconcile its conventions to the planner's canonical format. Slice grows; the rest of the work proceeds.
  2. Make the planner output match the renderer's existing form. Rewrites the planner's output to a different SQL convention.
  3. Special-case the byte-parity test to absorb the difference. Drops the byte-parity DoD.

Picked (1) — the planner's format is the canonical one (Postgres lowered to it in slice 4) and the renderer was the outlier. The slice's spec + plan amended to add the substrate fix as dispatch 2, the call-class refactor as dispatch 3, and the authoring API as dispatch 4.

Byte-parity — no behaviour change

A cross-implementation byte-parity test (packages/3-targets/6-adapters/sqlite/test/migrations/create-table-call-byte-parity.test.ts) drives SqliteCreateTableCall.toOp(lowerer) end-to-end with a real SqliteControlAdapter() and compares the lowered SQL to the pre-slice renderCreateTableSql output for 12 representative shapes (simple, composite PK, FK with actions, table-level unique, autoincrement, plus 7 literal-default kinds). The oracle imports createTable directly from the internal operations/tables.ts module — the free createTable and renderCreateTableSql stay on disk because recreateTable (Phase 2) still calls them; only the facade re-export was dropped.

The renderer's defaultVisitor.literal was expanded in D5 to type-branch correctly on boolean (→ 0 / 1), Date (→ ISO single-quoted), and bigint (→ String(value)) for parity with renderDefaultLiteral. This expansion is a transitional state that TML-2867 deletes by routing the value through the column's codec — the type-branching becomes dead once the codec produces the canonical wire form.

Scope — deliberately SQLite CreateTable only

Other SQLite ops (AddColumn/DropColumn/CreateIndex/DropIndex/DropTable/RecreateTable) stay on the existing string-build path. They're tracked as Phase 2 of the parent project (projects/migrate-marker-ledger-to-typed-query-ast-commands/). recreateTable still calls the free createTable + renderCreateTableSql from operations/tables.ts — preserved deliberately.

A handful of inline-autoincrement smuggling sites in the planner (tableToDdlParts encodes PRIMARY KEY AUTOINCREMENT into DdlColumn.type, the renderer substring-detects to special-case it) are cross-linked with comments naming the convention; the structural fix is tracked as TML-2866.

Verification

git grep "createTable(this.tableName, this.spec)" → zero matches. Byte-parity test passes for all 12 shapes. renderTypeScript() roundtrip test restored (executes the rendered TS scaffold through tsx and asserts ops.json equals renderOps(calls)). target-sqlite 112 / adapter-sqlite 194 / sqlite facade 46 pass. pnpm fixtures:check green; pnpm lint:deps green.

Alternatives considered

  • Eager codec routing in this slice. Would replace the renderer's type-branching with codec.encode(value) calls directly. Extracted to TML-2867 because the change is cross-target (PG already shipped with the same type-branching shape) and async-propagation touches the framework MigrationPlanWithAuthoringSurface interface — own slice's worth of work that deserves its own framing.
  • Migrate all SQLite ops in one slice. Rejected at project shaping. The Phase 1 / Phase 2 decomposition (projects/.../README.md) lands the planner-adoption pattern across all three targets (PG/SQLite/Mongo) before fanning out to per-target op vocabulary. Slice 5 is the SQLite pioneer.

Refs: TML-2859. Related follow-ups: TML-2860 (slice-4 adapter-sqlite test debt), TML-2862 (pre-commit check for transient project refs), TML-2866 (DdlColumn.type smuggling structural fix), TML-2867 (codec-routed DDL default literals across PG + SQLite).

Summary by CodeRabbit

  • Breaking Changes

    • SQLite migrations: createTable moved from exported function to protected method on Migration base class; call as this.createTable(...) within get operations().
    • Column/constraint builder helpers (col, lit, fn, primaryKey, foreignKey, unique) now exported directly from @prisma-next/sqlite/migration.
  • Improvements

    • Enhanced SQL rendering with proper identifier quoting and improved default value handling (dates, booleans, bigints, JSON).
  • Documentation

    • Added 0.12 → 0.13 upgrade guides for SQLite migration API changes.

wmadden and others added 9 commits June 8, 2026 08:20
…ice 5)

Slice 5 / Phase 1 of the marker/ledger-via-typed-query-AST project. The
first SQLite slice in the planner-adoption arc — proves the planner-adoption
pattern works for SQLite by mirroring slice 4 (TML-2754, Postgres).

- Spec: SQLite CreateTableCall.toOp() builds the slice-1 CreateTable DDL-AST
  node via the contract-free createTable(...) constructor and lowers through
  SqlControlAdapter.lower(), replacing renderCreateTableSql string
  concatenation on the planner's live path. Substrate from slice 4 (the
  constraint node, contract-free constructor, SQLite adapter
  constraint-rendering with 4 byte-parity tests) is fully in place; this
  slice does the SQLite adapter byte-parity reconciliation + planner-side
  migration.
- Plan: three INVEST-shippable dispatches. D1 plumbs Lowerer through the
  SQLite planner stack (mechanical fan-out, no behaviour change); D2
  migrates SqliteCreateTableCall.toOp(lowerer) with a byte-parity test that
  drives toOp(lowerer) end-to-end; D3 adds SqliteMigration.createTable({...})
  authoring method and drops the free createTable re-export from
  @prisma-next/sqlite/migration (mirroring slice 4's PG-facade change).
- Project plan.md: fills in slice 5's Linear reference (was TBD).
- Initial trace events (spec-authored, plan-authored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…fan-out)

Add `import type { Lowerer }` to op-factory-call.ts, render-ops.ts, planner.ts,
and planner-produced-sqlite-migration.ts. Change the abstract
`SqliteOpFactoryCallNode.toOp(): Op` signature to `toOp(lowerer?: Lowerer): Op`
and update all nine concrete `*Call.toOp` signatures to match (each body gains
an ignored `_lowerer?` param — no body change). Update `renderOps` to accept
and forward `lowerer?: Lowerer` via the same `blindCast` pattern used on the
Postgres path. Add `readonly #lowerer: Lowerer` to `SqliteMigrationPlanner` and
`TypeScriptRenderableSqliteMigration`, threading the value from
`createSqliteMigrationPlanner(lowerer: Lowerer)` through both
`TypeScriptRenderableSqliteMigration` constructor call sites. Update both
`createPlanner(adapter)` sites in `control-target.ts` to pass `adapter`. Update
all ten test call sites of `createSqliteMigrationPlanner()` — target-sqlite tests
use an inline `stubLowerer: Lowerer`; adapter-sqlite tests use `createSqliteAdapter()`
or `new SqliteControlAdapter()` (already in scope). Zero bare
`createSqliteMigrationPlanner()` calls remain across the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
D2 of slice 5 halted on a falsified-assumption stop-condition: the
spec assumed the SQLite adapter substrate was byte-parity-ready, but
grounding showed three gaps — the SQLite adapter renderer doesn't
quote identifiers (the planner's `renderCreateTableSql` does, so
adapter-output and planner-output cannot be byte-identical), the
adapter renderer's CREATE TABLE indentation differs from the
planner's (4-space vs 2-space), and `SqliteCreateTableCall` holds
pre-rendered `defaultSql` strings rather than structured `DdlColumn[]`
the way PG's `PostgresCreateTableCall` does after slice 4.

Operator decision: expand slice 5 to fix the SQLite adapter renderer
+ replicate slice 4's `*Call holds DdlColumn[]` refactor on the
SQLite side. Plan goes from 3 dispatches to 4: D1 (lowerer
plumbing — done in `de813cb18`) + D2 (substrate fix: renderer
convention) + D3 (consumer refactor + byte-parity proof) + D4
(authoring API, unchanged from original D3).

Captures the in-flight D1 dispatch brief at
`slices/sqlite-create-table-adoption/dispatches/01-lowerer-plumbing.md`
and a draft D2 brief at
`slices/sqlite-create-table-adoption/dispatches/02-create-table-lower-migration.md`
(written before the stop-condition surfaced; D2 will be re-briefed
after this commit lands and the spec amendment takes effect).

Trace events appended for `spec-amended` / `plan-amended` /
`falsified-assumption`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…able-adoption

Signed-off-by: Will Madden <madden@prisma.io>
The SQLite adapter DDL renderer (`packages/3-targets/6-adapters/sqlite/src/core/ddl-renderer.ts`) was emitting unquoted identifiers and using 4-space/2-space indentation that diverged from both the Postgres adapter renderer and the SQLite planner's `renderCreateTableSql`. This change adds `quoteIdentifier` (from `@prisma-next/target-sqlite/sql-utils`) and `quoteQualifiedIdentifier` to the SQLite renderer, applies them to every identifier reference (table name, column names, constraint names, FK column lists, FK ref-table), and switches indentation to the 2-space/0-space-close form used by the Postgres renderer. Expected-SQL literals in `ddl-create-table-lowering.test.ts` and `ddl-table-constraints-lowering.test.ts` are updated to match; a new test in the constraints file pins mixed-case column names and SQL reserved-word table/constraint names to demonstrate the behaviour the bare-string form could not provide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… (byte-parity)

Mirror slice 4's PG refactor onto SQLite.

`SqliteCreateTableCall` field shape changes from `spec: SqliteTableSpec` (a
flat struct of pre-rendered SQL fragments) to `columns: readonly DdlColumn[]
+ constraints?: readonly DdlTableConstraint[]` (structured AST nodes).
Constructor takes the structured shape directly. `toOp(lowerer)` builds a
typed DDL node via the contract-free `createTable({...})` constructor and
lowers it through `lowerer.lower(...)` — replacing the call to the free
`createTable(tableName, spec)` Op-builder on this path. The free Op-builder
and its `renderCreateTableSql` helper stay in `operations/tables.ts` for
`recreateTable`, which is Phase 2.

The upstream construction site in `issue-planner.ts` now builds the
structured shape: `StorageColumn` → `DdlColumn` instances via a new
`sqliteDefaultToDdlColumnDefault` helper (analogous to PG's
`postgresDefaultToDdlColumnDefault`); table constraints map to
`PrimaryKeyConstraint` / `ForeignKeyConstraint` / `UniqueConstraint`.

`renderTypeScript()` emits `this.createTable({ table, columns: [col(...),
...], constraints: [...] })` — the user-facing authoring form that
`SqliteMigration.createTable` (landing in the next dispatch) will consume.

Byte-parity proof at `adapter-sqlite/test/migrations/create-table-call-byte-parity.test.ts`:
the new test drives `SqliteCreateTableCall.toOp(lowerer)` end-to-end and
asserts byte-identity vs `renderCreateTableSql(tableName, spec)` across
five representative shapes (simple, composite PK, FK with referential
actions, table-level unique, autoincrement-inline-PK).

Roundtrip-test scope:
`render-typescript.roundtrip.test.ts` dropped `CreateTableCall` from its
operation list. The roundtrip evaluates rendered TS by calling `toOp` on
each construction, and CreateTableCall now needs both a Lowerer AND the
`this.createTable` method on the base class to round-trip — the method
doesn't land until the next dispatch. The remaining operations in the
test (AddColumn, CreateIndex, RawSql, …) still round-trip cleanly because
their toOp doesn't depend on either. Restore CreateTableCall coverage
once the migration method is available.

Includes the dispatch briefs for D2 and D3 as project-context artefacts;
the stale D3 brief from before the spec amendment is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…-export drop

Adds `SqliteMigration.createTable({ table, columns, constraints? })` as a protected
instance method, mirroring `PostgresMigration.createTable`. The method builds a
`CreateTableCall` and lowers it through a `SqlControlAdapter<'sqlite'>` stored in the
constructor (materialized from `stack?.adapter`). A companion error function
`errorSqliteMigrationStackMissing` surfaces the misuse when the adapter is absent.

Drops the free `createTable` re-export from the target migration facade
(`packages/3-targets/3-targets/sqlite/src/exports/migration.ts`) and the extension
facade (`packages/3-extensions/sqlite`). The function stays in `operations/tables.ts`
for internal use by `recreateTable`. Adds `col`, `fn`, `lit`, `primaryKey`, `foreignKey`,
and `unique` from `@prisma-next/sql-relational-core/contract-free` to both facades
so authored migration files can import all column-builder helpers from the single
`@prisma-next/sqlite/migration` entrypoint.

Restores `CreateTableCall` coverage in the SQLite adapter roundtrip test now that
the base class method exists. Updates the byte-parity test to use the DDL renderer
directly as the oracle (instead of the now-private free `createTable` op factory).

Records the breaking API change in the 0.12→0.13 upgrade instructions for both user
and extension-author skill packages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
… byte-parity oracle, autoincrement guard

Fix four reviewer findings from the slice-5 DoD pass. The literal-default
renderer now emits 0/1 for booleans, a single-quoted ISO string for Date,
and a bare integer string for bigint (defensive), matching the pre-slice
renderDefaultLiteral output exactly. The byte-parity test is rewritten with
a genuine cross-implementation oracle: each case independently provides a
SqliteTableSpec for the pre-slice createTable path and DdlColumn[] for the
new path, covering all ColumnDefaultLiteralInputValue kinds plus function
defaults. sqliteDefaultToDdlColumnDefault now short-circuits autoincrement()
to undefined at the helper boundary (mirroring the Postgres analogue) instead
of forwarding a FunctionColumnDefault the renderer silently discards. Both
the tableToDdlParts and renderColumn autoincrement sites carry cross-linking
comments describing the type-smuggling convention, referencing TML-2866 for
the structural fix. A tsconfig.test.json with rootDir widened to the monorepo
packages root is added so the cross-package direct-source import in the parity
test typechecks cleanly without relaxing the production tsconfig's rootDir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Trace records D5 (round-2 fixes) dispatch lifecycle: dispatch-start,
round-start, brief-issued, round-end (satisfied), dispatch-end. D4 and
D5 brief artifacts capture the dispatched scope so the slice's audit
trail is complete.

Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wmadden-electric wmadden-electric requested a review from a team as a code owner June 8, 2026 11:21
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@wmadden-electric, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 13 minutes and 44 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 86dde1c8-0767-486a-ac01-9a0dc620134a

📥 Commits

Reviewing files that changed from the base of the PR and between 46485d6 and cbd2cee.

📒 Files selected for processing (2)
  • packages/3-targets/6-adapters/sqlite/src/core/ddl-renderer.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.13-to-0.14/instructions.md
📝 Walkthrough

Walkthrough

This PR refactors the SQLite migration authoring surface by moving createTable from a free-standing function export to a protected method on the SqliteMigration base class, requiring a Lowerer adapter for DDL generation, and introducing contract-free column builders (col, lit, fn, primaryKey, foreignKey, unique) as direct re-exports for migration authoring.

Changes

SQLite Migration API Refactor

Layer / File(s) Summary
Control Adapter & Lowerer Dependency Injection
packages/3-targets/3-targets/sqlite/src/core/control-target.ts, packages/3-targets/3-targets/sqlite/src/core/migrations/planner.ts, packages/3-targets/3-targets/sqlite/src/core/migrations/planner-produced-sqlite-migration.ts, packages/3-targets/3-targets/sqlite/src/core/migrations/render-ops.ts
The migration planner factory now requires a Lowerer parameter and threads it through the entire operation rendering pipeline: createSqliteMigrationPlanner(lowerer)SqliteMigrationPlanner constructor → TypeScriptRenderableSqliteMigrationrenderOps(calls, lowerer). Control target descriptor forwards the SQLite adapter into the planner factory at both call sites.
CreateTableCall DDL Refactoring
packages/3-targets/3-targets/sqlite/src/core/migrations/op-factory-call.ts, packages/3-targets/3-targets/sqlite/src/core/migrations/issue-planner.ts
CreateTableCall now stores DdlColumn[] and optional DdlTableConstraint[] instead of a SqliteTableSpec, and its toOp(lowerer) method requires a lowerer to produce SQL via a contract-free DDL node. renderTypeScript() and importRequirements() now emit scaffolding using col(...), lit(...), fn(...), and constraint builders. The issue planner uses tableToDdlParts(table) to convert storage tables into DDL columns and constraints. All other *Call types accept optional lowerer for interface consistency.
SqliteMigration Base Class & Export Surface
packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts, packages/3-targets/3-targets/sqlite/src/exports/migration.ts, packages/3-targets/3-targets/sqlite/src/core/errors.ts, packages/3-extensions/sqlite/README.md
SqliteMigration now accepts an optional ControlStack<'sql', 'sqlite'>, materializes and stores a SqlControlAdapter, and makes createTable a protected method that throws errorSqliteMigrationStackMissing() if the adapter is undefined. The public exports from @prisma-next/sqlite/migration now include col, lit, fn, primaryKey, foreignKey, unique (contract-free builders) and exclude the createTable free function. README updated to document this change.
DDL Renderer: Identifier Quoting & Default Literals
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/ddl-table-constraints-lowering.test.ts
Table, column, and constraint identifiers are now quoted consistently in DDL SQL. Default literal rendering expanded to handle Date (ISO string), boolean (0/1), bigint (stringified), and null with proper escaping. Tests updated to assert quoted identifiers and correct default representations across column nullability, composite keys, table-level constraints, and default values.
Test Adapter & Lowerer Injection
packages/3-targets/3-targets/sqlite/test/migrations/*, packages/3-targets/6-adapters/sqlite/test/migrations/*
All tests that construct createSqliteMigrationPlanner() now pass an explicit Lowerer or SqliteControlAdapter instance (stubLowerer or createSqliteAdapter()). New test create-table-call-byte-parity.test.ts validates SQL output parity between the new DDL renderer and the pre-slice oracle across column nullability, constraints, autoincrement, and default literal shapes.
Build Configuration & Upgrade Documentation
packages/3-targets/6-adapters/sqlite/package.json, packages/3-targets/6-adapters/sqlite/tsconfig.json, packages/3-targets/6-adapters/sqlite/tsconfig.test.json, skills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.md, skills/upgrade/prisma-next-upgrade/upgrades/0.12-to-0.13/instructions.md
TypeScript typecheck script now validates both main and test TypeScript projects. New tsconfig.test.json added for test file type-checking. Comprehensive upgrade instructions added for both extension authors and end users, documenting the API shift from createTable(...) free function to this.createTable(...) protected method and the direct availability of contract-free builders.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • prisma/prisma-next#763: Modifies SQLite DDL default rendering in the same ddl-renderer.ts file; this PR expands default literal handling in coordination with that work.
  • prisma/prisma-next#760: Updates SQLite migration test wiring to pass SqliteControlAdapter through the planner factory; this PR formalizes that requirement at the API level.
  • prisma/prisma-next#751: Refactors CreateTable lowering through adapter-aware toOp and introduces typed DDL/contract-free nodes; both PRs converge on the same architectural pattern.

Suggested reviewers

  • wmadden
  • aqrln

🐰 A rabbit hops through the SQLite schema so spry,
With col() and lit() helpers passing by,
No more free createTable in sight—
Protected methods keep the adapters tight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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 title accurately and specifically describes the main technical change: SQLite migration planner now produces CREATE TABLE as typed DDL through the adapter, matching the core refactoring work across multiple files.
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-2859-sqlite-create-table-adoption

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.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 152.55 KB (0%)
postgres / emit 120.86 KB (0%)
mongo / no-emit 76.67 KB (0%)
mongo / emit 70.96 KB (0%)
cf-worker / no-emit 181.49 KB (0%)
cf-worker / emit 146.6 KB (0%)

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@768

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: cbd2cee

Comment thread packages/3-extensions/sqlite/test/migration/re-export.test.ts Outdated
Comment thread packages/3-targets/3-targets/sqlite/src/core/migrations/issue-planner.ts Outdated
wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
…lice

Operator decision on the #768 review: the precheck/postcheck verification
SELECTs (raw SQL in every *Call.toOp()) are the project's own "express, don't
concatenate" violation, but converting them is deferred — not absorbed into the
two large in-flight PRs (#768, #794). Carve it out of Phase 2 as an explicit
named slice (typed-migration-verification-queries) and record the substrate
finding: the contract-free builder's projection is column-only, so the slice
must add aggregate (count) + comparison projection first (the AST already has
AggregateExpr.count). Not a defense of the raw SQL — a scheduled removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
wmadden and others added 2 commits June 10, 2026 21:10
…stive default switch

#768 review fixes. Raw-SQL precheck (op-factory-call) deferred to the
typed-migration-verification-queries follow-up slice, per owner decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric marked this pull request as ready for review June 10, 2026 19:14
Keep all release-note entries from both sides of the conflict in
skills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.md:
- HEAD contributed: sqlite-create-table-method
- main contributed: regen-extension-contracts-strip-empty-type-params,
  thread-namespace-id-through-codec-ref-resolver-spi,
  storage-namespace-envelope-re-emit

No entries were contradictory; all were appended entries from independent PRs.

Signed-off-by: Will Madden <madden@prisma.io>
Two new bare as casts were introduced by this branch and flagged by
pnpm lint:casts (delta +2 vs merge base):

1. tableToDdlParts in issue-planner.ts: the parameter type was
   Readonly<Record<...>> but buildColumnTypeSql takes Record<...>.
   Fixed by dropping the Readonly wrapper — no cast needed.

2. sqlite-migration.ts: stack.adapter.create(stack) cast to
   SqlControlAdapter<'sqlite'>. Replaced with blindCast with an
   explicit reason string.

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/tsconfig.test.json (1)

8-8: 💤 Low value

Consider simplifying the include to only test files.

Since the main tsconfig.json already includes and typechecks src/**/*.ts, the test config only needs to cover test/**/*.ts. This would make the separation of concerns clearer:

-  "include": ["src/**/*.ts", "test/**/*.ts"],
+  "include": ["test/**/*.ts"],

TypeScript will still resolve imports from src/ via project references; the explicit src/**/*.ts inclusion is redundant.

🤖 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/tsconfig.test.json` at line 8, Update
the tsconfig.test.json "include" to only reference test files by removing the
redundant "src/**/*.ts" pattern; modify the include array so it contains just
"test/**/*.ts" (adjusting the "include" key in tsconfig.test.json) so tests are
typechecked separately while allowing the main tsconfig to continue covering src
via project references.
packages/3-targets/6-adapters/sqlite/tsconfig.json (1)

8-8: ⚡ Quick win

Clarify the single-file exclusion pattern.

Excluding one specific test file (create-table-call-byte-parity.test.ts) from the main TypeScript config while keeping test/**/*.ts in the include list is an unusual pattern. Given that tsconfig.test.json exists and typechecks all tests with a wider rootDir, consider whether:

  1. All test files should be excluded from the main config and typechecked only via tsconfig.test.json, or
  2. This specific test requires the wider rootDir and the single-file exclusion is intentional (in which case, a comment explaining why would be helpful).

The current setup works but may surprise future maintainers who expect either "all tests in main config" or "all tests in separate test config" rather than a hybrid approach.

🤖 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/tsconfig.json` at line 8, The
tsconfig.json currently excludes only
"test/migrations/create-table-call-byte-parity.test.ts", which is surprising
given tests are otherwise included and there is a separate tsconfig.test.json;
either broaden the exclusion to exclude all tests from the main config (e.g. add
"test/**/*.ts" to the "exclude" array so test files are only typechecked via
tsconfig.test.json) or keep the single-file exclusion but add a clear inline
comment in the same tsconfig.json explaining that this specific test needs the
wider rootDir from tsconfig.test.json and therefore must be excluded here; refer
to the "exclude" property and the filename
"test/migrations/create-table-call-byte-parity.test.ts" when making the change.
🤖 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/sqlite/tsconfig.json`:
- Line 8: The tsconfig.json currently excludes only
"test/migrations/create-table-call-byte-parity.test.ts", which is surprising
given tests are otherwise included and there is a separate tsconfig.test.json;
either broaden the exclusion to exclude all tests from the main config (e.g. add
"test/**/*.ts" to the "exclude" array so test files are only typechecked via
tsconfig.test.json) or keep the single-file exclusion but add a clear inline
comment in the same tsconfig.json explaining that this specific test needs the
wider rootDir from tsconfig.test.json and therefore must be excluded here; refer
to the "exclude" property and the filename
"test/migrations/create-table-call-byte-parity.test.ts" when making the change.

In `@packages/3-targets/6-adapters/sqlite/tsconfig.test.json`:
- Line 8: Update the tsconfig.test.json "include" to only reference test files
by removing the redundant "src/**/*.ts" pattern; modify the include array so it
contains just "test/**/*.ts" (adjusting the "include" key in tsconfig.test.json)
so tests are typechecked separately while allowing the main tsconfig to continue
covering src via project references.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 0c417730-57ea-4656-bbf5-f863a01c91e0

📥 Commits

Reviewing files that changed from the base of the PR and between 0a694de and 46485d6.

⛔ Files ignored due to path filters (10)
  • 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/sqlite-create-table-adoption/dispatches/01-lowerer-plumbing.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sqlite-create-table-adoption/dispatches/02-sqlite-renderer-substrate-fix.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sqlite-create-table-adoption/dispatches/03-create-table-call-refactor.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sqlite-create-table-adoption/dispatches/04-sqlite-migration-create-table-method.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sqlite-create-table-adoption/dispatches/05-reviewer-round-2.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sqlite-create-table-adoption/dispatches/06-review-address.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sqlite-create-table-adoption/plan.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/sqlite-create-table-adoption/spec.md is excluded by !projects/**
  • projects/migrate-marker-ledger-to-typed-query-ast-commands/trace.jsonl is excluded by !projects/**
📒 Files selected for processing (27)
  • packages/3-extensions/sqlite/README.md
  • packages/3-extensions/sqlite/test/migration/re-export.test.ts
  • packages/3-targets/3-targets/sqlite/src/core/control-target.ts
  • packages/3-targets/3-targets/sqlite/src/core/errors.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/issue-planner.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/op-factory-call.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/planner-produced-sqlite-migration.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/planner.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/render-ops.ts
  • packages/3-targets/3-targets/sqlite/src/core/migrations/sqlite-migration.ts
  • packages/3-targets/3-targets/sqlite/src/exports/migration.ts
  • packages/3-targets/3-targets/sqlite/test/migrations/nullability-backfill.test.ts
  • packages/3-targets/3-targets/sqlite/test/migrations/op-factory-call.test.ts
  • packages/3-targets/3-targets/sqlite/test/migrations/planner.authoring-surface.test.ts
  • packages/3-targets/6-adapters/sqlite/package.json
  • 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/ddl-table-constraints-lowering.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/create-table-call-byte-parity.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/planner-introspection.integration.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/planner.codec-field-event.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/planner.test.ts
  • packages/3-targets/6-adapters/sqlite/test/migrations/render-typescript.roundtrip.test.ts
  • packages/3-targets/6-adapters/sqlite/tsconfig.json
  • packages/3-targets/6-adapters/sqlite/tsconfig.test.json
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.12-to-0.13/instructions.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.12-to-0.13/instructions.md
💤 Files with no reviewable changes (1)
  • packages/3-extensions/sqlite/test/migration/re-export.test.ts

wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
New slice extracted from TML-2859 (slice 5) review finding F1. The
DDL default-literal renderer's type-branching on the JS value is a
parallel mini-codec; the runtime parameter-binding path already routes
through Codec.encode(value, ctx). The slice unifies the two by routing
DDL default literals through the same codec, making the chain async
end-to-end.

Spec covers: codec field on LiteralColumnDefault, async propagation
through Lowerer + DDL render + *Call.toOp + MigrationPlanWithAuthoring
Surface.operations getter, wireToDefaultLiteral helper per target,
roll-back of TML-2859 D5's transitional type-branching.

3 dispatches: D1 async interface plumbing (no behaviour change) →
D2 PG renderer codec routing → D3 SQLite renderer codec routing +
D5 roll-back.

Builds on TML-2859 (in PR #768) — needs slice 5 to land before
merging.

Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
…lice

Operator decision on the #768 review: the precheck/postcheck verification
SELECTs (raw SQL in every *Call.toOp()) are the project's own "express, don't
concatenate" violation, but converting them is deferred — not absorbed into the
two large in-flight PRs (#768, #794). Carve it out of Phase 2 as an explicit
named slice (typed-migration-verification-queries) and record the substrate
finding: the contract-free builder's projection is column-only, so the slice
must add aggregate (count) + comparison projection first (the AST already has
AggregateExpr.count). Not a defense of the raw SQL — a scheduled removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ental extension diff

Fixes #768 CI: the SQLite ddl-renderer's function-default handler emitted raw
DEFAULT (now()), which SQLite rejects (e2e widening now() round-trip); add the
datetime('now') mapping its siblings already have. Also declare the incidental
3-extensions diff (deleted re-export test) for check:upgrade-coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric added this pull request to the merge queue Jun 10, 2026
Merged via the queue into main with commit e5ea259 Jun 10, 2026
21 checks passed
@wmadden-electric wmadden-electric deleted the tml-2859-sqlite-create-table-adoption branch June 10, 2026 20:27
wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
New slice extracted from TML-2859 (slice 5) review finding F1. The
DDL default-literal renderer's type-branching on the JS value is a
parallel mini-codec; the runtime parameter-binding path already routes
through Codec.encode(value, ctx). The slice unifies the two by routing
DDL default literals through the same codec, making the chain async
end-to-end.

Spec covers: codec field on LiteralColumnDefault, async propagation
through Lowerer + DDL render + *Call.toOp + MigrationPlanWithAuthoring
Surface.operations getter, wireToDefaultLiteral helper per target,
roll-back of TML-2859 D5's transitional type-branching.

3 dispatches: D1 async interface plumbing (no behaviour change) →
D2 PG renderer codec routing → D3 SQLite renderer codec routing +
D5 roll-back.

Builds on TML-2859 (in PR #768) — needs slice 5 to land before
merging.

Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
…lice

Operator decision on the #768 review: the precheck/postcheck verification
SELECTs (raw SQL in every *Call.toOp()) are the project's own "express, don't
concatenate" violation, but converting them is deferred — not absorbed into the
two large in-flight PRs (#768, #794). Carve it out of Phase 2 as an explicit
named slice (typed-migration-verification-queries) and record the substrate
finding: the contract-free builder's projection is column-only, so the slice
must add aggregate (count) + comparison projection first (the AST already has
AggregateExpr.count). Not a defense of the raw SQL — a scheduled removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
New slice extracted from TML-2859 (slice 5) review finding F1. The
DDL default-literal renderer's type-branching on the JS value is a
parallel mini-codec; the runtime parameter-binding path already routes
through Codec.encode(value, ctx). The slice unifies the two by routing
DDL default literals through the same codec, making the chain async
end-to-end.

Spec covers: codec field on LiteralColumnDefault, async propagation
through Lowerer + DDL render + *Call.toOp + MigrationPlanWithAuthoring
Surface.operations getter, wireToDefaultLiteral helper per target,
roll-back of TML-2859 D5's transitional type-branching.

3 dispatches: D1 async interface plumbing (no behaviour change) →
D2 PG renderer codec routing → D3 SQLite renderer codec routing +
D5 roll-back.

Builds on TML-2859 (in PR #768) — needs slice 5 to land before
merging.

Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wmadden-electric pushed a commit that referenced this pull request Jun 10, 2026
…lice

Operator decision on the #768 review: the precheck/postcheck verification
SELECTs (raw SQL in every *Call.toOp()) are the project's own "express, don't
concatenate" violation, but converting them is deferred — not absorbed into the
two large in-flight PRs (#768, #794). Carve it out of Phase 2 as an explicit
named slice (typed-migration-verification-queries) and record the substrate
finding: the contract-free builder's projection is column-only, so the slice
must add aggregate (count) + comparison projection first (the AST already has
AggregateExpr.count). Not a defense of the raw SQL — a scheduled removal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
paulwer pushed a commit to paulwer/prisma-next that referenced this pull request Jun 11, 2026
…ped path (prisma#794)

## At a glance

A Postgres `timestamptz` column with a `Date` default. **Before**, the
migration planner fell through `defaultVisitor.literal`'s
`JSON.stringify(value)` branch and emitted invalid SQL — `DEFAULT
'"2026-01-01T00:00:00.000Z"'::timestamptz` (JSON quotes inside the
literal); a `bigint` default threw at plan time. **After**, the column's
codec encodes the value and it's inlined correctly:

```sql
CREATE TABLE "config" (
  "expires_at" timestamptz NOT NULL DEFAULT '2026-01-01T00:00:00.000Z'::timestamptz
);
```

That's the visible bug. The PR underneath it does two larger things: it
makes the adapter genuinely route DDL default literals through the
column's **codec** (not a hand-rolled `typeof` ladder), and it moves the
**marker/ledger bootstrap** onto the same typed-AST lowering path — the
parent project's namesake deliverable.

## The decision: a second lowering method, because DDL-lowering is async

The SQL family already had one adapter method, `lower(ast, ctx):
LoweredStatement` — sync. It's sync on purpose: the runtime query path
produces a SQL template with `$N` placeholders and defers codec encoding
to the driver (`encodeParams`), so middleware can mutate raw param
values before they're encoded. That model can't lower a DDL `DEFAULT`
clause: you can't write `DEFAULT $1`, so the codec has to run
**inline**, and `codec.encode` is async.

So the adapter grows a second, independent method:

```ts
interface SqlControlAdapter<T> extends ExecuteRequestLowerer {
  lower(ast, ctx): LoweredStatement;                            // existing — sync, query path, raw params
  lowerToExecuteRequest(ast, ctx): Promise<SqlExecuteRequest>;  // new — async, fully encoded
}
```

`SqlExecuteRequest` is `{ sql, params? }` where `params` are
codec-encoded, driver-ready wire values — the SQL analogue of Mongo's
`MigrationStep.command` (a wire-typed payload the driver executes
directly). It is **not a new type**: the driver port already defined it
(`relational-core/ast/driver-types`), at the correct low layer. This PR
deletes the three redundant near-duplicates that had accreted around the
same `{ sql, params }` shape — `ExecutableStatement`, `DriverStatement`,
`SerializedQueryPlan` — and converges every consumer onto
`SqlExecuteRequest`. Its `params` is optional (a param-less statement is
valid); step-construction sites default to `[]` and execute sites guard
on length.

`lower()` now **rejects DDL on every adapter** (control and runtime) and
points the caller at `lowerToExecuteRequest`. The runtime query path —
`lower()`, `LoweredStatement`, `encodeParams`, the middleware lifecycle
— is otherwise untouched.

## How codec routing actually works

`DdlColumn` gains an optional `codecRef`, mirroring how the query AST's
`ParamRef` already carries one. The planner populates it from the
column's `codecId` (+ `typeParams`). The walker resolves the codec via
the adapter's `codecLookup` and `await`s `codec.encode(value)`, then
inlines the wire result with dialect-correct quoting and (PG)
`::nativeType` cast. Contract-free `col()` calls with no codec follow
the documented `RawSqlLiteral` wire-scalar fallback.

The same encoding discipline now applies to the control path's **query**
params, not just DDL defaults: `lowerToExecuteRequest`'s query branch
codec-encodes its literal params (it previously forwarded raw values),
so the method's "driver-ready params" contract holds for every input.
The Postgres `dataTransform` migration step — the one control path that
lowers real user query ASTs whose literals can be `Date`/`bigint`/JSON —
routes through the same encoded path instead of extracting raw slot
values.

Because `toOp()` now does async work, `*Call.toOp()` returns `Op |
Promise<Op>`, `MigrationPlan.operations` widens to `(Op |
Promise<Op>)[]`, and consumers `await Promise.all(plan.operations)` once
at the boundary. The user-authoring surface is unchanged — `override get
operations() { return [this.createTable(...)] }` stays a plain sync
array literal; the async-ness is invisible to the migration author (`git
diff main -- examples/**/migration.ts` is empty).

## Marker/ledger on the typed path; one DDL walker per adapter

The marker/ledger bootstrap (the sign-marker loop + `lowerAst`, driven
by both runners) now lowers through `lowerToExecuteRequest` instead of
the sync `lower()`. With nothing left calling `lower()` for DDL, the old
`ddl-renderer.ts` (`renderLoweredDdl` + `defaultVisitor` +
constraint/column helpers) is **deleted wholesale** from both adapters.
Net −384 lines; one DDL walker, not two.

## A latent regression this surfaced — relevant to prisma#768

The e2e suite caught `unknown function: now()` on SQLite. The contract
canonicalizes `CURRENT_TIMESTAMP` → `now()`; the old flat-spec renderer
mapped it back to `datetime('now')`, but the new walker (which
`SqliteCreateTable` moved onto **in slice 5 / prisma#768**) emitted raw
`DEFAULT (now())`, which real SQLite rejects. It's been latent since
slice 5. Fixed here in the SQLite walker (the dialect authority), pinned
by a unit test and the additive/widening e2e. **Heads-up: prisma#768 carries
the same latent bug until this lands on top of it.**

## Honest caveat: codec routing has no observable effect on shipping
codecs yet

For every builtin codec (text, uuid, jsonb, timestamptz, int, …)
`encode` produces output byte-identical to the old type-branching —
which is why no migration golden regenerated and `fixtures:check` is
clean. Routing only diverges for a *transforming* codec (encryption, a
custom domain), none of which ship today. The proof that routing works
is therefore a unit test with a synthetic transforming codec
(`'plaintext'` → `'ENC:PLAINTEXT'`, raw value absent from the SQL) on
both targets. The behaviour is correct-by-construction; its payoff is
prospective (it's what makes a CipherStash-style encrypted column
default possible).

## Stacking

This branch is **stacked on prisma#768** (slice 5, still open) and targets
`main`, so the GitHub diff includes slice 5. The TML-2867 change itself
is ~98 files since the slice-5 tip; review from commit `330dccc82`
(`TML-2867 D1`) onward, or once prisma#768 merges this will rebase to a clean
diff.

## Verification

`pnpm typecheck` green · `pnpm test:packages` 9950 · `pnpm test:e2e` 105
· `pnpm test:integration` green (the one failure is a pre-existing PG
`portal "C_n" does not exist` concurrent-query flake — passes in
isolation) · `pnpm fixtures:check` green (no golden drift; constraint +
cast SQL verified byte-identical between the deleted renderer and the
new walker via the migrated `.toBe()` assertions) · `pnpm lint:deps`
green · `pnpm lint:casts` delta 0.

## Alternatives considered

- **Make `lower()` itself async / async-everywhere.** *Deferred, not
rejected on the merits.* The runtime middleware lifecycle is already
async, so an async `lower()` wouldn't break it — the middleware seam is
a pipeline *ordering* (`lower → beforeExecute → encodeParams → driver`),
not a sync/async boundary. Two reasons we used a second method instead:
(1) **blast radius** — `lower()` is on the query hot path (every
`runtime.execute`), so making it async ripples through the whole
execution path, well beyond this slice's scope; (2) the **output shapes
legitimately differ** — `lower()` returns *raw* params on purpose,
because `beforeExecute`'s param mutator rewrites raw values *before*
`encodeParams` (the CipherStash case: middleware turns plaintext into
what gets encoded). A control-plane lowering has no middleware, so it
encodes inline. The cleaner long-term shape is the runtime-symmetry
follow-up (`M-Arch-1` in the review artifacts): keep `lower → middleware
→ encode`, but expose the *post-encode* runtime result as
`SqlExecuteRequest` too, unifying the wire type across both planes.
Worth doing; out of scope here.
- **Eager-materialize `Op[]` at planner-construction time so
`operations` stays sync.** Works for planner-produced migrations but not
user-authored ones (`override get operations()` calls
`this.createTable(...)` lazily). Returning `(Op | Promise<Op>)[]` and
awaiting at the boundary handles both uniformly.
- **Access the lowerer via the Migration's stack instead of
`toOp(lowerer)`.** Considered (maintainer raised it); rejected to keep
`*Call` instances executable independently of any Migration context —
recorded as a positive constraint in the review artifacts.

Refs: TML-2867. Builds on prisma#768 (TML-2859, slice 5). Review artifacts
(architect / principal-engineer / walkthrough passes +
maintainer-comment resolutions) under
`projects/migrate-marker-ledger-to-typed-query-ast-commands/slices/codec-routed-ddl-defaults/`.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Breaking Changes**
* SQLite migrations: createTable is now a protected Migration method —
use this.createTable(...) inside get operations().
* Migration operations may be produced asynchronously; callers/tooling
should await resolved operations.

* **New Features**
* Migration helpers (col, lit, fn, primaryKey, foreignKey, unique)
exported from the SQLite migration entry.
* Adapters now provide async lowering into driver-ready execute
requests.

* **Improvements**
* Execute steps include explicit params arrays; planners, runners, and
tests updated for async resolution and codec-aware encoding.
<!-- 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>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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