Prisma ORM 8 is here.Read the docs

Coming from Prisma ORM 7

What each Prisma ORM 7 schema attribute, command, and query is called in Prisma ORM 8, and what is not available.

If you know Prisma ORM 7, this page tells you what each thing you already use is called in Prisma ORM 8. Two renamed things to know before the tables: the schema file is now called the contract (the same file, renamed), and generating the client types is now called emit. This page is a lookup table, not a tutorial: one table each for the schema, the command-line tool, and the query API, and a final section on what is missing.

To move a running application, follow Migrate from Prisma ORM 7 to 8 instead. It runs both versions side by side against the same database and moves one part of the app at a time. To decide whether to move at all, see Release status.

All examples use PostgreSQL.

Before you start

If you are adding Prisma ORM 8 to an application that runs Prisma ORM 7, do not run the new-project commands further down in that project, because npm install prisma installs version 8 in place of the version 7 command-line tool. Follow Migrate from Prisma ORM 7 to 8 instead. It runs both versions side by side against your existing database without changing its data, and ends with Prisma ORM 8 managing its migrations.

On PostgreSQL, the following command moves Prisma ORM 7 to the prisma7 command and prisma7.config.ts, and sets up Prisma ORM 8 to read your schema.prisma as its contract:

bunx prisma@latest orm init --from-prisma7-schema prisma/schema.prisma

It does not change prisma/ or the database. orm init on a Prisma ORM 7 project lists what it changes. Then run npx prisma db sign and continue with phase 3 of the guide, moving routes one at a time to the client in src/prisma/db.ts. Prisma ORM 7 keeps owning your migrations until the guide's phase 4, where Prisma ORM 8 takes them over. Phase 4 needs a contract file written for Prisma ORM 8. Before you start it, create one with the guide's steps 2.3 to 2.5, and set contract in prisma.config.ts to that file's path.

To set up Prisma ORM 8 in a new project, install prisma and @prisma/orm-postgres, then run orm init:

Terminal
bun add --dev prisma
bun add @prisma/orm-postgres
bunx prisma orm init --write-env

orm init writes prisma.config.ts, a starter src/prisma/contract.prisma, src/prisma/db.ts, and .env. Put your connection string in .env as DATABASE_URL, write your models in the contract, and run npx prisma contract emit; db.ts imports the two files it writes.

Commands are written as prisma ... in the tables below. Run them as npx prisma ....

schema.prisma is now contract.prisma

schema.prisma is now src/prisma/contract.prisma. It is the same file, renamed: you still describe your models in it. In Prisma ORM 8, "schema" means a PostgreSQL schema, the namespace your tables live in, usually public.

The biggest visible change is in field types. Where Prisma ORM 7 wrote a Prisma type plus a @db. attribute, such as String @db.VarChar(255), Prisma ORM 8 writes the database type as the field type: VarChar(255).

Prisma ORM 7Prisma ORM 8Notes
schema.prismacontract.prisma
(nothing)// use prisma-8 as the first linerequired in every Prisma ORM 8 .prisma contract file; see the note below the table
generator client { ... }removedemit is the new word for generate. prisma contract emit writes two files next to your contract, contract.json and contract.d.ts. Commit both
datasource db { ... }the orm section of prisma.config.tsthe connection string and file paths live in the config file, not the schema. Example below
String @db.TextStringString is already stored as text. A known bug in one error message suggests Text; there is no such type
String @db.VarChar(255)VarChar(255)the same for every other @db. type: the attribute name becomes the field type. PSL syntax lists them
Decimal @db.Decimal(10, 2)Numeric(10, 2)Decimal on its own still exists and is a numeric column without a fixed precision
Int, Boolean, Float, BigInt, Bytesunchanged
String @db.UuidUuid
DateTime, DateTime @db.TimestamptzDateTimesame name, and it is already a timestamptz column. Values come back as Temporal.Instant, not JavaScript Date; new Date(value.epochMilliseconds) converts one. See the note below the table
updatedAt DateTime @updatedAtupdatedAt temporal.updatedAt()write temporal.updatedAt() where the type would go. It declares a DateTime field and sets it on every create and update. Any field name works. temporal. is part of the contract syntax; there is nothing to import
createdAt DateTime @default(now())the same, or createdAt temporal.createdAt()both work. @default(now()) is a database default. temporal.createdAt() is set by Prisma ORM from your application's clock and gives the column no database default
String @default(cuid())String @default(cuid(2))2 is the cuid version. Plain cuid() is rejected
@default(dbgenerated("gen_random_uuid()"))@default(sql`gen_random_uuid()`)dbgenerated is rejected. Write any SQL expression as sql`...` instead, but keep writing now() and autoincrement() as plain functions. Default values covers every kind of default
JsonJsonbPrisma ORM 7's Json stored a PostgreSQL jsonb column, so write Jsonb. In Prisma ORM 8, Json means PostgreSQL's older json type, which you almost certainly do not want
Json @default("{}")Jsonb @default(json`{}`)write a JSON default as json`...`. A quoted string is text, and a JSON column does not accept it
enum Role { ADMIN USER }, in a new databaseenum Role { ADMIN USER }same syntax. You can give members explicit values (ADMIN = "admin"). The column is stored as text, not as a PostgreSQL enum type
enum Role { ADMIN USER } with a field role Role, in an existing Prisma ORM 7 databasenative_enum Role { ADMIN = "ADMIN" USER = "USER" } with the field role pg.enum(Role)your database already has a PostgreSQL enum type for it, and native_enum keeps it. See the note below the table
String[]String[]unchanged on PostgreSQL. The list filters (has, hasEvery, hasSome, isEmpty) are not available; filter on a list with raw SQL for now (see below)
@@schema("billing")namespace billing { model ... }a namespace block is a PostgreSQL schema; wrap the models in it. They become db.orm.billing.Invoice and so on
implicit many-to-many (Post[] on Tag and Tag[] on Post, with no model for the join table)the same two list fields, plus a model for the join tableyou write the join table as a model yourself. Example below
@id, @unique, @default, @relation, @map, @@map, @@index, @@id, @@uniqueunchangeda model without @@map names its table after the model, exactly as written, as in Prisma ORM 7
a required relation field over an optional foreign key, or the reverserejectedsince 8.0.0-rc.10, contract emit reports PSL_RELATION_NULLABILITY_MISMATCH. Give the relation field and its @relation(fields: [...]) columns the same ?, or none

A many-to-many relation keeps the two list fields and adds a model for the join table. Its primary key must be the two foreign keys and nothing else, as @@id([postId, tagId]) below:

src/prisma/contract.prisma
// use prisma-8

model Post {
  id   Int    @id @default(autoincrement())
  tags Tag[]
}

model Tag {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

model PostTag {
  postId Int
  tagId  Int
  post   Post @relation(fields: [postId], references: [id])
  tag    Tag  @relation(fields: [tagId], references: [id])

  @@id([postId, tagId])
}

That is complete as written: you do not need a PostTag[] field on Post or Tag. .include("tags") gives you post.tags as a Tag[], and connect and disconnect work on it. set does not; see Not available below.

If the join table already exists from Prisma ORM 7, it is named _PostToTag with columns A and B. Keep the table by mapping the model to those names: @@map("_PostToTag") on the model, @map("A") on the field that points at the model whose name sorts first alphabetically (postId), and @map("B") on the other (tagId).

The config file replaces the generator and datasource blocks. This one is for PostgreSQL:

prisma.config.ts
import "dotenv/config";
import { definePrismaConfig } from "prisma/config";
import { defineConfig as ormConfig } from "@prisma/orm-postgres/config";

export default definePrismaConfig({
  orm: ormConfig({
    contract: "./src/prisma/contract.prisma",
    db: {
      connection: process.env.DATABASE_URL!,
    },
  }),
});

The orm section takes these keys:

  • contract: the path to your contract file, or a glob for a contract in several files. On PostgreSQL it can also be prisma7Schema("prisma/schema.prisma"), with prisma7Schema imported from @prisma/orm-postgres/config next to defineConfig. The schema stays in Prisma ORM 7 syntax, so the changes in the table above do not apply to it, and it needs no // use prisma-8 line. contract emit fails on any part of it that Prisma ORM 8 cannot read, such as a view block, and the error names the line and suggests a change to the Prisma ORM 7 schema; see Use a Prisma ORM 7 schema.
  • db: the connection.
  • output (optional): where contract.json and contract.d.ts are written. Default: next to the contract.
  • extensions (optional): database extensions, for example extensions: [pgvector] with import pgvector from "@prisma/orm-extension-pgvector/control". See Using extensions.
  • migrations (optional): { dir: "migrations" }, relative to prisma.config.ts. Your own migrations go in migrations/app/ under it.

There is no provider setting; importing from @prisma/orm-postgres/config is what selects PostgreSQL.

Commands

The Prisma ORM 8 command-line tool groups commands by what they act on: contract for your contract file, db for the database you are connected to, and migration for the migration files in your repository.

Prisma ORM 7Prisma ORM 8Notes
prisma generateprisma contract emitrun it after every change to the contract. It writes contract.json and contract.d.ts; commit both
prisma migrate devprisma db update, or prisma migration plan then prisma db migratesee the paragraph below the table
prisma migrate deployprisma db migrateapplies the migration files in your repository
prisma db pushprisma db init the first time, on an empty database; prisma db update after thatdb init creates the tables; db update changes existing tables to match the contract. For a database that already has your Prisma ORM 7 tables, run neither; see step 4 of the migration guide. See db update
prisma db pullprisma contract inferwrites a first draft of a contract from an existing database
prisma migrate diffprisma db update --dry-run to see what would change in the database, or prisma migration show <migration name> to print what a migration file does<migration name> is a folder name under migrations/app/, for example 20260911T1030_add_bio. To diff two migrations into a new migration file, prisma migration plan --from <migration name> --to <migration name>
prisma migrate resolve --appliedsee Rollbacks and recoveryfor a migration that failed half way
prisma migrate resolve for a database that already matches your schemastep 4 of the migration guidethe guide records which contract the database matches, both in the database and in your repository. Follow it rather than running the commands by hand
prisma migrate resetnonedrop and recreate the database with your database tools, then prisma db init
prisma db seednonethere is no seed command. Write a script that imports db from src/prisma/db.ts and calls .create(...), and run it the way you run any TypeScript file, for example npx tsx src/prisma/seed.ts
prisma studionone in the Prisma ORM 8 toolrun Studio from the Prisma ORM 7 tool, from a directory outside your project, with the connection string spelled out: cd /tmp && npx prisma@7 studio --url "postgresql://user:password@localhost:5432/mydb". See Studio with Prisma ORM
prisma formatprisma contract format
prisma validatenoneprisma contract emit fails if the contract is invalid, and prisma db verify checks it against the database

Day to day, the loop that replaces prisma migrate dev is: edit the contract, run prisma contract emit, then prisma db update to apply the change to your development database. db update --dry-run shows what it would change, and db update asks before it drops anything. When you want a migration file to commit, run prisma migration plan instead of db update; it writes a folder under migrations/app/ with a migration.ts you can read, edit, and commit. Then prisma db migrate applies it. See Generating a migration.

Four new commands you will meet first:

  • prisma orm init sets up Prisma ORM 8 in a project: config file, starter contract, and db.ts (see Before you start).
  • prisma db init creates the tables for your contract in an empty database. (db update is for a database that already has tables.)
  • prisma db verify checks whether the database still matches your contract.
  • prisma db sign records in the database that it matches your contract. Step 4 of the migration guide runs it for your existing database; you will rarely type it yourself.

The rest are in the CLI reference.

src/prisma/db.ts replaces PrismaClient

There is no generated PrismaClient: you create the client once, in src/prisma/db.ts, from the two files prisma contract emit wrote:

src/prisma/db.ts
import "dotenv/config";
import postgres from "@prisma/orm-postgres/runtime";
import type { Contract } from "./contract.d";
import contractJson from "./contract.json" with { type: "json" };

export const db = postgres<Contract>({
  contractJson,
  url: process.env.DATABASE_URL!,
});

prisma orm init writes this file for you. Every example on this page imports db from it.

db has these parts:

  • db.orm: your models, by model name (db.orm.public.User).
  • db.sql: the SQL query builder. It holds your tables by table name, as in db.sql.public.User, and a table's name is its model's name unless you set @@map. On this page, the raw SQL examples use it only to give each column's type.
  • db.raw.sql: lets you write a SQL query as text.
  • db.runtime(): the connection. It runs raw queries.
  • db.transaction: runs several queries in one transaction.

To add middleware, add a middleware: [...] key to the postgres({ ... }) call above. A middleware is a plain object with a name and one or more hook functions, for example { name: "log", async beforeQuery(plan) { console.log(plan.sql); } }. How middleware works lists the hooks.

Queries

A query is a chain of calls that you await as a whole. The last call (.all(), .first(), .create(...), and so on) says what you want back, instead of one method with one big options object.

Prisma ORM 7Prisma ORM 8
new PrismaClient()db, from src/prisma/db.ts above. The examples below assume they live in a file directly inside src/, so the import path is ./prisma/db
prisma.userdb.orm.public.User (public is the PostgreSQL schema; unless you set one up, every table is in public)
findMany({ where }).where(...).all()
findFirst({ where }).where(...).first(). Returns null when nothing matches. .first({ id: 1 }) is a shortcut for .where({ id: 1 }).first()
findUnique({ where }).where(...).first(), and check for null. There is no separate unique lookup
where: { id: 1, active: true }.where({ id: 1, active: true }); for comparisons, .where((u) => u.age.gt(18)). Filter conditions and operators lists them all
select: { id: true, email: true }.select("id", "email")
include: { posts: { where: ... } }.include("posts"), or .include("posts", (posts) => posts.where({ published: true })) to filter or sort the related records
where: { name: { contains: "ali" } }.where((u) => u.name.like("%ali%")); see Not available for startsWith and case-insensitive matching
orderBy: { createdAt: "desc" }.orderBy((u) => u.createdAt.desc())
take / skip.limit(n) / .offset(n)
distinct: ["country"].distinct("country")
create({ data: { ... } }).create({ ... }), without the data wrapper
create({ data: { ..., posts: { create: [...] } } }).create({ ..., posts: (p) => p.create([...]) }); connect works the same way
createMany({ data: [...] }).createAll([...]) to get the rows back, or .createAndCount([...]) to get a count
createMany({ data: [...], skipDuplicates: true }).createAll([...], { onConflict: "skip" }) or .createAndCount([...], { onConflict: "skip" })
where: { title: { search: "cat & dog" } }, with the fullTextSearchPostgres preview feature.where((p) => p.title.fullTextMatches(toTsquery("cat & dog"))). See Full-text search below
update({ where, data }).where(...).update({ ... })
updateMany({ where, data }).where(...).updateAll({ ... }) for the rows, or .updateAndCount({ ... }) for a count
delete({ where }).where(...).delete()
deleteMany({ where }).where(...).deleteAll() for the rows, or .deleteAndCount() for a count
upsert({ where, create, update }).upsert({ create: { email: "a@b.c", name: "A" }, update: { name: "A" }, conflictOn: { email: "a@b.c" } }). create is the row to insert. conflictOn takes a unique column with its value; if a row with that value exists, update is applied to it instead of inserting. Leave conflictOn out to use the primary key
count({ where }).where(...).aggregate((agg) => ({ total: agg.count() })), which returns one object, { total: 5 }
aggregate({ _sum, _avg }).aggregate((agg) => ({ total: agg.sum("views"), average: agg.avg("views") }))
groupBy({ by, _count }).groupBy("userId").aggregate((agg) => ({ count: agg.count() }))
$transaction(async (tx) => ...)db.transaction(async (tx) => ...); inside, query through tx.orm instead of db.orm
$queryRawsee the raw SQL example below
$queryRaw with SELECT ... FOR UPDATE to lock rows.forUpdate() on a SQL query builder SELECT, inside db.transaction(...). forNoKeyUpdate(), forShare(), and forKeyShare() take the other locks
$executeRawsee the raw SQL example below
$connect()db.connect(). You rarely need it: the client connects on the first query
$disconnect()db.close()

Find with a filter

Prisma ORM 7
const users = await prisma.user.findMany({
  where: { active: true },
  select: { id: true, email: true },
  orderBy: { createdAt: "desc" },
  take: 10,
});
Prisma ORM 8
import { db } from "./prisma/db";

const users = await db.orm.public.User
  .where({ active: true })
  .select("id", "email")
  .orderBy((u) => u.createdAt.desc())
  .limit(10)
  .all();

Create a record

Prisma ORM 7
const user = await prisma.user.create({
  data: { email: "alice@prisma.io", name: "Alice" },
});
Prisma ORM 8
const user = await db.orm.public.User.create({
  email: "alice@prisma.io",
  name: "Alice",
});

Raw SQL

Prisma ORM 7
const rows = await prisma.$queryRaw`SELECT id, email FROM "User" WHERE active = true`;
await prisma.$executeRaw`UPDATE "User" SET active = false WHERE id = ${id}`;
Prisma ORM 8
// A query that returns rows must say what type each column is.
const user = db.sql.public.User;
const select = db.raw.sql`SELECT id, email FROM "User" WHERE active = true`
  .returnsRow({ id: user.columns.id, email: user.columns.email })
  .build();
const rows = await db.runtime().query(select);

// A statement that changes rows: ask for the count.
const update = db.raw.sql`UPDATE "User" SET active = false WHERE id = ${id}`
  .affectedCount()
  .build();
await db.runtime().execute(update);

The table is User, because a model without @@map names its table after the model. As in Prisma ORM 7, a value you put in the SQL with ${...}, such as ${id}, is sent to the database as a query parameter.

Every raw query that returns rows needs .returnsRow(...) with a type per column, and you take the type from the table, as above. A computed column has no table column to take it from, so write the type name as a string instead: for SELECT count(*) AS total FROM "User", write .returnsRow({ total: "pg/int8@1" }). Raw queries lists the type names. Finish the query with .build() and pass it to db.runtime().query() for rows, or to db.runtime().execute() for the number of rows a write changed. On PostgreSQL, db.runtime() returns the connection directly, so it needs no await.

Transaction

Prisma ORM 7
const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: { email: "jane@prisma.io" } }),
  prisma.post.create({ data: { title: "Hello" } }),
]);
Prisma ORM 8
const result = await db.transaction(async (tx) => {
  const user = await tx.orm.public.User.create({ email: "jane@prisma.io" });
  const post = await tx.orm.public.Post.create({ title: "Hello", authorId: user.id });
  return { user, post };
});

Import the functions that build a search query from @prisma/orm-postgres/target/full-text. toTsquery takes PostgreSQL's operator syntax as written, such as cat & dog, and fails on text that is not valid syntax, so use it only for text your application writes. For text a user types into a search box, use websearchToTsquery, which accepts quoted phrases, or, and - in front of a word to exclude it. To sort by relevance, pass the same query to fullTextRank:

Prisma ORM 8
import { websearchToTsquery } from "@prisma/orm-postgres/target/full-text";

const query = websearchToTsquery('"cat food" -dog');
const posts = await db.orm.public.Post
  .where((p) => p.title.fullTextMatches(query))
  .orderBy((p) => p.title.fullTextRank(query).desc())
  .all();

Add @@fullTextIndex([title], name: "post_title_search") to the Post model so PostgreSQL can use an index for the search. Full-text search covers the other options.

Not available

Prisma ORM 7 features that have no direct form in Prisma ORM 8, with what to do instead. The status column says whether each feature is not available, available in a different form, or, for $extends, replaced by middleware.

Prisma ORM 7 featureStatusWhat to do instead
skipDuplicates on createManyavailable in a different form.createAll(rows, { onConflict: "skip" }) or .createAndCount(rows, { onConflict: "skip" }). See the note below the table
{ increment: n } / { decrement: n } in an updatenot availablewrite the arithmetic as raw SQL (see the example above), or read the value and write it back inside db.transaction(...)
findUniqueOrThrow / findFirstOrThrownot available on the querythe usual form is .first() and a null check. If you want the throw, write await db.orm.public.User.where({ id }).all().firstOrThrow(). It throws an error with code RUNTIME.NO_ROWS when nothing matches. Avoid it on a filter that can match many rows; it reads them all first
mode: "insensitive"available in a different formon PostgreSQL, .ilike("%alice%") on a text field. You write the % wildcards yourself
contains / startsWith / endsWithavailable in a different form.like("%alice%"), .like("alice%"), .like("%alice"); you write the % wildcards yourself
filtering inside JSON (path, string_contains, array_contains)not availablea Jsonb field can only be compared as a whole (eq, neq, in, notIn) or checked for null. Write the query as raw SQL (see the example above)
$transaction([...]) with an array of queriesavailable in a different formdb.transaction(async (tx) => ...); the queries inside run one after another
Prisma.User and Prisma.UserGetPayload<...>available in a different formcontract.d.ts exports a Models namespace with one type per model. Scalars<Models.public_User> is the row a plain query returns, Shape<Models.public_User, { posts: { "+": "id" | "title" } }> is a row with chosen relations, and ResultType<typeof query> is what a query you already wrote returns. See Model and result types
implicit many-to-many relationsavailable in a different formwrite the join table as a model; see the example under Schema
@@map on an enumnot availablecontract emit rejects it with Unknown attribute "@@map" in "enum" block. A plain enum is stored as text, so there is nothing to name. If you need a PostgreSQL enum type with a specific name, declare it with native_enum, which does accept @@map
$use middlewareavailable in a different formpass middleware: [...] when you create the client in db.ts (example in the db.ts section above). How middleware works lists the hooks
$extendsreplaced by middleware, custom collections, and scopes$extends will not be added. Middleware replaces query extensions; see How middleware works. For model methods, subclass Collection (Custom Collection subclass). To share a filter or a selection between queries and models, write a scope

Also not available today:

  • Nested writes other than create, connect, and disconnect: connectOrCreate, nested update, updateMany, upsert, delete, deleteMany, and set on a relation. Change the related rows through their own model instead, for example db.orm.public.Post.where({ authorId }).updateAll({ ... }), inside db.transaction(...) if it must be atomic.

  • The list filters has, hasEvery, hasSome, and isEmpty on String[] and other list fields. The fields themselves work, and you can filter on them with a raw SQL query written the same way as the raw SQL example above. For a Post model with a tags String[] field:

    Prisma ORM 8
    const post = db.sql.public.Post;
    const query = db.raw.sql`SELECT id FROM "Post" WHERE ${tag} = ANY(tags)`
      .returnsRow({ id: post.columns.id })
      .build();
    const rows = await db.runtime().query(query);
  • Transaction options: isolationLevel, timeout, maxWait, and transactions inside transactions.

  • omit, relationLoadStrategy, Prisma.skip, and the automatic batching of findUnique calls.

  • The P2002 / P2025 style error codes. Errors carry a code such as RUNTIME.NO_ROWS instead, and a database error such as a unique-constraint violation carries the standard SQL state code in error.sqlState (23505 for a unique violation, on every database), so catch it with if (error instanceof Error && "sqlState" in error && error.sqlState === "23505"). See the error reference.

  • Prisma.sql, Prisma.join, Prisma.raw, Prisma.empty, and TypedSQL. Use db.raw.sql (example above); Raw queries covers building a query from pieces.

  • Soft delete, validation rules in the schema, lifecycle hooks on models, and read replicas. For soft delete, add a nullable deletedAt field and filter on it, with one scope that every model with the field can use. Validate in your application code. Use middleware for hooks. For read replicas, create one client per database.

Where to go next

On this page