Skip to content
Conduit

instructions

0.13 → 0.14 — User upgrade instructions

<! TML 2867: codec routed DDL defaults. The migration planner now resolves each plan operation lazily (operations are Promise<Op [] ), and DDL execute steps carry a params array. The example migration

Set up

Skills are installed by placing their folder where your agent loads skills.

Part of the collection prisma/prisma-next.

Copy the text of instructions.md into the instructions file your agent reads (CLAUDE.md, AGENTS.md, .cursor/rules, …). Written for AI coding agents in general (from where the project keeps it; we have not run it).

README

synced Oct 14, 2026

<! TML 2867: codec routed DDL defaults. The migration planner now resolves each plan operation lazily (operations are Promise<Op [] ), and DDL execute steps carry a params array. The example migration fixtures ( prisma next demo , prisma next postgis demo ) were regenerated to reflect the added para

0.13 → 0.14 — User upgrade instructions

uuid-preset-rename

The uuid field preset names now include the storage encoding suffix:

BeforeAfter
field.uuid()field.uuidString()
field.id.uuidv4()field.id.uuidv4String()
field.id.uuidv7()field.id.uuidv7String()

These presets store UUIDs as char(36) strings and work across all SQL targets. If you want the Postgres-native uuid column type instead, use field.uuidNative() / field.id.uuidv4Native() / field.id.uuidv7Native() from @prisma-next/postgres/contract-builder.

The rename is mechanical. Run the colocated script or apply the following find-and-replace in your contract.ts (or wherever you use the field builder):

// Before
id: field.id.uuidv7(),
userId: field.id.uuidv4(),
externalId: field.uuid(),

// After
id: field.id.uuidv7String(),
userId: field.id.uuidv4String(),
externalId: field.uuidString(),

No change to contract.json — both the old and new preset names emit the same codec (sql/char@1), so existing emitted contracts remain valid.

qualify-flat-builder-accessors

The query builder and ORM client are now always qualified by namespace. The flat by-bare-name accessors are gone: there is no sql.<table> and no orm.<Model> at the builder layer, and the Postgres facade exposes the qualified surface (db.sql / db.orm are the namespace map). You reach a table or model by naming its namespace.

Namespace selection separates which namespace's table from the ergonomic shorthand for the single-namespace case. The builder layer always names the namespace; the single-namespace shorthand is recovered by the facade on targets that have only one namespace (SQLite, Mongo).

Who needs to change code

Postgres projects that build queries through the facade or the builder outputs. A standard Postgres project keeps its tables and models in the public schema, so the namespace to insert is public:

// Before
const users = await db.sql.user.select('id', 'email').build().execute();
const alice = await db.orm.User.find({ where: { id } });

// After — name the namespace the table/model is declared in (`public` for a standard schema)
const users = await db.sql.public.user.select('id', 'email').build().execute();
const alice = await db.orm.public.User.find({ where: { id } });

The same rule applies inside a transaction (tx.sql.public.user, tx.orm.public.User), inside a prepare(...) callback ((sql) => sql.public.user…), and to code that imports the builder outputs directly rather than through the facade (sql.public.user, orm.public.User). If your Postgres contract declares more than one namespace, name the namespace each table/model actually sits in — db.sql.auth.user for a table in the auth schema, db.sql.public.profile for one in public.

Who does not need to change anything

SQLite and Mongo projects. These targets have a single namespace, so their facade still exposes the flat surface — db.sql.<table> and db.orm.<Model> keep working unchanged. No edits are required.

How to migrate

There is no codemod, because the correct namespace is the one each table or model is declared in — a fact that lives at the call site, not in a mechanical rule. For each flagged file:

  1. If the project's facade is SQLite or Mongo (sqlite(...) / mongo(...)), leave it unchanged.
  2. If it is Postgres (postgres(...)), insert the namespace segment after .sql / .orm (and on direct sql / orm builder calls): use public for a standard single-schema project, or the specific schema name for each table/model in a multi-schema contract.

After migrating, run your project's pnpm typecheck (or equivalent) — a missed site is a compile error (Property '<table>' does not exist on type 'Db<…>'), so the type checker pins every remaining flat access for you.

sql-runtime-base-class-naming

The SQL runtime class hierarchy now follows the repo naming convention:

  • SqlRuntime (previously exported) → now SqlRuntimeBase (abstract family base)
  • PostgresRuntime (previously a class) → now an interface (the type to depend on); the concrete class is PostgresRuntimeImpl
  • SqliteRuntime (previously a class) → now an interface (the type to depend on); the concrete class is SqliteRuntimeImpl

App code using the facade factories (postgres(...), sqlite(...)) is unaffected — those return Runtime / the interface. Only code that referenced the class names directly needs to change:

// Before — referencing the class as a type
import { PostgresRuntime } from '@prisma-next/postgres/runtime';
function takesRuntime(r: PostgresRuntime) { ... }

// After — use the interface (same import path)
import type { PostgresRuntime } from '@prisma-next/postgres/runtime';
function takesRuntime(r: PostgresRuntime) { ... }

// Before — subclassing
import { PostgresRuntime } from '@prisma-next/postgres/runtime';
class MyRuntime extends PostgresRuntime { ... }

// After — subclass the Impl
import { PostgresRuntimeImpl } from '@prisma-next/postgres/runtime';
class MyRuntime extends PostgresRuntimeImpl { ... }

create-runtime-removed

createRuntime is removed from @prisma-next/sql-runtime. App code using the facade factories (postgres(...), sqlite(...)) is unaffected — those still return a Runtime as before. Only code that imported and called createRuntime directly needs to change.

Replace direct createRuntime calls with the appropriate target class constructor or factory:

// Before
import { createRuntime } from '@prisma-next/sql-runtime';
const runtime = createRuntime({ stackInstance, context, driver, ...opts });

// After — use the target factory (recommended for app code)
import { postgres } from '@prisma-next/postgres';
const db = postgres({ contract, ...opts });
// runtime is accessed via db.connect() / db.runtime() etc.

// Or construct the target class directly (for advanced/test use)
import { PostgresRuntimeImpl } from '@prisma-next/postgres/runtime';
const runtime = new PostgresRuntimeImpl({ adapter: stackInstance.adapter, context, driver, ...opts });

The constructor options are identical to what createRuntime accepted, except stackInstance is not taken: pass adapter from stackInstance.adapter directly.

migration-op-factories-to-methods

The bare op factory functions previously exported from @prisma-next/postgres/migration (and the deprecated @prisma-next/target-postgres/migration alias) are removed. Each function is now a protected method on the PostgresMigration base class — call it as this.<method>(...) inside your Migration subclass body.

The option shapes also changed: positional arguments are replaced by a single options object.

Remove the bare names from your import and replace each call-site:

Before (bare function)After (method)
dropColumn(schema, table, column)this.dropColumn({ schema, table, column })
setNotNull(schema, table, column)this.setNotNull({ schema, table, column })
setDefault(schema, table, column, defaultSql)this.setDefault({ schema, table, column, defaultSql })
addPrimaryKey(schema, table, name, columns)this.addPrimaryKey({ schema, table, constraint: name, columns })
addForeignKey(schema, table, { name, columns, references, onDelete })this.addForeignKey({ schema, table, foreignKey: { name, columns, references, onDelete } })
addCheckConstraint(schema, table, name, column, values)this.addCheckConstraint({ schema, table, constraint: name, column, values })
createIndex(schema, table, indexName, columns)this.createIndex({ schema, table, index: indexName, columns })
installExtension({ id, extensionName, invariantId })this.installExtension({ id, extensionName, invariantId })

Example:

// Before
import { addForeignKey, createIndex, dropColumn } from '@prisma-next/postgres/migration';

override get operations() {
  return [
    dropColumn('public', 'user', 'legacyName'),
    addForeignKey('public', 'post', {
      name: 'post_userId_fkey',
      columns: ['userId'],
      references: { schema: 'public', table: 'user', columns: ['id'] },
    }),
    createIndex('public', 'post', 'post_userId_idx', ['userId']),
  ];
}

// After
import { Migration, MigrationCLI } from '@prisma-next/postgres/migration';

override get operations() {
  return [
    this.dropColumn({ schema: 'public', table: 'user', column: 'legacyName' }),
    this.addForeignKey({
      schema: 'public',
      table: 'post',
      foreignKey: {
        name: 'post_userId_fkey',
        columns: ['userId'],
        references: { schema: 'public', table: 'user', columns: ['id'] },
      },
    }),
    this.createIndex({ schema: 'public', table: 'post', index: 'post_userId_idx', columns: ['userId'] }),
  ];
}

The colocated script applies this transformation automatically. Run it from your project root:

pnpm exec tsx node_modules/.skills/prisma-next-upgrade/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts

postgres-contract-serializer

SqlContractSerializer (from @prisma-next/family-sql/ir) now rejects Postgres contracts. The family serializer validates entries against a registry of known entity kinds; it only knows the SQL-family built-ins (table, valueSet) and has no knowledge of the Postgres-specific type key (Postgres enum types). Every Postgres namespace carries "type": {} in its entries, so the family serializer throws a ContractValidationError naming type as an unregistered kind.

Replace SqlContractSerializer with PostgresContractSerializer in any migration file or app code that deserializes a Postgres-emitted contract:

// Before
import { SqlContractSerializer } from '@prisma-next/family-sql/ir';
const contract = new SqlContractSerializer().deserializeContract(contractJson) as Contract;

// After
import { PostgresContractSerializer } from '@prisma-next/target-postgres/runtime';
const contract = new PostgresContractSerializer().deserializeContract(contractJson) as Contract;

SQLite and family-only (non-Postgres) contracts are unaffected — their namespaces carry only table entries, which the family serializer knows about.

enum-becomes-domain-concept

The enum keyword changed meaning. Before 0.14 a PSL enum block authored a native Postgres enum (CREATE TYPE <name> AS ENUM (…), columns typed with the named type). Starting at 0.14 the same keyword authors the domain enum: the column stores plain values through a declared codec (typically pg/text@1 → a text column) and the value set is enforced by a CHECK constraint the migration planner generates and verifies. The native enum machinery (the pg/enum@1 codec, native CREATE TYPE planning, native-enum introspection adoption) is deleted.

Who needs to change code

Any project whose .prisma schema contains an enum block without an @@type(...) attribute (the old native form), or with @map on members, or whose schema uses the transitional enum2 keyword. Projects that already author enums with @@type + member values (the enum2-era shape introduced in 0.13) only need the keyword rename described below — the emitted contract is identical.

1. Convert the schema syntax

// Before — native enum (0.13)
enum user_type {
  admin
  user
}

// After — domain enum (0.14)
enum user_type {
  @@type("pg/text@1")
  admin = "admin"
  user  = "user"
}

Rules:

  • @@type("<codec-id>") is required. For string-valued enums use @@type("pg/text@1").
  • Each member maps to its database value with member = "value". Under the native semantics the stored label was the member name, so a faithful conversion sets each value to the member's name (admin = "admin"). A member that previously carried @map("dbvalue") becomes member = "dbvalue" — @map on enum members is removed; the member value is the mapping.
  • If your schema uses the transitional enum2 keyword (added in 0.13), rename enum2 → enum. Nothing else changes — that block shape is exactly what enum now means.

If you author contracts in TypeScript instead of PSL: the native enumType(name, values[]) and enumColumn(...) helpers from @prisma-next/adapter-postgres/column-types are deleted. Author the domain enum with enumType + member from your target's contract-builder and return it under the enums key:

import { defineContract, enumType, member } from '@prisma-next/postgres/contract-builder';

const pgText = { codecId: 'pg/text@1', nativeType: 'text' } as const;
const UserType = enumType('user_type', pgText, member('admin', 'admin'), member('user', 'user'));

export const contract = defineContract({ /* … */ }, ({ field, model }) => ({
  enums: { user_type: UserType },
  models: {
    User: model('User', {
      fields: { /* … */ kind: field.namedType(UserType) },
    }),
  },
}));

Then re-emit: prisma-next contract emit. The emitted contract carries the enum as a domain entity plus a storage valueSet; the column becomes pg/text@1 / text with a valueSet reference and a table-level check entry.

2. Migrate the database off the native type

A database created under 0.13 still has the native enum type and columns typed with it. Author a one-time converting migration — for each native enum type, in order:

  1. Alter each column off the native type, casting the stored labels: ALTER TABLE … ALTER COLUMN <col> TYPE text USING <col>::text.
  2. Add the value-set CHECK constraint the contract now declares (name it as the contract does, e.g. <table>_<col>_check).
  3. Drop the native type: DROP TYPE "<schema>"."<type>".

Because the contract hash does not change (the schema conversion in step 1 and the emitted contract are the end state), scaffold the migration as a data-only edge on the current hash: prisma-next migration new --name convert-<type>-to-value-set --from <current-storage-hash>, give the ALTER op operationClass: 'data', and self-emit by running the scaffolded migration.ts. The DROP TYPE has no op builder — express it as an inline rawSql op.

A complete worked example ships in the Prisma Next repo: examples/prisma-next-demo/migrations/app/20260611T1856_convert_user_type_to_value_set/migration.ts — three ops (data-class ALTER … USING, addCheckConstraint, rawSql DROP TYPE), each with pre/postchecks that make replay idempotent.

Note: prisma-next contract infer refuses databases containing native enum types — it names each offending type and points at this conversion. Convert the database first, then infer.

3. Verify

Run prisma-next db verify (or your project's test suite) after applying the converting migration: the live schema must now match the contract — text column, CHECK constraint present, native type gone.

generated-models-export-removed

The generated contract.d.ts no longer emits the flat top-level export type Models (the first-name-wins map of every model across namespaces). Models now resolve per-namespace from the domain plane, matching how the runtime and DSL read them.

If your code imported Models from the generated contract, read a namespace's models instead:

// Before
import type { Contract, Models } from './prisma/contract';
type UserModel = Models['User'];

// After — name the namespace the model is declared in
import type { Contract } from './prisma/contract';
type Models = Contract['domain']['namespaces']['public']['models'];
type UserModel = Models['User'];

Use public for a standard single-schema Postgres project, or __unbound__ for SQLite and Mongo. In a multi-schema Postgres contract, name the schema each model is declared in. Re-emit your contract (prisma-next contract emit) so the generated .d.ts drops the Models export; the emitted contract.json is unchanged.

↑↓ navigate openesc close