prisma-8

Use when working in a project that depends on @prisma/orm-postgres, @prisma/orm-sqlite, or @prisma/orm-mongo (Prisma 8, formerly Prisma Next): editing contract.prisma or a contract.ts builder, running `prisma contract emit`, planning or applying migrations, editing migration.ts, writing db.orm / db.sql / db.query queries, wiring db.ts or middleware, integrating a build tool, using the Supabase extension or RLS, or reading a dotted error code such as MIGRATION.HASH_MISMATCH. Use when the user asks "what is Prisma 8", "where do I start", or compares it to another ORM. Use when the user asks to upgrade or bump Prisma 8 in an app or an extension package. Use when you see @internal/* or @prisma/orm-* imports, prisma.config.ts with definePrismaConfig, or contract.json / contract.d.ts. Do not use for Prisma ORM 7 or earlier (schema.prisma + @prisma/client).

Install
npx skills add 'https://github.com/prisma/orm/tree/main/skills/prisma-8'
Download bundle ↓
main · fac8604Scanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

upgrading/app/upgrades/0.13-to-0.14/instructions.md

upgrading/app/upgrades/0.13-to-0.14/instructions.mdBrowse 76 files
View on GitHub
← Back to SKILL.md

from: "0.13" to: "0.14" changes:

  • id: uuid-preset-rename summary: | The uuid field presets are renamed: field.uuid()field.uuidString(), field.id.uuidv4()field.id.uuidv4String(), field.id.uuidv7()field.id.uuidv7String(). These names now describe the storage encoding (char(36) string). Postgres-native uuid storage uses the new field.uuidNative() / field.id.uuidv4Native() / field.id.uuidv7Native() presets from @internal/postgres/contract-builder. detection: glob: "**/*.ts" contains: - "field.uuid()" - "field.id.uuidv4()" - "field.id.uuidv7()" anyMatch: true script: uuid-preset-rename.ts
  • id: qualify-flat-builder-accessors summary: | The builder-layer flat accessors are removed: the query builder and ORM client now expose per-namespace facets only, and the Postgres facade exposes the qualified surface. Code that builds queries against a Postgres (multi-namespace) contract must name the namespace the table/model is declared in: db.sql.<table> becomes db.sql.<namespace>.<table> and db.orm.<Model> becomes db.orm.<namespace>.<Model> (for a standard single-schema Postgres project the namespace is public). Code that calls the builder outputs directly migrates the same way: sql.<table>sql.<namespace>.<table>, orm.<Model>orm.<namespace>.<Model>. SQLite and Mongo projects are unaffected — their single-namespace facade keeps flat db.sql.<table> / db.orm.<Model> working. There is no codemod: the correct namespace is the one each table/model is declared in, which is call-site-specific. detection: glob: "**/*.{ts,tsx}" contains: - "db.sql." - "db.orm." anyMatch: true
  • id: sql-runtime-base-class-naming summary: | @internal/sql-runtime now exports abstract class SqlRuntimeBase (previously SqlRuntime) — the family-layer subclass seam. Target classes are now named with Impl suffix: PostgresRuntimeImpl and SqliteRuntimeImpl. The bare names PostgresRuntime and SqliteRuntime are now interfaces — the correct types to depend on in extension and app code. App code using the facade factories (postgres(...), sqlite(...)) is unaffected. detection: glob: "**/*.{ts,tsx}" contains: - "SqlRuntime" - "PostgresRuntime" - "SqliteRuntime" anyMatch: true
  • id: create-runtime-removed summary: | createRuntime is removed from @internal/sql-runtime. Use the target factory (postgres(...) / sqlite(...)) or construct the target class directly: new PostgresRuntimeImpl({...}) from @internal/postgres/runtime, new SqliteRuntimeImpl({...}) from @internal/sqlite/runtime. App code using the facade factories (postgres(...), sqlite(...)) is unaffected. detection: glob: "**/*.{ts,tsx}" contains: - "createRuntime"
  • id: migration-op-factories-to-methods summary: | The bare migration op factory functions are removed from @internal/postgres/migration. Replace each import and call-site with the corresponding method on this inside your Migration subclass. The option shapes changed from positional arguments to a single options object. detection: glob: "**/migration.ts" contains: - "from '@internal/postgres/migration'" - "from '@internal/target-postgres/migration'" anyMatch: true script: migration-op-factories-to-methods.ts
  • id: postgres-contract-serializer summary: | SqlContractSerializer (from @internal/family-sql/ir) can no longer deserialize Postgres contracts. The family serializer has an empty entries registry and now rejects the type key that every Postgres namespace carries. Any migration file or app code that calls new SqlContractSerializer().deserializeContract(postgresContractJson) must switch to new PostgresContractSerializer() imported from @internal/target-postgres/runtime. detection: glob: "**/*.{ts,tsx}" contains: - "SqlContractSerializer"
  • id: enum-becomes-domain-concept summary: | The PSL enum keyword now authors the domain enum (a text-class column whose value set is enforced by a CHECK constraint) — the native Postgres CREATE TYPE … AS ENUM semantics are gone. An enum block must carry @@type("<codec-id>") (e.g. @@type("pg/text@1")), members map to database values with Name = "value" (a bare member name defaults to itself where the codec accepts it), and @map on members is removed — the member value IS the mapping. The transitional enum2 keyword is retired; rename those blocks to enum (emitted contract is identical). The TS authoring equivalent is enumType(name, codecRef, ...member(name, value)) from @internal/postgres/contract-builder returned under the contract's enums key; the old native enumType(name, values[]) / enumColumn from @internal/adapter-postgres/column-types are deleted. Databases that already carry a native enum type need a one-time converting migration (alter column to text USING ::text, add the value-set CHECK, DROP TYPE) — contract infer refuses native enum types by design and names them in its diagnostic. detection: glob: "**/*.prisma" contains: - "enum " - "enum2 " anyMatch: true
  • id: generated-models-export-removed summary: | The generated contract.d.ts no longer emits the flat top-level export type Models. Models resolve per-namespace from the domain plane: replace a reference to the generated Models with Contract['domain']['namespaces']['<namespace>']['models'] (use public for a standard single-schema Postgres project, __unbound__ for SQLite or Mongo). Re-emit the contract to drop the export. detection: glob: "**/*.{ts,tsx}" contains: - "Models"

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 @internal/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 '@internal/postgres/runtime';
function takesRuntime(r: PostgresRuntime) { ... }

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

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

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

create-runtime-removed

createRuntime is removed from @internal/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 '@internal/sql-runtime';
const runtime = createRuntime({ stackInstance, context, driver, ...opts });

// After — use the target factory (recommended for app code)
import { postgres } from '@internal/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 '@internal/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 @internal/postgres/migration (and the deprecated @internal/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 '@internal/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 '@internal/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 .claude/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts

postgres-contract-serializer

SqlContractSerializer (from @internal/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 '@internal/family-sql/ir';
const contract = new SqlContractSerializer().deserializeContract(contractJson) as Contract;

// After
import { PostgresContractSerializer } from '@internal/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 enum2enum. 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 @internal/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 '@internal/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 8 repo: examples/prisma-8-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.