Skip to content

feat(sqlite): add migration planner, runner, and introspection#341

Draft
SevInf wants to merge 3 commits intomainfrom
feat/sqlite-migrations
Draft

feat(sqlite): add migration planner, runner, and introspection#341
SevInf wants to merge 3 commits intomainfrom
feat/sqlite-migrations

Conversation

@SevInf
Copy link
Copy Markdown
Contributor

@SevInf SevInf commented Apr 15, 2026

closes TML-2168

Intent

Complete the SQLite target's control plane — migration planner, runner, schema introspection, and PSL type mappings — so that db init / db update work against SQLite databases using the same contract-driven workflow as Postgres.

Change map

Area What changed Why
Migration planner New SqliteMigrationPlanner that diffs contract against live schema and emits additive operations (CREATE TABLE, ADD COLUMN, CREATE INDEX, FK-backing indexes) SQLite needs its own planner because DDL syntax and ALTER TABLE semantics differ from Postgres
DDL builders buildCreateTableSql, buildAddColumnSql, buildCreateIndexSql with SQLite-specific rendering (AUTOINCREMENT, inline FKs, type affinities, CAST-based defaults) Renders the actual SQL that the runner executes
Migration runner New SqliteMigrationRunner with exclusive-transaction locking, precheck/execute/postcheck lifecycle, idempotency (skip if postchecks already pass), marker/ledger bookkeeping, post-migration schema verification SQLite has no advisory locks — uses BEGIN EXCLUSIVE instead. Control tables use TEXT instead of JSONB/TIMESTAMPTZ
Control tables _prisma_marker and _prisma_ledger with SQLite-compatible types (TEXT for JSON, TEXT for timestamps via datetime('now')) Mirrors the Postgres marker/ledger pattern adapted for SQLite's type system
Schema introspection SqliteControlAdapter.introspect() queries sqlite_master + PRAGMAs (table_info, foreign_key_list, index_list, index_info) to build SqlSchemaIR Required for both the planner's diff and the runner's post-migration verification
Default parsing parseSqliteDefault normalizes SQLite's stored defaults (strips parens, maps datetime('now')now(), handles numeric/string/null literals) Enables schema verification to compare introspected defaults against contract defaults
PSL type mappings control-mutation-defaults.ts maps all PSL scalars to SQLite native types/codecs and implements default function lowering (autoincrement, now, uuid, cuid, ulid, nanoid, dbgenerated) Required for target = "sqlite" in PSL to emit correct contracts
Control target wiring SqlControlTargetDescriptor now exposes migrations.createPlanner(), createRunner(), contractToSchema() Plugs the new components into the framework's control plane API

The story

The SQLite target already had a working runtime plane (driver, adapter, codecs, query builder, ORM). This commit adds the control plane — the last major piece before SQLite is a fully functional target.

Planner (planner.ts): Walks the contract's tables, columns, indexes, and foreign keys. For each object missing from the live schema, it emits an operation with precheck ("does not exist"), execute (DDL), and postcheck ("now exists"). DDL rendering is delegated to planner-ddl-builders.ts which handles SQLite-specific syntax: INTEGER PRIMARY KEY AUTOINCREMENT for auto-increment PKs, inline FOREIGN KEY constraints with referential actions, CAST-based default rendering, and safe native-type validation.

Runner (runner.ts): Wraps the entire migration in BEGIN EXCLUSIVE (SQLite's equivalent of advisory locks), creates control tables if needed, checks marker compatibility, applies operations with idempotency support (postchecks satisfied → skip), verifies the resulting schema against the contract, then upserts the marker and records a ledger entry. All within a single transaction — if anything fails, everything rolls back.

Introspection (control-adapter.ts): Reads sqlite_master for table names, then for each table queries PRAGMA table_info (columns, PKs), PRAGMA foreign_key_list (FKs with referential actions), and PRAGMA index_list/index_info (indexes and unique constraints, classified by origin). Produces the standard SqlSchemaIR that the planner and runner consume.

PSL mappings (control-mutation-defaults.ts): Maps PSL scalar types → SQLite affinities (e.g. Boolean → integer with boolean codec, DateTime → text with datetime codec, Json → text with json codec). Implements all default function lowering handlers with proper argument validation and diagnostic reporting.

Behavior changes & evidence

Change Evidence
createPlanner() / createRunner() are now callable on the SQLite target descriptor runner-integration.test.ts — full lifecycle: plan from empty → run → verify tables exist → re-plan → idempotent re-run
Planner generates correct CREATE TABLE DDL with inline PKs, FKs, uniques, defaults planner-ddl-builders.test.ts — 23 tests covering all DDL shapes
Runner handles errors gracefully (precheck failure, execution failure, policy violation, marker mismatch, destination mismatch) runner.errors.test.ts — 7 error scenarios
Runner is idempotent — re-running the same plan skips already-applied operations runner.idempotency.test.ts — 2 idempotency scenarios
Introspection round-trips: create schema → introspect → verify IR matches control-adapter.test.ts — 20 tests covering tables, columns, PKs, FKs, indexes, uniques, defaults

Compatibility / migration / risk

  • No breaking changes to existing packages — all new code is additive within the sqlite packages.
  • New dependencies on @prisma-next/family-sql, @prisma-next/sql-schema-ir, @prisma-next/sql-errors, @prisma-next/utils added to target-sqlite's package.json.
  • SQLite ALTER TABLE limitations: The planner currently only supports additive operations (CREATE TABLE, ADD COLUMN, CREATE INDEX). Destructive operations (drop column, change type) via the recreate-table pattern are deferred to a follow-up.

Non-goals / intentionally out of scope

  • :memory: database support
  • sql-lane (legacy) and kysely-lane SQLite support
  • Migration DOWN operations
  • WAL mode configuration

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 15, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: dcc5a4bc-25a2-445b-8bea-63dc29bd597e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sqlite-migrations

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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 15, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@341

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@341

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-pipeline-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-pipeline-builder@341

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: d84caac

SevInf added 2 commits April 20, 2026 18:38
Implement the SQLite migration control plane (Milestone 4):

Adapter (control-mutation-defaults.ts):
- PSL scalar type → SQLite native type mappings
- Default function registry (autoincrement, now, uuid, etc.)

Adapter (control-adapter.ts):
- SqliteControlAdapter with introspect() via sqlite_master + PRAGMA
- parseSqliteDefault() normalizer returning ColumnDefault
- normalizeSqliteNativeType() for schema verification

Target (migrations/):
- Planner generating CREATE TABLE, ADD COLUMN, CREATE INDEX ops
- DDL builders with SQLite dialect (AUTOINCREMENT, inline FK/UNIQUE)
- Runner with BEGIN EXCLUSIVE transactions, control tables
  (_prisma_marker, _prisma_ledger) using TEXT instead of JSONB
- Statement builders with ? params instead of $1

Tests:
- 88 adapter tests (introspection, default parsing, codecs)
- 41 target tests (DDL builders, planner round-trips, runner
  lifecycle with real file-backed databases, idempotency,
  error scenarios matching Postgres test patterns)
Extend the SQLite migration planner with full reconciliation support:
- Widening: recreate-table for nullability relaxation, default changes
- Destructive: DROP TABLE, DROP INDEX, DROP COLUMN, recreate-table for
  type changes, nullability tightening, constraint changes
- Conflict reporting with granular kinds (typeMismatch,
  nullabilityConflict, indexIncompatible, foreignKeyConflict)

Add e2e migration tests in the e2e package using the contract authoring
DSL (defineContract/model/field) covering additive, schema evolution,
destructive, widening, and recreate-table operations.
@SevInf SevInf force-pushed the feat/sqlite-migrations branch from 3da2d59 to b8527ee Compare April 20, 2026 16:40
The SQLite recreate-table flow (used for widening/destructive column
changes) drops and rebuilds the table. Two problems made this unsafe:

- FK-backing indexes were not re-emitted, so verification failed after
  recreate when the contract declared an FK with index:true.
- foreign_keys was left ON during the migration, so DROP TABLE on a
  referenced parent cascade-deleted rows in child tables.

The planner now re-emits FK-backing indexes alongside declared indexes
when building recreate-table operations. The runner turns foreign_keys
OFF before BEGIN EXCLUSIVE (the pragma is a no-op inside a transaction),
runs PRAGMA foreign_key_check before commit, and restores the pragma in
an outer finally. A new FOREIGN_KEY_VIOLATION error code surfaces any
violations detected by the integrity check.

Also fixes the adapter so the test suite can load: controlMutationDefaults
was a thunk but the framework expects a plain object (matches postgres);
scalarTypeDescriptors was nested under a nonexistent pslTypeDescriptors
field; and control-mutation-defaults.ts imported type names that do not
exist in family-sql (they live in framework-components).

Tests added in test/e2e/framework/test/sqlite/migrations.test.ts:
- preserves FK when the child table (holder of the FK) is recreated
- preserves FK when the parent (referenced) table is recreated
- preserves declared indexes when the table is recreated
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.

1 participant