Coming from Prisma ORM 7
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.
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.prismapnpm dlx prisma@latest orm init --from-prisma7-schema prisma/schema.prismayarn dlx prisma@latest orm init --from-prisma7-schema prisma/schema.prismanpx prisma@latest orm init --from-prisma7-schema prisma/schema.prismaIt 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:
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:
bun add --dev prisma
bun add @prisma/orm-postgres
bunx prisma orm init --write-envpnpm add --save-dev prisma
pnpm add @prisma/orm-postgres
pnpm prisma orm init --write-envyarn add --dev prisma
yarn add @prisma/orm-postgres
yarn prisma orm init --write-envnpm install --save-dev prisma
npm install @prisma/orm-postgres
npx prisma orm init --write-envorm 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 ....
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 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 7 | Prisma ORM 8 | Notes |
|---|---|---|
schema.prisma |
contract.prisma |
|
| (nothing) | // use prisma-8 as the first line |
required in every Prisma ORM 8 .prisma contract file; see the note below the table |
generator client { ... } |
removed | emit 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.ts |
the connection string and file paths live in the config file, not the schema. Example below |
String @db.Text |
String |
String 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, Bytes |
unchanged | |
String @db.Uuid |
Uuid |
|
DateTime, DateTime @db.Timestamptz |
DateTime |
same 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 @updatedAt |
updatedAt 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 |
Json |
Jsonb |
Prisma 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 database |
enum 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 database |
native_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 table | you write the join table as a model yourself. Example below |
@id, @unique, @default, @relation, @map, @@map, @@index, @@id, @@unique |
unchanged | a 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 reverse | rejected | since 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:
// 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:
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 beprisma7Schema("prisma/schema.prisma"), withprisma7Schemaimported from@prisma/orm-postgres/confignext todefineConfig. 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-8line.contract emitfails on any part of it that Prisma ORM 8 cannot read, such as aviewblock, 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): wherecontract.jsonandcontract.d.tsare written. Default: next to the contract.extensions(optional): database extensions, for exampleextensions: [pgvector]withimport pgvector from "@prisma/orm-extension-pgvector/control". See Using extensions.migrations(optional):{ dir: "migrations" }, relative toprisma.config.ts. Your own migrations go inmigrations/app/under it.
There is no provider setting; importing from @prisma/orm-postgres/config is what selects PostgreSQL.
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 7 | Prisma ORM 8 | Notes |
|---|---|---|
prisma generate |
prisma contract emit |
run it after every change to the contract. It writes contract.json and contract.d.ts; commit both |
prisma migrate dev |
prisma db update, or prisma migration plan then prisma db migrate |
see the paragraph below the table |
prisma migrate deploy |
prisma db migrate |
applies the migration files in your repository |
prisma db push |
prisma db init the first time, on an empty database; prisma db update after that |
db 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 pull |
prisma contract infer |
writes a first draft of a contract from an existing database |
prisma migrate diff |
prisma 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 --applied |
see Rollbacks and recovery | for a migration that failed half way |
prisma migrate resolve for a database that already matches your schema |
step 4 of the migration guide | the 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 reset |
none | drop and recreate the database with your database tools, then prisma db init |
prisma db seed |
none | there 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 studio |
none in the Prisma ORM 8 tool | run 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 format |
prisma contract format |
|
prisma validate |
none | prisma 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 initsets up Prisma ORM 8 in a project: config file, starter contract, anddb.ts(see Before you start).prisma db initcreates the tables for your contract in an empty database. (db updateis for a database that already has tables.)prisma db verifychecks whether the database still matches your contract.prisma db signrecords 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.
There is no generated PrismaClient: you create the client once, in src/prisma/db.ts, from the two files prisma contract emit wrote:
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 indb.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.
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 7 | Prisma 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.user |
db.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 |
$queryRaw |
see the raw SQL example below |
$executeRaw |
see the raw SQL example below |
$connect() |
db.connect(). You rarely need it: the client connects on the first query |
$disconnect() |
db.close() |
const users = await prisma.user.findMany({
where: { active: true },
select: { id: true, email: true },
orderBy: { createdAt: "desc" },
take: 10,
});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();const user = await prisma.user.create({
data: { email: "alice@prisma.io", name: "Alice" },
});const user = await db.orm.public.User.create({
email: "alice@prisma.io",
name: "Alice",
});const rows = await prisma.$queryRaw`SELECT id, email FROM "User" WHERE active = true`;
await prisma.$executeRaw`UPDATE "User" SET active = false WHERE id = ${id}`;// 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.
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { email: "jane@prisma.io" } }),
prisma.post.create({ data: { title: "Hello" } }),
]);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:
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.
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 feature | Status | What to do instead |
|---|---|---|
skipDuplicates on createMany |
available 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 update |
not available | write the arithmetic as raw SQL (see the example above), or read the value and write it back inside db.transaction(...) |
findUniqueOrThrow / findFirstOrThrow |
not available on the query | the 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 form | on PostgreSQL, .ilike("%alice%") on a text field. You write the % wildcards yourself |
contains / startsWith / endsWith |
available in a different form | .like("%alice%"), .like("alice%"), .like("%alice"); you write the % wildcards yourself |
filtering inside JSON (path, string_contains, array_contains) |
not available | a 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 queries |
available in a different form | db.transaction(async (tx) => ...); the queries inside run one after another |
Prisma.User and Prisma.UserGetPayload<...> |
available in a different form | contract.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 relations | available in a different form | write the join table as a model; see the example under Schema |
@@map on an enum |
not available | contract 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 middleware |
available in a different form | pass middleware: [...] when you create the client in db.ts (example in the db.ts section above). How middleware works lists the hooks |
$extends |
replaced by middleware | $extends will not be added. Middleware replaces it and can do more; see How middleware works |
Also not available today:
-
Nested writes other than
create,connect, anddisconnect:connectOrCreate, nestedupdate,updateMany,upsert,delete,deleteMany, andseton a relation. Change the related rows through their own model instead, for exampledb.orm.public.Post.where({ authorId }).updateAll({ ... }), insidedb.transaction(...)if it must be atomic. -
The list filters
has,hasEvery,hasSome, andisEmptyonString[]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 aPostmodel with atags String[]field:ORM 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 offindUniquecalls. -
The
P2002/P2025style error codes. Errors carry acodesuch asRUNTIME.NO_ROWSinstead, and a database error such as a unique-constraint violation carries the standard SQL state code inerror.sqlState(23505for a unique violation, on every database), so catch it withif (error instanceof Error && "sqlState" in error && error.sqlState === "23505"). See the error reference. -
Prisma.sql,Prisma.join,Prisma.raw,Prisma.empty, and TypedSQL. Usedb.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
deletedAtfield and filter on it. Validate in your application code. Use middleware for hooks. For read replicas, create one client per database.
- Migrate from Prisma ORM 7 to 8, the step-by-step migration for a running PostgreSQL application.
- Core concepts, for what a contract is and what
contract emitdoes. - ORM client reference, for every query method.
- Reading data and Writing data, for the query API in full.
- Release status, for how close Prisma ORM 8 is to its final release, and how to stay on Prisma ORM 7.