diff --git a/.claude/skills/docs-writer/SKILL.md b/.claude/skills/docs-writer/SKILL.md index cac0ebe62a..ce4c613e0d 100644 --- a/.claude/skills/docs-writer/SKILL.md +++ b/.claude/skills/docs-writer/SKILL.md @@ -14,6 +14,8 @@ If a rule here conflicts with house style, follow the house style and flag the c For a step-by-step example of writing each kind of page (how-to, concept, reference) and rewriting an existing one, see `references/how-to-use.md`. +For a Prisma Next docs page or section (anything under `content/docs/orm/next/` or `content/docs/(index)/next/`), also read `references/prisma-next.md`: page location, redirect handling (commented out until the URL cutover), tested-example requirements, tab and diagram conventions, and naming rules. + ## Foundation These come from Prisma's positioning. They shape how docs frame the product, without turning a page into marketing. diff --git a/.claude/skills/docs-writer/references/prisma-next.md b/.claude/skills/docs-writer/references/prisma-next.md new file mode 100644 index 0000000000..4e7e331151 --- /dev/null +++ b/.claude/skills/docs-writer/references/prisma-next.md @@ -0,0 +1,40 @@ +# Prisma Next docs conventions + +Follow these when writing or reviewing a Prisma Next docs section in `apps/docs` (the Fundamentals, Middleware, Extensions, Reference, and Guides trees). They encode decisions from DR-8681/DR-8687/DR-8688 and PR #8011 review rounds so parallel section PRs stay consistent. + +## Where pages live + +- Concept and task docs go in the ORM "Next" version tree: `content/docs/orm/next/
/` served at `/docs/orm/next/
/`. The version dropdown keys off the `/orm/next/*` path. +- Getting-started funnel pages stay under `content/docs/(index)/next/`. +- Frontmatter `url` must mirror the file path. Register the section in `content/docs/orm/next/meta.json`. + +## Redirects: commented out until the cutover + +Do not add live Prisma 7 → Prisma Next redirects yet. They retire live URLs, and they ship together when the `/orm/next` tree becomes `/orm` (DR-8687). + +Instead, append your section's redirect map to the commented block in `apps/docs/next.config.mjs` (search for "Prisma Next URL cutover"). Keep the entries commented, one per line, `permanent: false`, with your DR reference. Pages with no Prisma Next equivalent stay live on the Prisma 7 tree; list them in the same comment block for the SEO owner. + +## Accuracy: test before you write + +Every code sample must be executed against the published `@prisma-next` packages before it lands, or clearly marked as conceptual. Scaffold throwaway apps with `create-prisma@next` non-interactive flags; use `bunx create-db` for PostgreSQL and `mongodb-memory-server` (replica set) for MongoDB. Key tested facts that older internal docs get wrong: + +- PostgreSQL model access is namespace-qualified: `db.orm.public.User`, `db.sql.public.user`. MongoDB uses flat lowercase plural roots (`db.orm.users`) and documents keep `_id`. +- `.update()` / `.delete()` affect one record; `updateAll` / `deleteAll` / `*Count` are the bulk forms. No `.count()` terminal; use `.aggregate(...)`. +- No `data` wrapper on `.create(...)`. SQL-builder inserts take an array of rows. +- MongoDB: no `db.transaction(...)`, no ORM `.aggregate(...)`, `@default(now())` not applied at create time. +- Raw SQL exists only as `fns.raw` fragments inside the SQL query builder, not as standalone statements. + +## Page shape + +- Task-first sections: what the user does, which API, simplest example, what it returns, caveats last. Result shapes go in ` ```js no-copy ` blocks after the query. +- Database variants use adjacent code fences with `tab="PostgreSQL"` / `tab="MongoDB"` (PostgreSQL first). Never use the `` JSX in mdx pages; it renders broken. +- Open with a section-linked intro; add an expandable `
` example schema when the page's examples depend on one. +- Prisma 7 migration notes are ` ```diff ` blocks, framed as help, never required context. +- Diagrams: add a `FlowScene` to `apps/docs/src/components/concept-animation/flow-presets.ts` and render with ``. Design as Mermaid first in the PR description. +- End each page with a "Prompt your coding agent" section: copyable prompts per section referencing the scaffolded skills (`prisma-next-queries`, `prisma-next-contract`, ...), then "Next" links. + +## Naming and claims + +- The product is "Prisma Next", not "Prisma Next ORM". The high-level query lane is "the ORM API"; the low-level lanes are "the SQL query builder" and "the pipeline builder". +- No em dashes. Be honest about Early Access; state limitations in user-facing language ("X is not supported yet. For now, do Y") and show the tested workaround. +- Validate with `pnpm --filter docs types:check`, `pnpm lint:links`, `pnpm exec cspell --no-progress "content/docs/orm/next/**/*.mdx"` (use placeholder ids in result blocks; random cuid fragments fail cspell), and a dev-server smoke test. diff --git a/.gitignore b/.gitignore index c10da36d2e..b22648a0f4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,8 @@ opensrc # locally cloned prisma-next repo prisma-next/ +# locally cloned prisma-orm-messaging repo — internal, keep out of this public repo +prisma-orm-messaging/ + # internal positioning doc — not user-facing, keep out of this public repo .claude/skills/content-write-blog/assets/positioning.md diff --git a/apps/docs/content/docs/(index)/next/index.mdx b/apps/docs/content/docs/(index)/next/index.mdx index 04fde5d939..695e6c1002 100644 --- a/apps/docs/content/docs/(index)/next/index.mdx +++ b/apps/docs/content/docs/(index)/next/index.mdx @@ -51,6 +51,28 @@ Start with the setup page when you want a guided first run. +## Learn the fundamentals + +Once you are connected, the Fundamentals section teaches the everyday query patterns. + + + }> + Filter with where, project with select, sort, and paginate. + + }> + Create, update, delete, upsert, and the bulk write variants. + + }> + Read related records with include on PostgreSQL and MongoDB. + + }> + Make several writes succeed or fail together. + + }> + The SQL builder and the MongoDB pipeline builder for shapes the ORM can't express. + + + ## Learn the concepts diff --git a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx new file mode 100644 index 0000000000..7cf93f6323 --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx @@ -0,0 +1,245 @@ +--- +title: Advanced queries +description: Use the SQL query builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM API can't express. +url: /orm/next/fundamentals/advanced-queries +metaTitle: Advanced queries in Prisma Next +metaDescription: Use the Prisma Next SQL query builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB. +--- + +When the ORM API can't express a query, drop one level: the SQL query builder on PostgreSQL, or the pipeline builder on MongoDB. Both stay typed against your contract; neither means writing raw strings. + +The choice is per query, not per app. A codebase that uses the ORM API everywhere and the builders in three hot spots is the intended shape. + +## PostgreSQL: SQL query builder + +The SQL query builder composes a single SQL statement as a typed *plan*: a description of the query you build once and execute through the runtime. You keep full control over the SQL shape (joins, grouping, projections) and full type safety against your contract. + +**Use it when:** + +- The query is easier to say in SQL: joins with conditions, computed columns, set-shaped results. +- You need PostgreSQL behavior the ORM API doesn't surface, such as `RETURNING` on a bulk insert. +- An aggregation needs precise control, like ordering and limiting by an aggregate in the database. +- A query is performance-sensitive and you want to decide its exact shape. + +**Prefer the ORM API when** the query is CRUD, filtered reads, or relation traversal. The [reading](/orm/next/fundamentals/reading-data), [writing](/orm/next/fundamentals/writing-data), and [relations](/orm/next/fundamentals/relations-and-joins) pages cover that surface, with less code and the same type safety. + +### Build and run a plan + +Start from a table with `db.sql.public.` (tables use lowercase storage names), chain clauses, and call `.build()`. Execute the plan with the runtime: + +```typescript +import { db } from "./prisma/db"; + +const plan = db.sql.public.post + .select("id", "title", "authorId") + .where((f, fns) => fns.eq(f.published, true)) + .limit(10) + .build(); + +const publishedPosts = await db.runtime().execute(plan); +``` + +The `.where(...)` callback receives `(fields, fns)`: `fields` holds the column references, `fns` the operators (`eq`, `ne`, `gt`, `lt`, `ilike`, `and`, `count`, and operators added by extensions). + +### Join tables with precise control + +Alias each side with `.as(...)`, join on any condition, and project columns from both sides into a flat result: + +```typescript +const plan = db.sql.public.post + .as("p") + .innerJoin(db.sql.public.user.as("u"), (f, fns) => fns.eq(f.p.authorId, f.u.id)) + .select((f) => ({ + postId: f.p.id, + title: f.p.title, + authorEmail: f.u.email, + })) + .where((f, fns) => fns.eq(f.p.published, true)) + .limit(10) + .build(); + +const postsWithAuthors = await db.runtime().execute(plan); +// Array<{ postId, title, authorEmail }> +``` + +Chain more joins for multi-hop traversals. This is how you get a flat post-tag list through a [many-to-many junction table](/orm/next/fundamentals/relations-and-joins#many-to-many), one row per pair: + +```typescript +const plan = db.sql.public.postTag + .as("pt") + .innerJoin(db.sql.public.tag.as("t"), (f, fns) => fns.eq(f.pt.tagId, f.t.id)) + .innerJoin(db.sql.public.post.as("p"), (f, fns) => fns.eq(f.pt.postId, f.p.id)) + .select((f) => ({ postTitle: f.p.title, tagName: f.t.name })) + .build(); + +const postTagPairs = await db.runtime().execute(plan); +``` + +```js no-copy +[ + { postTitle: 'Hello Prisma Next', tagName: 'databases' }, + { postTitle: 'Hello Prisma Next', tagName: 'typescript' }, + { postTitle: 'Typed queries', tagName: 'typescript' } +] +``` + +### Group and rank results + +Answer "top N groups" questions, such as the authors with the most posts, by ordering and limiting on an aggregate directly in the database: + +```typescript +const plan = db.sql.public.post + .select((f, fns) => ({ + authorId: f.authorId, + posts: fns.count(), + })) + .groupBy((f) => f.authorId) + .orderBy((f, fns) => fns.count(), { direction: "desc" }) + .limit(5) + .build(); + +const topAuthors = await db.runtime().execute(plan); +``` + +```js no-copy +[ + { authorId: 'cuid20000000000000000001', posts: '2' }, + { authorId: 'cuid20000000000000000002', posts: '1' } +] +``` + +PostgreSQL returns counts as strings; convert with `Number(row.posts)`. + +### Write with RETURNING + +SQL builder writes take an array of rows. Use `.returning(...)` to choose which columns come back from the same statement: + +```typescript +const plan = db.sql.public.user + .insert([{ email: "sql@prisma.io" }]) + .returning("id", "email") + .build(); + +const [insertedUser] = await db.runtime().execute(plan); +// Contract defaults such as generated IDs are applied +``` + +### Raw SQL fragments + +Prisma Next does not run standalone raw SQL statements: every query goes through the typed builder. When the operators don't cover an expression you need, embed a raw fragment with `fns.raw` and declare its type with `.returns(...)`. The rest of the query stays typed: + +```typescript +const plan = db.sql.public.user + .select("id", "email") + .select("upperEmail", (f, fns) => fns.raw`UPPER(${f.email})`.returns("pg/text@1")) + .limit(10) + .build(); + +const users = await db.runtime().execute(plan); +// [{ id: 'cuid20000000000000000001', email: 'alice@prisma.io', upperEmail: 'ALICE@PRISMA.IO' }, ...] +``` + +Interpolated values are AST nodes, not string splices, so a fragment can reference columns and other typed expressions safely. If the builder plus `fns.raw` still can't express a shape you need, [share the use case](https://pris.ly/discord). + +## MongoDB: Pipeline builder + +The pipeline builder composes a typed MongoDB aggregation pipeline: a sequence of stages such as `$match`, `$group`, `$sort`, and `$lookup`, checked against your contract. It is the MongoDB counterpart of the SQL query builder, and it is also where all MongoDB aggregation lives, because the ORM API has no `.aggregate(...)` on MongoDB. + +**Use it when:** + +- You need an aggregation: counts, grouping, or summaries per key. +- You want to join and reshape documents across collections with `$lookup`. +- A query needs MongoDB pipeline stages that don't map to the ORM API, such as multi-stage filtering and projection. +- You need operators the MongoDB `.where(...)` doesn't cover yet, like ranges or boolean logic. + +**Prefer the ORM API when** the query is document CRUD or a reference-relation read; `.include(...)` already covers the common `$lookup` case. + +### Build and run a pipeline + +Start from a collection with `db.query.from(...)`, chain stages, and call `.build()`. Execute the plan through the runtime: + +```typescript +import { acc } from "@prisma-next/mongo-query-builder"; +import { db } from "./prisma/db"; + +const runtime = await db.runtime(); + +// Post count per author, most prolific first +const plan = db.query + .from("posts") + .group((f) => ({ + _id: f.authorId, + postCount: acc.count(), + })) + .sort({ postCount: -1 }) + .build(); + +const postsByAuthor = await runtime.execute(plan); +``` + +```js no-copy +[ + { _id: new ObjectId('650000000000000000000001'), postCount: 3 }, + { _id: new ObjectId('650000000000000000000002'), postCount: 2 } +] +``` + +Accumulators such as `acc.count()` and `acc.max(...)` import from `@prisma-next/mongo-query-builder`. + +### Filter and group in stages + +Chain `.match(...)` before `.group(...)` to aggregate over a subset, the pipeline equivalent of `WHERE` before `GROUP BY`: + +```typescript +const plan = db.query + .from("posts") + .match((f) => f.published.eq(false)) + .group((f) => ({ _id: f.authorId, draftCount: acc.count() })) + .build(); + +const draftsByAuthor = await runtime.execute(plan); +``` + +### Join collections with $lookup + +Use `.lookup(...)` for a type-checked join against another collection. The joined documents arrive under the name you give `.as(...)`: + +```typescript +const plan = db.query + .from("posts") + .match((f) => f.published.eq(true)) + .lookup((from) => + from("users") + .on((local, foreign) => ({ local: local.authorId, foreign: foreign._id })) + .as("author"), + ) + .build(); + +const postsWithAuthors = await runtime.execute(plan); +// Each post carries an "author" array with the matching user documents +``` + +## Choose the right query API + +| You need | Use | +| --- | --- | +| CRUD, filters, relations, simple aggregates | ORM API (`db.orm`) | +| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL query builder (`db.sql.public.
`) | +| `$group`, `$lookup` with reshaping, any MongoDB aggregation | Pipeline builder (`db.query.from(...)`) | + +Plans execute through `db.runtime().execute(plan)` on PostgreSQL and `(await db.runtime()).execute(plan)` on MongoDB. Inside a [transaction](/orm/next/fundamentals/transactions), use `tx.execute(plan)`. + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers both builders and the choice between them and the ORM API. Prompts that map to each section: + +- "Using the prisma-next-queries skill, write a SQL builder plan for the top 10 authors by post count." +- "This report needs post and author columns in one flat result. Build the join with the SQL query builder." +- "On MongoDB, group posts per author with the pipeline builder and sort by the count." +- "Review this file and tell me which queries should stay on the ORM API and which need a builder." + +## Next + +- [Read data](/orm/next/fundamentals/reading-data): the ORM happy path these builders back up. +- [Understand relationships](/orm/next/fundamentals/relations-and-joins) before reaching for explicit joins. +- [Run SQL builder plans atomically](/orm/next/fundamentals/transactions) inside a transaction. diff --git a/apps/docs/content/docs/orm/next/fundamentals/meta.json b/apps/docs/content/docs/orm/next/fundamentals/meta.json new file mode 100644 index 0000000000..9e7dba847a --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/meta.json @@ -0,0 +1,10 @@ +{ + "title": "Fundamentals", + "pages": [ + "reading-data", + "writing-data", + "relations-and-joins", + "transactions", + "advanced-queries" + ] +} diff --git a/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx new file mode 100644 index 0000000000..03acab6a99 --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx @@ -0,0 +1,384 @@ +--- +title: Reading data +description: Fetch one record or many with Prisma Next, then filter, select, sort, paginate, and stream the results. +url: /orm/next/fundamentals/reading-data +metaTitle: Reading data with Prisma Next +metaDescription: Query PostgreSQL and MongoDB with Prisma Next. Filter with where, project with select, sort with orderBy, paginate with take and skip, and stream large results. +--- + +This page shows how to read data with Prisma Next: fetching [many records or one](#fetch-many-records-or-one), [filtering](#filter-records), [selecting fields](#select-fields), [sorting and paginating](#sort-and-paginate), [counting](#count-records), and [streaming large results](#stream-large-results). + +Every query chains methods on a model and runs when you call `.all()` or `.first()`: + +```typescript tab="PostgreSQL" +import { db } from "./prisma/db"; + +// Every published post +const posts = await db.orm.public.Post.where({ published: true }).all(); + +// One user, or null +const user = await db.orm.public.User.where({ email: "alice@prisma.io" }).first(); +``` + +```typescript tab="MongoDB" +import { db } from "./prisma/db"; + +// Every published post +const posts = await db.orm.posts.where({ published: true }).all(); + +// One user, or null +const user = await db.orm.users.where({ email: "alice@prisma.io" }).first(); +``` + +Every result is typed against your contract. Models are addressed by schema namespace on PostgreSQL (`db.orm.public.User`, where `public` is the default PostgreSQL schema) and by collection name on MongoDB (`db.orm.users`). + +For Prisma 7 users, `findMany` and `findFirst` / `findUnique` map directly onto the two terminal calls: + +```diff +- const posts = await prisma.post.findMany({ where: { published: true } }); ++ const posts = await db.orm.public.Post.where({ published: true }).all(); + +- const user = await prisma.user.findUnique({ where: { email } }); ++ const user = await db.orm.public.User.where({ email }).first(); +``` + +## Example schema + +All examples on this page are based on the following schema: + +
+ +Expand for sample schema + +```prisma tab="PostgreSQL" +model User { + id String @id @default(cuid(2)) + email String @unique + name String? + createdAt DateTime @default(now()) + posts Post[] +} + +model Post { + id String @id @default(cuid(2)) + title String + content String? + published Boolean + authorId String + author User @relation(fields: [authorId], references: [id]) + createdAt DateTime @default(now()) +} +``` + +```prisma tab="MongoDB" +model User { + id ObjectId @id @map("_id") + email String @unique + name String? + createdAt DateTime @default(now()) + posts Post[] + @@map("users") +} + +model Post { + id ObjectId @id @map("_id") + title String + content String? + published Boolean + author User @relation(fields: [authorId], references: [id]) + authorId ObjectId + createdAt DateTime @default(now()) + @@map("posts") +} +``` + +
+ +## Fetch many records or one + +Use `.all()` when you want every matching record. It returns an array: + +```typescript +const users = await db.orm.public.User.all(); +``` + +```js no-copy +[ + { id: 'cuid20000000000000000001', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z }, + { id: 'cuid20000000000000000002', email: 'bob@prisma.io', name: 'Bob', createdAt: 2026-07-06T09:03:14.112Z } +] +``` + +`.all()` applies no limit of its own, so combine it with [`take`](#sort-and-paginate) on tables that can grow. + +Use `.first()` when you want a single record. It returns the record, or `null` when nothing matches, and on PostgreSQL it fetches at most one row: + +```typescript +const user = await db.orm.public.User.where({ email: "alice@prisma.io" }).first(); +``` + +```js no-copy +{ id: 'cuid20000000000000000001', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z } +``` + +For a primary-key lookup, pass the key directly on PostgreSQL, or filter on `_id` on MongoDB: + +```typescript tab="PostgreSQL" +const user = await db.orm.public.User.first({ id: userId }); +``` + +```typescript tab="MongoDB" +const user = await db.orm.users.where({ _id: id }).first(); +``` + +## Filter records + +Use `.where(...)` to narrow a query. Pass an object to match fields by equality: + +```typescript +const drafts = await db.orm.public.Post.where({ published: false }).all(); +``` + +Chain several `.where(...)` calls to combine conditions with AND. This is also how you express a range: + +```typescript +const recentPosts = await db.orm.public.Post + .where((p) => p.createdAt.gte(start)) + .where((p) => p.createdAt.lte(end)) + .all(); +``` + +### Filter operators on PostgreSQL + +On PostgreSQL, `.where(...)` also accepts a lambda for richer comparisons, as in the range example above. The field proxy supports `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte`, `.like`, `.ilike`, `.in([...])`, `.isNull()`, and `.isNotNull()`: + +```typescript +// Case-insensitive text search +const matchingPosts = await db.orm.public.Post + .where((p) => p.title.ilike("%prisma%")) + .all(); + +// One of several values +const team = await db.orm.public.User + .where((u) => u.email.in(["alice@prisma.io", "bob@prisma.io"])) + .all(); +``` + +To combine conditions with OR or NOT, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`: + +```typescript +import { or } from "@prisma-next/sql-orm-client"; + +const highlighted = await db.orm.public.Post + .where((p) => or(p.title.ilike("%hello%"), p.title.ilike("%prisma%"))) + .all(); +``` + +### Filter operators on MongoDB + +On MongoDB, `.where(...)` accepts the object form only. Comparison operators and boolean logic are not available on it yet. The same filters are available one level down, in the [pipeline builder](/orm/next/fundamentals/advanced-queries#mongodb-pipeline-builder), where `.match(...)` gives you the full operator set: + +```typescript +// Object form: equality filters +const drafts = await db.orm.posts.where({ published: false }).all(); + +// Pipeline builder: ranges and richer operators +const runtime = await db.runtime(); +const plan = db.query + .from("posts") + .match((f) => f.createdAt.gte(new Date("2026-06-01"))) + .match((f) => f.createdAt.lte(new Date("2026-07-01"))) + .build(); +const junePosts = await runtime.execute(plan); +``` + +`.match(...)` calls AND-compose exactly like chained `.where(...)`, and `.in([...])` works the same way: `.match((f) => f.title.in(["Old", "New"]))`. + +## Select fields + +Use `.select(...)` to fetch only the fields you need. The result type narrows to match: + +```typescript tab="PostgreSQL" +const users = await db.orm.public.User.select("id", "email").all(); +``` + +```typescript tab="MongoDB" +const users = await db.orm.users.select("_id", "email").all(); +``` + +```js no-copy +[ + { id: 'cuid20000000000000000001', email: 'alice@prisma.io' }, + { id: 'cuid20000000000000000002', email: 'bob@prisma.io' } +] +``` + +## Sort and paginate + +Use `.orderBy(...)` to sort, `.take(n)` to limit, and `.skip(n)` to offset. On PostgreSQL, sort with a lambda that calls `.asc()` or `.desc()` on a field; on MongoDB, sort with the driver's direction values, `1` for ascending and `-1` for descending: + +```typescript tab="PostgreSQL" +// Second page of posts, newest first +const page = await db.orm.public.Post + .orderBy((p) => p.createdAt.desc()) + .take(20) + .skip(20) + .all(); +``` + +```typescript tab="MongoDB" +// Second page of posts, newest first +const page = await db.orm.posts + .orderBy({ createdAt: -1 }) + .take(20) + .skip(20) + .all(); +``` + +For a composite sort on PostgreSQL, pass an array of lambdas. Records are sorted by the first field, with the second as tiebreaker: + +```typescript +const posts = await db.orm.public.Post + .orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()]) + .all(); +``` + +Offset pagination (`.skip`) re-counts skipped rows on every page, which gets slower as readers go deeper. For stable pagination over large tables on PostgreSQL, follow `.orderBy(...)` with `.cursor(...)` to resume from the last record you returned. + +Keep the `id` tiebreaker in both the sort and the cursor: `createdAt` is not unique, and a cursor on a non-unique field alone can skip or repeat records that share the boundary value. With the composite cursor, pages never overlap even when timestamps tie: + +```typescript +const page1 = await db.orm.public.Post + .orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()]) + .take(20) + .all(); + +const last = page1[page1.length - 1]!; +const page2 = await db.orm.public.Post + .orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()]) + .cursor({ createdAt: last.createdAt, id: last.id }) + .take(20) + .all(); +``` + +## Count records + +Count through `.aggregate(...)` on PostgreSQL. It returns an object with the keys you name: + +```typescript +const result = await db.orm.public.Post + .where({ published: true }) + .aggregate((a) => ({ total: a.count() })); +``` + +```js no-copy +{ total: 2 } +``` + +There is no `.count()` method on the query chain. On MongoDB, count with a `$group` stage in the [pipeline builder](/orm/next/fundamentals/advanced-queries#mongodb-pipeline-builder). + +## Stream large results + +Streaming means processing records one at a time as they arrive from the database, instead of waiting for the full result and holding it in memory. For a query that returns millions of rows, that is the difference between a steady, flat memory footprint and buffering the entire table; it also lets you start working on the first record before the last one has arrived. + +Prisma Next builds this into every read: a query result is both a promise and an async iterator, so you choose how to consume it. + +`await` runs the query and buffers every record into an array. This is the right default: the whole result is in memory, and you can read the array as often as you like. + +```typescript +const posts = await db.orm.public.Post.all(); + +console.log(posts.length); +console.log(posts[0]); +``` + +`for await` streams the result instead. Records are handed to your loop one at a time, without buffering the full result first. Use it when the result is too large to hold in memory, or when you want to start processing before the last record arrives: + +```typescript +for await (const post of db.orm.public.Post.all()) { + await exportToSearchIndex(post); +} +``` + +You can also leave the loop early; unprocessed records are never buffered: + +```typescript +for await (const post of db.orm.public.Post.all()) { + if (isMatch(post)) break; +} +``` + +### A streamed result can only be read once + +Streaming hands each record to your loop and then lets go of it; nothing is kept. So once a `for await` loop has touched a result, that result is finished, even if the loop exited early. Iterating it again, or `await`ing it afterwards, throws an error explaining the result was already consumed: + +```typescript +const result = db.orm.public.Post.all(); + +for await (const post of result) { + // ... +} + +await result; +``` + +```text no-copy +Error: AsyncIterableResult iterator has already been consumed via for-await loop. +Each AsyncIterableResult can only be iterated once. +``` + +If you need the data more than once, `await` the query into an array and reuse the array: + +```typescript +const posts = await db.orm.public.Post.all(); + +const published = posts.filter((p) => p.published); +const titles = posts.map((p) => p.title); +``` + +Streaming behaves the same on PostgreSQL and MongoDB. + +## Common mistakes + +### Fetching everything to use one record + +You wanted one record, so you queried and took the first element: + +```typescript +const users = await db.orm.public.User.where({ email }).all(); +const user = users[0]; +``` + +This fetches every matching record and throws away the rest. Use `.first()` instead: it returns one record or `null`, and on PostgreSQL it asks the database for at most one row: + +```typescript +const user = await db.orm.public.User.where({ email }).first(); +``` + +### Forgetting that .all() has no limit + +`.all()` returns every match. On a table that grows, yesterday's fast query becomes today's slow one. Add `.take(n)` when you don't genuinely need every record, or [stream the result](#stream-large-results) when you do. + +### Reusing a streamed result + +You streamed a result with `for await`, then tried to read it again. The second read throws, because a streamed result is consumed as it is read. Store the data if you need it twice: + +```typescript +const posts = await db.orm.public.Post.all(); +// posts is a plain array now; read it as often as you like +``` + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers everything on this page. Prompts that map to each section: + +- "Using the prisma-next-queries skill, write a query that returns the 20 newest published posts." +- "Add a case-insensitive title search to the posts query, using the field-proxy operators." +- "Convert this offset pagination to cursor pagination with the .cursor() API." +- "This export loops over a huge table. Rewrite it to stream with for await instead of buffering." + +## Next + +- [Write data](/orm/next/fundamentals/writing-data): create, update, delete, and upsert records. +- [Read related records](/orm/next/fundamentals/relations-and-joins) in the same query with `.include(...)`. +- [Use advanced queries](/orm/next/fundamentals/advanced-queries) when a shape needs the SQL query builder or a MongoDB pipeline. diff --git a/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx b/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx new file mode 100644 index 0000000000..7bda40b5f8 --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx @@ -0,0 +1,233 @@ +--- +title: Relations and joins +description: Read related records in one query with .include(), and understand how one-to-one, one-to-many, and many-to-many relationships work. +url: /orm/next/fundamentals/relations-and-joins +metaTitle: Relations and joins in Prisma Next +metaDescription: Query one-to-one, one-to-many, and many-to-many relationships with Prisma Next on PostgreSQL and MongoDB, with diagrams and tested examples. +--- + +Read related records in the same query by adding `.include(...)`. The related records come back nested on the parent, typed to match. + +```typescript tab="PostgreSQL" +import { db } from "./prisma/db"; + +const posts = await db.orm.public.Post + .where({ published: true }) + .include("author") + .all(); +// posts[0].author is the full User record +``` + +```typescript tab="MongoDB" +import { db } from "./prisma/db"; + +const posts = await db.orm.posts + .where({ published: true }) + .include("author") + .all(); +// posts[0].author is the referenced user document +``` + +The relation name in `.include(...)` is the field name from your contract, not a table name. Use `.include(...)` when the caller needs the related data in the same response; skip it when the foreign key on the record is enough. + +This page walks through the three relationship shapes, from the data model to the query and the result. If you already know how relational data is modeled, jump to [filtering by relation data](#filter-parent-records-by-relation-data) or the [limitations](#current-limitations). + +## One-to-one + +One record is linked to at most one other record. The classic example: every profile belongs to exactly one user. + + + +The model that holds the foreign key declares the relation, and the `@unique` on the foreign key is what makes it one-to-one: + +```prisma +model Profile { + id String @id @default(cuid(2)) + bio String + userId String @unique + user User @relation(fields: [userId], references: [id]) +} +``` + +To read a profile with its user, query from the profile and include the relation: + +```typescript +const profileWithUser = await db.orm.public.Profile + .where({ userId: user.id }) + .include("user") + .first(); +// { id, bio, userId, user: { id, email, name, createdAt } } +``` + +`.first()` returns `null` when the profile doesn't exist, so a user without a profile is a `null` check, not an error. + +One thing to know: the mirror field on the other model (`profile Profile?` on `User`) is not supported in the contract yet, so include the relation from the side that owns the foreign key. To start from users and attach profiles in one query, join the two tables with the [SQL query builder](/orm/next/fundamentals/advanced-queries#join-tables-with-precise-control): + +```typescript +const plan = db.sql.public.user + .as("u") + .innerJoin(db.sql.public.profile.as("pr"), (f, fns) => fns.eq(f.pr.userId, f.u.id)) + .select((f) => ({ email: f.u.email, bio: f.pr.bio })) + .build(); + +const usersWithProfiles = await db.runtime().execute(plan); +``` + +```js no-copy +[ { email: 'alice@prisma.io', bio: 'Writes about typed databases.' } ] +``` + +## One-to-many + +One parent record is linked to any number of child records: one user has many posts. This is the relationship you'll query most. + + + +The child stores the parent's id, and the parent declares a list field: + +```prisma +model User { + id String @id @default(cuid(2)) + email String @unique + posts Post[] +} + +model Post { + id String @id @default(cuid(2)) + title String + authorId String + author User @relation(fields: [authorId], references: [id]) +} +``` + +Query it in either direction. From the parent, the children arrive as an array; from the child, the parent arrives as one object: + +```typescript +// Each user with their posts +const usersWithPosts = await db.orm.public.User.include("posts").all(); +// Array<{ id, email, posts: Post[] }> + +// Each post with its author +const postsWithAuthors = await db.orm.public.Post.include("author").all(); +// Array<{ id, title, authorId, author: User }> +``` + +To shape what each relation returns, pass a callback as the second argument. Inside it, chain `.where`, `.select`, `.orderBy`, and `.take` exactly like a top-level query. This is how you fetch "each user with their five newest posts" in one query: + +```typescript +const usersWithRecentPosts = await db.orm.public.User + .select("id", "email") + .include("posts", (post) => + post + .select("id", "title", "createdAt") + .orderBy((p) => p.createdAt.desc()) + .take(5), + ) + .take(10) + .all(); +// Array<{ id, email, posts: Array<{ id, title, createdAt }> }> +``` + +The common mistake here is the N+1 loop: fetching users, then querying posts inside a `for` loop over them. That runs one query per user. One `.include("posts")` on the user query returns the same data in a single query. + +## Many-to-many + +Records on both sides connect to many on the other: a post has many tags, and a tag appears on many posts. Neither table can hold the other's foreign key, so a junction model holds one link per pair. + + + +Model the junction explicitly. Each `PostTag` record links one post to one tag: + +```prisma +model Tag { + id String @id @default(cuid(2)) + name String @unique + posts PostTag[] +} + +model PostTag { + id String @id @default(cuid(2)) + postId String + tagId String + post Post @relation(fields: [postId], references: [id]) + tag Tag @relation(fields: [tagId], references: [id]) +} +``` + +Traverse both hops in one query by nesting an include inside the relation callback: + +```typescript +const postsWithTags = await db.orm.public.Post + .where({ published: true }) + .include("tags", (postTag) => postTag.include("tag")) + .all(); +``` + +```js no-copy +[ + { + title: 'Hello Prisma Next', + // ... + tags: [ + { id: 'k2…', postId: 'i3…', tagId: 't1…', tag: { id: 't1…', name: 'typescript' } }, + { id: 'k3…', postId: 'i3…', tagId: 't2…', tag: { id: 't2…', name: 'databases' } } + ] + }, + { title: 'Typed queries', tags: [ /* one link record */ ] } +] +``` + +The result keeps the junction records in the shape, with each tag nested inside its link record. Read the tag names as `post.tags.map((pt) => pt.tag.name)`. + +Connecting a post to a tag is a plain create on the junction model: + +```typescript +await db.orm.public.PostTag.create({ postId: post.id, tagId: tag.id }); +``` + +For a flat result without the junction records (one row per post-tag pair), [join through the junction table with the SQL builder](/orm/next/fundamentals/advanced-queries#join-tables-with-precise-control). + +## Filter parent records by relation data + +On PostgreSQL, `.where(...)` can reach into a relation: `.some(...)` matches parents with at least one matching child, `.none(...)` matches parents with none, and `.every(...)` requires all children to match. + +```typescript +// Users who have at least one published post +const activeAuthors = await db.orm.public.User + .where((u) => u.posts.some((p) => p.published.eq(true))) + .all(); + +// Posts that carry a specific tag +const taggedPosts = await db.orm.public.Post + .where((p) => p.tags.some((pt) => pt.tagId.eq(tag.id))) + .all(); +``` + +On MongoDB, query the child collection directly, or express the shape as a pipeline with `$lookup` and `$match`. + +## PostgreSQL and MongoDB differences + +On PostgreSQL, Prisma Next fetches included relations with joins. On MongoDB, it uses `$lookup` for reference-style relations; embedded documents are already part of the parent and need no include. + +The relationship shapes above apply to reference-style relations on both databases. On MongoDB, one-to-one and one-to-many data is often embedded in the parent document instead of referenced; embedded data comes back with every read automatically. + +## Current limitations + +- Relations declared with an implicit many-to-many (a `through` junction the contract manages for you) are not supported by `.include(...)` yet. Model the junction explicitly, as shown [above](#many-to-many), and it works today. +- A one-to-one relation can only declare its relation field on the side that holds the foreign key. The mirror field on the other model is not supported yet. +- The include refinement callback is tested on PostgreSQL. On MongoDB, start with the plain `.include("author")` form and use the [pipeline builder](/orm/next/fundamentals/advanced-queries#mongodb-pipeline-builder) to reshape joined documents. + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers relation queries, and `prisma-next-contract` covers the relation fields in your schema. Prompts that map to each section: + +- "Using the prisma-next-contract skill, add a one-to-one Profile model with a unique foreign key to User." +- "Using the prisma-next-queries skill, fetch each user with their five newest posts in one query." +- "Model a many-to-many between Post and Tag with an explicit junction model, and write the nested include that reads a post's tag names." +- "Find users that have at least one published post, using a relation predicate instead of a loop." + +## Next + +- [Use advanced queries](/orm/next/fundamentals/advanced-queries) for explicit joins, flat junction traversals, and `$lookup` pipelines. +- [Read data](/orm/next/fundamentals/reading-data) to filter, sort, paginate, and select fields from your models. +- [Run writes that span several models atomically](/orm/next/fundamentals/transactions) with a transaction. diff --git a/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx b/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx new file mode 100644 index 0000000000..d288cc01f4 --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx @@ -0,0 +1,184 @@ +--- +title: Transactions +description: Run several writes so they all succeed or all fail together with db.transaction(). +url: /orm/next/fundamentals/transactions +metaTitle: Transactions in Prisma Next +metaDescription: Group writes atomically with db.transaction() on PostgreSQL, and run multi-document MongoDB transactions with the MongoDB driver. +--- + +Run several writes as one unit with `db.transaction(...)`: they all commit together, or they all roll back. + +## Run writes in a transaction + +Use a transaction whenever one business operation spans more than one write: creating a user with their first records, moving a value between two rows, or deleting a parent after its children. + +Pass a callback to `db.transaction(...)`. Inside it, query through `tx.orm` instead of `db.orm`; every call rides the same transaction. The callback's return value passes through: + +```typescript +import { db } from "./prisma/db"; + +const result = await db.transaction(async (tx) => { + const user = await tx.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" }); + const post = await tx.orm.public.Post.create({ + title: "Hello", + content: null, + published: false, + authorId: user.id, + }); + return { userId: user.id, postId: post.id }; +}); +// Both records exist now +``` + +You don't need a transaction for a single write; every mutation is already atomic on its own. + +## Roll back on errors + +The transaction commits when the callback returns and rolls back when it throws. Nothing inside the callback survives an error: + +```typescript +try { + await db.transaction(async (tx) => { + await tx.orm.public.User.create({ email: "ghost@prisma.io", name: "Ghost" }); + throw new Error("boom"); + }); +} catch { + // The user record was rolled back and does not exist +} +``` + +## Use the SQL builder in a transaction + +[SQL builder](/orm/next/fundamentals/advanced-queries) plans run inside a transaction through `tx.execute(...)`, with `tx.sql` mirroring `db.sql`: + +```typescript +await db.transaction(async (tx) => { + const plan = tx.sql.public.post + .update({ published: false }) + .where((f, fns) => fns.lt(f.createdAt, cutoff)) + .build(); + await tx.execute(plan); +}); +``` + +## Transactions on MongoDB + +Prisma Next does not support MongoDB transactions yet: there is no `db.transaction(...)` on the MongoDB client, and each ORM write is atomic per document. + +To run a multi-document transaction today, use the MongoDB driver directly. Share one `MongoClient` between Prisma Next and your code, and group the writes in a driver session. MongoDB requires a replica set for transactions. + +```typescript title="src/prisma/db.ts" +import mongo from "@prisma-next/mongo/runtime"; +import { MongoClient } from "mongodb"; +import type { Contract } from "./contract.d"; +import contractJson from "./contract.json" with { type: "json" }; + +export const client = new MongoClient(process.env["DATABASE_URL"]!); + +export const db = mongo({ + contractJson, + mongoClient: client, + dbName: "app", +}); +``` + +```typescript +import { client, db } from "./prisma/db"; + +const session = client.startSession(); +try { + await session.withTransaction(async () => { + const database = client.db("app"); + const user = await database + .collection("users") + .insertOne({ email: "jane@prisma.io", name: "Jane", createdAt: new Date() }, { session }); + await database.collection("posts").insertOne( + { title: "Hello", content: null, published: false, authorId: user.insertedId, createdAt: new Date() }, + { session }, + ); + }); +} finally { + await session.endSession(); +} + +// Prisma Next reads see the committed result +const jane = await db.orm.users.where({ email: "jane@prisma.io" }).first(); +``` + +Writes made through the driver skip the type-checking that Prisma Next queries get, so keep these sections small: one function per atomic operation, with Prisma Next queries for everything around it. + +## Common mistakes + +### Side effects inside the callback + +You sent an email or queued a job inside the transaction, next to the write it belongs to: + +```typescript +await db.transaction(async (tx) => { + const user = await tx.orm.public.User.create({ email, name }); + await sendWelcomeEmail(user.email); // runs even if the transaction rolls back +}); +``` + +Database writes roll back; emails don't. If a later statement throws, the record disappears but the email was already sent. + +Return what you need from the callback, and run the side effect after the transaction has committed: + +```typescript +const user = await db.transaction(async (tx) => { + return tx.orm.public.User.create({ email, name }); +}); + +await sendWelcomeEmail(user.email); +``` + +Now the email can only go out for a user that actually exists. + +### Querying through db instead of tx + +You opened a transaction but kept writing `db.orm` inside the callback: + +```typescript +await db.transaction(async (tx) => { + await db.orm.public.User.create({ email, name }); // outside the transaction +}); +``` + +Queries on `db` run on their own connection, outside the open transaction. They commit immediately and won't roll back with the rest of the callback. Use the `tx` handle for every query inside the callback: `tx.orm` for models, `tx.sql` and `tx.execute` for SQL builder plans. + +### Passing an array of queries + +Prisma 7 supported `$transaction([query1, query2])`. Prisma Next has no `$transaction` and no array form; the callback replaces it: + +```diff +- const [user, post] = await prisma.$transaction([ +- prisma.user.create({ data: { email, name } }), +- prisma.post.create({ data: { title, authorId } }), +- ]); ++ const { user, post } = await db.transaction(async (tx) => { ++ const user = await tx.orm.public.User.create({ email, name }); ++ const post = await tx.orm.public.Post.create({ ++ title, ++ content: null, ++ published: false, ++ authorId: user.id, ++ }); ++ return { user, post }; ++ }); +``` + +You get the same atomicity, plus something the array form never had: one query's result (here `user.id`) can feed the next query in the same transaction. + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers transactions. Prompts that map to each section: + +- "Using the prisma-next-queries skill, wrap this signup flow (create user, create welcome post) in a db.transaction so both writes commit together." +- "Check this transaction callback for queries that use db instead of tx." +- "Move the email send out of this transaction callback so it only runs after commit." +- "This project is on MongoDB. Show me the driver-session pattern for an atomic two-collection write with a shared MongoClient." + +## Next + +- [Write data](/orm/next/fundamentals/writing-data): the single-record and bulk mutations you group in a transaction. +- [Use advanced queries](/orm/next/fundamentals/advanced-queries) to run SQL builder plans inside or outside transactions. diff --git a/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx new file mode 100644 index 0000000000..09081d9acf --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx @@ -0,0 +1,324 @@ +--- +title: Writing data +description: Create, update, delete, and upsert records with Prisma Next, one at a time or in bulk. +url: /orm/next/fundamentals/writing-data +metaTitle: Writing data with Prisma Next +metaDescription: Create, update, delete, and upsert records in PostgreSQL and MongoDB with Prisma Next, and use createAll, updateAll, and deleteAll for bulk writes. +--- + +This page shows how to write data with Prisma Next: [creating](#create-one-record), [updating](#update-one-record), [deleting](#delete-one-record), and [upserting](#upsert-a-record) single records, and [writing many records at once](#write-many-records) with the `All` and `Count` variants. + +## Example schema + +All examples on this page are based on the following schema: + +
+ +Expand for sample schema + +```prisma tab="PostgreSQL" +model User { + id String @id @default(cuid(2)) + email String @unique + name String? + createdAt DateTime @default(now()) + posts Post[] +} + +model Post { + id String @id @default(cuid(2)) + title String + content String? + published Boolean + authorId String + author User @relation(fields: [authorId], references: [id]) + createdAt DateTime @default(now()) +} +``` + +```prisma tab="MongoDB" +model User { + id ObjectId @id @map("_id") + email String @unique + name String? + createdAt DateTime @default(now()) + posts Post[] + @@map("users") +} + +model Post { + id ObjectId @id @map("_id") + title String + content String? + published Boolean + author User @relation(fields: [authorId], references: [id]) + authorId ObjectId + createdAt DateTime @default(now()) + @@map("posts") +} +``` + +
+ +## Create one record + +Use `.create(...)` to insert one record. Pass the fields directly, and Prisma Next returns the inserted record, including generated values such as IDs and database defaults: + +```typescript tab="PostgreSQL" +import { db } from "./prisma/db"; + +const user = await db.orm.public.User.create({ + email: "jane@prisma.io", + name: "Jane", +}); +// user.id and user.createdAt are filled in +``` + +```typescript tab="MongoDB" +import { db } from "./prisma/db"; + +const user = await db.orm.users.create({ + email: "jane@prisma.io", + name: "Jane", + createdAt: new Date(), +}); +// user._id is filled in by the server +``` + +The returned record is complete, so you can use the generated values right away: + +```js no-copy +{ + id: 'cuid20000000000000000003', + email: 'jane@prisma.io', + name: 'Jane', + createdAt: 2026-07-06T09:09:56.119Z +} +``` + +To get back only some fields, chain `.select(...)` before `.create(...)`. The insert is the same; only the returned shape narrows: + +```typescript +const account = await db.orm.public.User + .select("id", "email") + .create({ email: "jane@prisma.io", name: "Jane" }); +``` + +```js no-copy +{ id: 'cuid20000000000000000003', email: 'jane@prisma.io' } +``` + +For Prisma 7 users, there is no `data` wrapper: + +```diff +- const user = await prisma.user.create({ data: { email: "jane@prisma.io", name: "Jane" } }); ++ const user = await db.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" }); +``` + +:::note + +On MongoDB, pass timestamp fields such as `createdAt` explicitly. `@default(now())` from the contract is not applied at create time yet. On PostgreSQL the database fills them in. + +::: + +## Update one record + +Use `.where(...)` to pick the record, then `.update(...)` with the fields to change. It updates **one** matching record and returns it: + +```typescript tab="PostgreSQL" +const updatedUser = await db.orm.public.User + .where({ email: "jane@prisma.io" }) + .update({ name: "Jane Doe" }); +``` + +```typescript tab="MongoDB" +const updatedUser = await db.orm.users + .where({ email: "jane@prisma.io" }) + .update({ name: "Jane Doe" }); +``` + +```js no-copy +{ + id: 'cuid20000000000000000003', + email: 'jane@prisma.io', + name: 'Jane Doe', + createdAt: 2026-07-06T09:09:56.119Z +} +``` + +If the filter can match more than one record, `.update(...)` still changes only one. To change every match, use [`updateAll` or `updateCount`](#write-many-records). + +On MongoDB, you can also update with field operations such as `.set(...)`, `.inc(...)`, and `.push(...)` by passing a callback: + +```typescript +await db.orm.posts + .where({ title: "Draft thoughts" }) + .update((p) => [p.content.set("Now filled in")]); +``` + +## Delete one record + +Use `.where(...)` then `.delete()`. It deletes **one** matching record and returns it: + +```typescript tab="PostgreSQL" +const deletedUser = await db.orm.public.User + .where({ email: "jane@prisma.io" }) + .delete(); +``` + +```typescript tab="MongoDB" +const deletedUser = await db.orm.users + .where({ email: "jane@prisma.io" }) + .delete(); +``` + +To delete every match, use [`deleteAll` or `deleteCount`](#write-many-records). + +## Upsert a record + +Use `.upsert(...)` to update a record if it exists and create it otherwise. Pass the two branches separately: + +```typescript tab="PostgreSQL" +await db.orm.public.User.upsert({ + create: { email: "eve@prisma.io", name: "Eve" }, + update: { name: "Eve Exists" }, +}); +``` + +```typescript tab="MongoDB" +await db.orm.users.where({ email: "eve@prisma.io" }).upsert({ + create: { email: "eve@prisma.io", name: "Eve", createdAt: new Date() }, + update: { name: "Eve Exists" }, +}); +``` + +On PostgreSQL, the record is matched on the model's unique fields (here `email`). On MongoDB, put the match in `.where(...)` before `.upsert(...)`. + +## Write many records + +Use the `All` and `Count` variants when you intend to affect every matching record: + +```typescript +// Insert many records +const newPosts = await db.orm.public.Post.createAll([ + { title: "One", content: null, published: false, authorId: user.id }, + { title: "Two", content: null, published: false, authorId: user.id }, +]); + +// Update every match +const updatedCount = await db.orm.public.Post + .where({ published: false }) + .updateCount({ published: true }); + +// Delete every match +const deletedCount = await db.orm.public.Post + .where((p) => p.title.ilike("draft%")) + .deleteCount(); +``` + +The `Count` variants return plain numbers: + +```js no-copy +updatedCount: 3 +deletedCount: 3 +``` + +The bulk methods work the same on MongoDB, on the collection roots (`db.orm.posts`). + +## Return rows or counts + +Each mutation comes in three forms. Pick by what you need back: + +| Form | Affects | Returns | +| --- | --- | --- | +| `create`, `update`, `delete` | one record | the affected record | +| `createAll`, `updateAll`, `deleteAll` | every match | the affected records | +| `createCount`, `updateCount`, `deleteCount` | every match | the number affected | + +The `Count` forms skip re-reading the affected records, so prefer them for large batches. The `All` forms return their records as a result you can `await` into an array or [stream with `for await`](/orm/next/fundamentals/reading-data#stream-large-results): + +```typescript +const publishedPosts = await db.orm.public.Post + .where({ published: false }) + .updateAll({ published: true }); +``` + +```js no-copy +[ + { id: 'cuid20000000000000000101', title: 'One', published: true, /* ... */ }, + { id: 'cuid20000000000000000102', title: 'Two', published: true, /* ... */ } +] +``` + +## Common mistakes + +### Updating or deleting more than one record + +You filtered on a non-unique field and expected every match to change: + +```typescript +await db.orm.public.Post.where({ published: false }).update({ published: true }); +``` + +`.update(...)` and `.delete()` only change one record. Even when the filter matches many, Prisma Next updates or deletes a single matching record, and the rest stay as they were. + +When you intend to affect every match, say so with the bulk variants: + +```typescript +const updatedCount = await db.orm.public.Post + .where({ published: false }) + .updateCount({ published: true }); +``` + +Use `updateAll` or `deleteAll` when you also need the changed records back, and `updateCount` or `deleteCount` when the number is enough. The singular forms stay safe for the one-record case: they can never fan out further than you expected. + +### Wrapping create fields in a data object + +You wrote the Prisma 7 shape, and it fails type-checking, because your contract has no field named `data`: + +```diff +- await db.orm.public.User.create({ data: { email, name } }); ++ await db.orm.public.User.create({ email, name }); +``` + +The shape you pass is the shape of the record, which is also why the return value needs no unwrapping. + +### Updating or deleting without a filter + +You called `.update(...)` or `.delete()` straight on the model: + +```typescript +await db.orm.public.User.delete(); +``` + +Both mutations require a `.where(...)` first, and the types reject the call without one. This is deliberate: there is no accidental way to write "delete a record, whichever one". If you truly mean every record, write the filter that says so and use `deleteAll`. + +### Running related writes back to back + +You created a user, then created their first post as a second await: + +```typescript +const user = await db.orm.public.User.create({ email, name }); +const post = await db.orm.public.Post.create({ title, published: false, authorId: user.id }); +``` + +If the second write fails, the first has already committed, and you're left with half the operation. When writes must succeed together, run them in a [transaction](/orm/next/fundamentals/transactions): one callback, one commit, or one rollback. + +### Passing an array of queries to a transaction + +Prisma 7 supported `$transaction([query1, query2])`. Prisma Next does not: there is no `$transaction`, and queries don't queue up in arrays. Put the calls inside one `db.transaction(...)` callback instead; the [Transactions page](/orm/next/fundamentals/transactions) shows the pattern. + +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent; the `prisma-next-queries` skill covers everything on this page. Prompts that map to each section: + +- "Using the prisma-next-queries skill, add a signup function that creates a User and returns only its id and email." +- "Write an upsert that creates a user by email or updates their name if they exist." +- "This cleanup script must delete every draft older than 30 days. Use the bulk delete variant and log how many records were removed." +- "Review my mutations for places where .update() should be updateAll or updateCount." + +## Next + +- [Run several writes atomically](/orm/next/fundamentals/transactions) with `db.transaction(...)`. +- [Read data](/orm/next/fundamentals/reading-data) to filter, sort, paginate, and select fields from your models. +- [Use the SQL builder](/orm/next/fundamentals/advanced-queries#postgresql-sql-query-builder) for inserts and updates with explicit `RETURNING` clauses. diff --git a/apps/docs/content/docs/orm/next/index.mdx b/apps/docs/content/docs/orm/next/index.mdx index 57db7497a0..2dbafc33b2 100644 --- a/apps/docs/content/docs/orm/next/index.mdx +++ b/apps/docs/content/docs/orm/next/index.mdx @@ -76,6 +76,14 @@ Prisma Next is in active development, and full conceptual and reference document > Scaffold a new Prisma Next app or add it to an existing project. + } + > + Read, write, and relate data with the ORM, run transactions, and reach for + advanced queries when you need them. + >; + "relation-one-to-one": relationOneToOne, + "relation-one-to-many": relationOneToMany, + "relation-many-to-many": relationManyToMany, +} satisfies Record; + +export type FlowName = keyof typeof FLOW_SCENES; diff --git a/apps/docs/src/components/concept-animation/index.tsx b/apps/docs/src/components/concept-animation/index.tsx index f0d3d2c03d..71802383d8 100644 --- a/apps/docs/src/components/concept-animation/index.tsx +++ b/apps/docs/src/components/concept-animation/index.tsx @@ -1,4 +1,4 @@ -import { FLOW_SCENES } from "./flow-presets"; +import { FLOW_SCENES, type FlowName } from "./flow-presets"; import { FlowPlayer } from "./flow"; import { ConceptPlayer } from "./player"; import { CONCEPT_PRESETS, type ConceptName, parseStepTokens } from "./presets"; @@ -12,11 +12,11 @@ import { CONCEPT_PRESETS, type ConceptName, parseStepTokens } from "./presets"; * other name falls back to the Code Hike token animation in presets.ts. Either * way the surrounding layout never shifts as you step through. */ -export function ConceptAnimation({ name }: { name: ConceptName }) { - const scene = FLOW_SCENES[name]; +export function ConceptAnimation({ name }: { name: ConceptName | FlowName }) { + const scene = (FLOW_SCENES as Partial>)[name]; if (scene) return ; - const preset = CONCEPT_PRESETS[name]; + const preset = (CONCEPT_PRESETS as Partial>)[name]; if (!preset) throw new Error(`Unknown concept animation: ${String(name)}`); const steps = preset.steps.map((step) => ({ ...parseStepTokens(step.code),