From e2b2984fc7b1d3e693b16e7983595c0ebd5ecf2a Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:23:14 +0200 Subject: [PATCH 1/6] docs(next): add Fundamentals section with tested Postgres/MongoDB examples (DR-8681) Adds the five Prisma Next Fundamentals pages (reading data, writing data, relations and joins, transactions, advanced queries) under /docs/next/fundamentals, wires them into the sidebar and the Prisma Next landing page, and adds the temporary Prisma 7 -> Prisma Next redirects from the DR-8681 table to apps/docs/vercel.json. Every code sample was validated against @prisma-next 0.14.0: PostgreSQL via a create-db database, MongoDB via mongodb-memory-server (replica set). Co-Authored-By: Claude Fable 5 --- .gitignore | 3 + apps/docs/content/docs/(index)/meta.json | 1 + .../next/fundamentals/advanced-queries.mdx | 142 ++++++++++++++ .../docs/(index)/next/fundamentals/meta.json | 10 + .../next/fundamentals/reading-data.mdx | 174 ++++++++++++++++++ .../next/fundamentals/relations-and-joins.mdx | 98 ++++++++++ .../next/fundamentals/transactions.mdx | 115 ++++++++++++ .../next/fundamentals/writing-data.mdx | 154 ++++++++++++++++ apps/docs/content/docs/(index)/next/index.mdx | 22 +++ apps/docs/vercel.json | 45 +++++ 10 files changed, 764 insertions(+) create mode 100644 apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx create mode 100644 apps/docs/content/docs/(index)/next/fundamentals/meta.json create mode 100644 apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx create mode 100644 apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx create mode 100644 apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx create mode 100644 apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx 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)/meta.json b/apps/docs/content/docs/(index)/meta.json index c7375e791a..010c21ef0a 100644 --- a/apps/docs/content/docs/(index)/meta.json +++ b/apps/docs/content/docs/(index)/meta.json @@ -11,6 +11,7 @@ "---Prisma Next---", "next/quickstart", "next/add-to-existing-project", + "next/fundamentals", "---Prisma ORM---", "...prisma-orm", "---Prisma Postgres---", diff --git a/apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx new file mode 100644 index 0000000000..34fe86f4be --- /dev/null +++ b/apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx @@ -0,0 +1,142 @@ +--- +title: Advanced queries +description: Drop below the ORM when a query needs shapes the ORM can't express, with the SQL builder on PostgreSQL and the aggregation pipeline builder on MongoDB. +url: /next/fundamentals/advanced-queries +metaTitle: Advanced queries in Prisma Next +metaDescription: Use the Prisma Next SQL builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB. +--- + +When the ORM can't express a query, drop one level: the SQL builder on PostgreSQL, the aggregation pipeline builder on MongoDB. Both stay fully typed against your contract; neither means writing raw strings. + +The ORM remains the default. Reach for these lanes for explicit joins, computed projections, grouped top-N queries, and pipeline stages. The choice is per query, not per app. + +## The SQL builder (PostgreSQL) + +`db.sql.public.` builds a *plan*: a typed, serializable description of one SQL statement. Tables use their storage names (lowercase), and you execute the plan through 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 rows = await db.runtime().execute(plan); +``` + +The `.where(...)` callback receives `(fields, fns)`: `fields` holds the column references, `fns` the operator namespace (`eq`, `ne`, `gt`, `lt`, `and`, `count`, and extension-provided operators). + +### Explicit joins + +The ORM does not express arbitrary joins. Alias each side with `.as(...)`, join on any predicate, and project columns from both: + +```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 rows = await db.runtime().execute(plan); +``` + +This is also the workaround for reading through a many-to-many junction table, which [`.include(...)` does not handle yet](/next/fundamentals/relations-and-joins#limitations). + +### Grouped top-N aggregates + +The ORM's `.groupBy(...).aggregate(...)` cannot order or limit the grouped result at the database. The SQL builder can, so use it for "top authors by post count" shapes: + +```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 rows = await db.runtime().execute(plan); +// PostgreSQL returns count as a string; convert with Number(row.posts) +``` + +### Writes with RETURNING + +SQL builder writes take an array of rows and expose `.returning(...)` explicitly: + +```typescript +const plan = db.sql.public.user + .insert([{ email: "sql@prisma.io" }]) + .returning("id", "email") + .build(); + +const [row] = await db.runtime().execute(plan); +// Contract defaults such as generated ids are applied to the inserted rows +``` + +There is no raw-SQL escape hatch (`db.sql.raw` does not exist) and no TypedSQL. If the builder can't express a shape you need, that is worth [reporting](https://pris.ly/discord). + +## The pipeline builder (MongoDB) + +MongoDB reads below the ORM are typed aggregation pipelines. Start with `db.query.from(...)`, chain stages, `.build()`, and execute through the runtime. This is also where MongoDB aggregation lives: the MongoDB ORM has no `.aggregate(...)` or `.groupBy(...)`. + +```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 byAuthor = await runtime.execute(plan); +``` + +`.match(...)` filters with typed field accessors, and `.lookup(...)` gives you a compile-time-checked `$lookup` join: + +```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 rows = await runtime.execute(plan); +``` + +Accumulators (`acc.count()`, `acc.max(...)`) and expression helpers import from `@prisma-next/mongo-query-builder`. + +## How to choose + +| You need | Use | +| --- | --- | +| CRUD, filters, relations, simple aggregates | ORM (`db.orm`) | +| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL 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, `(await db.runtime()).execute(plan)` on MongoDB, and `tx.execute(plan)` inside a [transaction](/next/fundamentals/transactions). + +## Next + +- [Reading data](/next/fundamentals/reading-data): the ORM happy path these lanes back up. +- [Transactions](/next/fundamentals/transactions): run SQL builder plans atomically. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/meta.json b/apps/docs/content/docs/(index)/next/fundamentals/meta.json new file mode 100644 index 0000000000..9e7dba847a --- /dev/null +++ b/apps/docs/content/docs/(index)/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/(index)/next/fundamentals/reading-data.mdx b/apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx new file mode 100644 index 0000000000..f6266d2f9c --- /dev/null +++ b/apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx @@ -0,0 +1,174 @@ +--- +title: Reading data +description: Read data with the Prisma Next ORM by chaining query methods and running them with .all() or .first(). +url: /next/fundamentals/reading-data +metaTitle: Reading data with Prisma Next +metaDescription: Query PostgreSQL and MongoDB with the Prisma Next ORM. Filter with where, project with select, sort with orderBy, and paginate with take and skip. +--- + +Read data by chaining query methods on a model collection, then running the chain with a terminal: `.all()` for many rows, `.first()` for one. + + + + +```typescript +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 +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(); +``` + + + + +## How it works in Prisma Next + +A query states what you want, the ORM runs it, and the result is typed against your [contract](/orm/next). There is no `findMany`, `findUnique`, or `findFirst`: if you are coming from Prisma 7, `.where(...).all()` replaces `findMany` and `.where(...).first()` replaces `findFirst` and `findUnique`. + +The model access path differs by database: + +- On PostgreSQL, models are addressed by namespace and PascalCase model name: `db.orm.public.Post`. `public` is the default PostgreSQL schema; contracts with more namespaces expose each one the same way. +- On MongoDB, collections are addressed by their lowercased plural root from the contract: `db.orm.posts`. Documents keep their raw `_id`, so filter on `_id`, not `id`. + +`.first()` returns the row or `null`. On PostgreSQL it issues `LIMIT 1`, and `db.orm.public.User.first({ id })` is a shorthand for primary-key lookups. `.all()` has no implicit limit, so reserve it for the genuine many case. + +## Filter with where + +`.where(...)` narrows the result. Pass an object to match fields by equality. Chained `.where(...)` calls AND-compose, which is also how you express ranges. + + + + +On PostgreSQL, `.where(...)` also takes a lambda over a field proxy for richer operators: `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte`, `.like`, `.ilike`, `.in([...])`, `.isNull()`, `.isNotNull()`. + +```typescript +// Object form: equality on named fields +const drafts = await db.orm.public.Post.where({ published: false }).all(); + +// Lambda form: full operator set +const matches = await db.orm.public.Post.where((p) => p.title.ilike("%prisma%")).all(); + +// A range is two chained where clauses +const recent = await db.orm.public.Post + .where((p) => p.createdAt.gte(start)) + .where((p) => p.createdAt.lte(end)) + .all(); +``` + +There is no `.between(...)` operator. For `OR` and `NOT`, the `or`, `and`, and `not` combinators currently import from `@prisma-next/sql-orm-client`; they are planned to move to the `@prisma-next/postgres` facade. + + + + +On MongoDB, use the object form. Values are compared by equality and are codec-aware, so `ObjectId` fields accept both `ObjectId` values and strings. + +```typescript +const alicesPosts = await db.orm.posts + .where({ published: true }) + .where({ authorId: alice._id }) + .all(); +``` + +Richer operators (ranges, `in`, boolean logic) are not yet exposed on the MongoDB facade. When you need them, drop to the [aggregation pipeline builder](/next/fundamentals/advanced-queries). + + + + +## Choose fields with select + +`.select(...)` limits which fields come back, and the result type narrows to match. + + + + +```typescript +const users = await db.orm.public.User.select("id", "email").all(); +// users: Array<{ id: string; email: string }> +``` + + + + +```typescript +const users = await db.orm.users.select("_id", "email").all(); +// users: Array<{ _id: ObjectId; email: string }> +``` + + + + +## Sort and paginate + +`.orderBy(...)` sorts, `.take(n)` limits, and `.skip(n)` offsets. Chain them before the terminal. + + + + +```typescript +const page = await db.orm.public.Post + .orderBy((p) => p.createdAt.desc()) + .take(20) + .skip(20) + .all(); +``` + +Pass an array of lambdas for a composite sort. For stable pagination over large sets, follow `.orderBy(...)` with `.cursor({ createdAt: last.createdAt })` to resume from a known position instead of counting offsets. + + + + +```typescript +const page = await db.orm.posts + .orderBy({ createdAt: -1 }) + .take(20) + .skip(20) + .all(); +``` + +MongoDB sorts use the driver's direction values: `1` ascending, `-1` descending. + + + + +## Count rows + +There is no `.count()` terminal on the collection. On PostgreSQL, count through `.aggregate(...)`: + +```typescript +const result = await db.orm.public.Post + .where({ published: true }) + .aggregate((a) => ({ total: a.count() })); +// result.total: number +``` + +The MongoDB ORM does not expose `.aggregate(...)`. Count with a `$group` stage in the [aggregation pipeline builder](/next/fundamentals/advanced-queries) instead. + +## Common gotchas + +:::warning + +A query result is single-consumption. `await` buffers it into an array once; a second `await` on the same result throws `RUNTIME.ITERATOR_CONSUMED`. Store the array in a variable and reuse the variable. To stream rows one at a time instead of buffering, use `for await (const row of query.all())`. + +::: + +## Next + +- [Writing data](/next/fundamentals/writing-data): create, update, delete, and bulk writes. +- [Relations and joins](/next/fundamentals/relations-and-joins): read related records with `.include(...)`. +- [Advanced queries](/next/fundamentals/advanced-queries): the SQL builder and the MongoDB pipeline builder. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx b/apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx new file mode 100644 index 0000000000..4dabd2887e --- /dev/null +++ b/apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx @@ -0,0 +1,98 @@ +--- +title: Relations and joins +description: Read related records with .include() and refine each relation branch with its own where, select, orderBy, and take. +url: /next/fundamentals/relations-and-joins +metaTitle: Relations and joins in Prisma Next +metaDescription: Eager-load related records with the Prisma Next ORM. Includes join on PostgreSQL and lower to $lookup on MongoDB. +--- + +Read related records by adding `.include(...)` to a query. The related rows come back nested on the parent, typed to match. + + + + +```typescript +import { db } from "./prisma/db"; + +// Each published post with its author +const posts = await db.orm.public.Post + .where({ published: true }) + .include("author") + .all(); +// posts[0].author is the full User row +``` + + + + +```typescript +import { db } from "./prisma/db"; + +// Each published post with its author +const posts = await db.orm.posts + .where({ published: true }) + .include("author") + .all(); +// posts[0].author is the referenced user document +``` + + + + +## How it works in Prisma Next + +The relation name in `.include(...)` matches the field name in your contract, so `author` and `posts` come from the `User`/`Post` models, not from table names. What runs underneath differs by database: + +- On PostgreSQL, `.include(...)` compiles into a join on the foreign key. Nothing extra to configure for one level; nested includes use the `lateral` and `jsonAgg` capabilities, which the PostgreSQL adapter advertises by default. +- On MongoDB, `.include(...)` lowers to a `$lookup` stage against the referenced collection. This covers reference-style relations; embedded documents are part of the parent and need no include. + +Use `.include(...)` when the caller needs the related data in the same response. Skip it when you only need the foreign key, which is already on the row. + +## Refine the included branch + +The second argument to `.include(...)` is a callback that receives the relation as its own collection. Chain `.where`, `.select`, `.orderBy`, and `.take` on it, exactly like a top-level query. This is how you fetch "each user with their five newest posts" in one query: + +```typescript +const users = 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 refinement callback is PostgreSQL-tested; on MongoDB, start with the plain `.include("author")` form and drop to the [pipeline builder](/next/fundamentals/advanced-queries) with `$lookup` when you need to reshape the joined documents. + +## Filter parents by their relations + +On PostgreSQL, relation predicates let a `.where(...)` recurse into a relation: `.some(...)` matches parents with at least one matching child, `.none(...)` and `.every(...)` complete the set. + +```typescript +// Users who have at least one published post +const authors = await db.orm.public.User + .where((u) => u.posts.some((p) => p.published.eq(true))) + .all(); +``` + +On MongoDB, filter the child collection directly, or express the shape as a pipeline with `$lookup` and `$match`. + +## Limitations + +:::warning + +Many-to-many relations through a junction table are not wired into `.include(...)` yet. The relation appears in the contract types, but the query planner does not emit the two-step junction join. Traverse the junction table explicitly with the [SQL builder](/next/fundamentals/advanced-queries) until this lands. + +::: + +There is no automatic N+1 detection. If you loop over parents and query children inside the loop, that is one query per parent; one `.include(...)` on the parent query replaces the whole loop. + +## Next + +- [Advanced queries](/next/fundamentals/advanced-queries): explicit joins, junction-table traversal, and `$lookup` pipelines. +- [Reading data](/next/fundamentals/reading-data): filters, projection, sorting, and pagination. +- [Transactions](/next/fundamentals/transactions): group writes that touch several models. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx b/apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx new file mode 100644 index 0000000000..4c872b5a6c --- /dev/null +++ b/apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx @@ -0,0 +1,115 @@ +--- +title: Transactions +description: Run several writes so they all succeed or all fail together with db.transaction() on PostgreSQL, and the driver-session escape hatch on MongoDB. +url: /next/fundamentals/transactions +metaTitle: Transactions in Prisma Next +metaDescription: Group writes atomically with db.transaction() on PostgreSQL. MongoDB multi-document atomicity uses the driver session escape hatch. +--- + +Run several writes as one unit with `db.transaction(...)`: they all commit together, or a thrown error rolls them all back. + +```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 rows exist now; the callback's return value passes through +``` + +## How it works in Prisma Next + +`db.transaction(fn)` opens a database transaction and hands your callback a `tx` context. `tx.orm` mirrors `db.orm` but rides the open transaction, so every query inside the callback sees the same consistent state. The transaction commits when the callback returns and rolls back when it throws: + +```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 row was rolled back and does not exist +} +``` + +Use a transaction whenever a 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. Don't reach for it on single writes; every mutation is already atomic on its own. + +[SQL builder](/next/fundamentals/advanced-queries) plans join 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); +}); +``` + +Batch-as-array semantics from Prisma 7 (`$transaction([query1, query2])`) do not exist. Put the calls inside one `db.transaction(...)` callback instead. + +## On MongoDB + +The MongoDB facade does not expose `db.transaction(...)` yet. Prisma Next does not wrap MongoDB multi-document transactions, so state that plainly in your design: each ORM write is atomic per document, and there is no facade-level way to group them. + +When you need multi-document atomicity today, use the escape hatch: share one `MongoClient` between Prisma Next and your code, and run the grouped writes in a driver session. This is tested against a replica set, which MongoDB requires 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(); +``` + +The session writes bypass the contract type-checking that ORM writes get, so keep the escape hatch small: one function per atomic operation, with Prisma Next queries for everything around it. + +## Common gotchas + +:::warning + +Values created inside the callback are only durable after `db.transaction(...)` resolves. Return the ids you need from the callback rather than using them for side effects (emails, queue messages) inside it; if the transaction retries or rolls back, the side effect already happened. + +::: + +## Next + +- [Writing data](/next/fundamentals/writing-data): the mutations you group in a transaction. +- [Advanced queries](/next/fundamentals/advanced-queries): SQL builder plans inside and outside transactions. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx b/apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx new file mode 100644 index 0000000000..96ee2e5f30 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx @@ -0,0 +1,154 @@ +--- +title: Writing data +description: Create, update, delete, and upsert records with the Prisma Next ORM, including the bulk variants. +url: /next/fundamentals/writing-data +metaTitle: Writing data with Prisma Next +metaDescription: Create, update, delete, and upsert records in PostgreSQL and MongoDB with the Prisma Next ORM, and use createAll, updateAll, and deleteAll for bulk writes. +--- + +Write data with `.create(...)`, `.update(...)`, and `.delete()`. Each mutation returns the affected row, and each has bulk variants for many rows at once. + + + + +```typescript +import { db } from "./prisma/db"; + +// Create takes the row directly and returns it +const user = await db.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" }); + +// Update and delete filter first, then mutate +await db.orm.public.Post.where({ id }).update({ published: true }); +await db.orm.public.Post.where({ id }).delete(); +``` + + + + +```typescript +import { db } from "./prisma/db"; + +// Create takes the document directly and returns it, including the server-assigned _id +const user = await db.orm.users.create({ + email: "jane@prisma.io", + name: "Jane", + createdAt: new Date(), +}); + +// Update and delete filter first, then mutate +await db.orm.posts.where({ _id: id }).update({ published: true }); +await db.orm.posts.where({ _id: id }).delete(); +``` + +On MongoDB, `@default(now())` in the contract is not applied at create time today, so pass timestamp fields explicitly. On PostgreSQL the database fills them in. + + + + +## How it works in Prisma Next + +`.create(...)` takes the row as its argument, not a wrapper object, and returns the inserted row with database-assigned values filled in (generated ids, `now()` defaults). Chain `.select(...)` before `.create(...)` to narrow what comes back. + +`.update(...)` and `.delete()` require a preceding `.where(...)` and operate on **one** matching row, returning it. This is the single-record path. When your filter can match more than one row and you want them all changed, reach for the bulk variants below; the singular forms will not touch the rest. + +## Update one record + + + + +```typescript +const updated = await db.orm.public.User + .where({ email: "jane@prisma.io" }) + .update({ name: "Jane Doe" }); +// updated is the changed row +``` + + + + +A plain object replaces the named top-level fields. A callback gives you MongoDB field operations such as `.set(...)`, `.inc(...)`, and `.push(...)` on the field accessor. + +```typescript +// Replace fields +await db.orm.users.where({ email: "jane@prisma.io" }).update({ name: "Jane Doe" }); + +// Field operations +await db.orm.posts + .where({ title: "Draft thoughts" }) + .update((p) => [p.content.set("Now filled in")]); +``` + + + + +## Upsert + +`.upsert(...)` splits the create and update branches. On PostgreSQL it matches on the model's unique fields. On MongoDB the filter comes from the preceding `.where(...)`. + + + + +```typescript +await db.orm.public.User.upsert({ + create: { email: "eve@prisma.io", name: "Eve" }, + update: { name: "Eve Exists" }, +}); +``` + + + + +```typescript +await db.orm.users.where({ email: "eve@prisma.io" }).upsert({ + create: { email: "eve@prisma.io", name: "Eve", createdAt: new Date() }, + update: { name: "Eve Exists" }, +}); +``` + + + + +## Bulk writes + +Every mutation has an `All` variant that affects every match and returns the affected rows, and a `Count` variant that returns only the number affected. The `Count` forms skip re-reading full rows, so prefer them for large batches. + +The bulk surface is the same on PostgreSQL and MongoDB; the example below shows PostgreSQL model paths, MongoDB uses `db.orm.posts` and friends. + +```typescript +// Insert many: rows back, or only the count +const rows = await db.orm.public.Post.createAll([ + { title: "One", content: null, published: false, authorId: user.id }, + { title: "Two", content: null, published: false, authorId: user.id }, +]); +const inserted = await db.orm.public.Post.createCount([ + { title: "Three", content: null, published: false, authorId: user.id }, +]); + +// Update every match +const published = await db.orm.public.Post + .where({ published: false }) + .updateCount({ published: true }); + +// Delete every match +const removed = await db.orm.public.Post + .where((p) => p.title.ilike("draft%")) + .deleteCount(); +``` + +`updateAll(...)` and `deleteAll()` return the affected rows as an async-iterable result: `await` it for an array, or `for await` to stream. + +## Common gotchas + +:::warning + +`.update(...)` and `.delete()` change one row even when the filter matches many. If you meant "all matches", use `updateAll` / `updateCount` or `deleteAll` / `deleteCount`. When you review generated code (yours or an agent's), check this first. + +::: + +Multi-statement batching in the Prisma 7 style (`$transaction([call1, call2])`) does not exist. Wrap related writes in a [transaction](/next/fundamentals/transactions) on PostgreSQL. + +## Next + +- [Transactions](/next/fundamentals/transactions): make several writes succeed or fail together. +- [Reading data](/next/fundamentals/reading-data): verify what you wrote. +- [Advanced queries](/next/fundamentals/advanced-queries): SQL builder inserts with `RETURNING`. diff --git a/apps/docs/content/docs/(index)/next/index.mdx b/apps/docs/content/docs/(index)/next/index.mdx index 04fde5d939..95f56d6586 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/vercel.json b/apps/docs/vercel.json index aff111ca08..14f4514058 100644 --- a/apps/docs/vercel.json +++ b/apps/docs/vercel.json @@ -6084,6 +6084,51 @@ "source": "/docs/cli/console/platform", "destination": "/docs/cli/console", "permanent": true + }, + { + "source": "/docs/orm/prisma-client", + "destination": "/docs/next/fundamentals/reading-data", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/queries/crud", + "destination": "/docs/next/fundamentals/reading-data", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/queries/select-fields", + "destination": "/docs/next/fundamentals/reading-data", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/queries/filtering-and-sorting", + "destination": "/docs/next/fundamentals/reading-data", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/queries/pagination", + "destination": "/docs/next/fundamentals/reading-data", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/queries/aggregation-grouping-summarizing", + "destination": "/docs/next/fundamentals/reading-data", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/queries/relation-queries", + "destination": "/docs/next/fundamentals/relations-and-joins", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/queries/transactions", + "destination": "/docs/next/fundamentals/transactions", + "permanent": false + }, + { + "source": "/docs/orm/prisma-client/using-raw-sql", + "destination": "/docs/next/fundamentals/advanced-queries", + "permanent": false } ] } From 101699f616fcea4aebfeffe79f7780f043eed4c2 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:09:19 +0200 Subject: [PATCH 2/6] docs(next): move Fundamentals under /orm/next, fix code tabs, rewrite task-first - Move the five Fundamentals pages from /docs/next/fundamentals to /docs/orm/next/fundamentals so they live in the ORM "Next" version dropdown, and register the section in orm/next/meta.json. - Replace the broken JSX with the app's code-tab syntax (```lang tab="PostgreSQL"), which renders the styled code switcher. - Rewrite all pages task-first: each section starts with what to do and which API to use, shows the simplest example, states what it returns, and keeps caveats after the happy path. Implementation details (adapter capabilities, planner internals) removed from Fundamentals; limitations rephrased as user-facing guidance. Count-returning examples use count-named variables. - Update redirect destinations in vercel.json and landing-page cards to the new /docs/orm/next/fundamentals URLs. Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/(index)/meta.json | 1 - .../next/fundamentals/advanced-queries.mdx | 142 -------------- .../next/fundamentals/reading-data.mdx | 174 ------------------ .../next/fundamentals/relations-and-joins.mdx | 98 ---------- .../next/fundamentals/transactions.mdx | 115 ------------ .../next/fundamentals/writing-data.mdx | 154 ---------------- apps/docs/content/docs/(index)/next/index.mdx | 10 +- .../next/fundamentals/advanced-queries.mdx | 142 ++++++++++++++ .../next/fundamentals/meta.json | 0 .../orm/next/fundamentals/reading-data.mdx | 143 ++++++++++++++ .../next/fundamentals/relations-and-joins.mdx | 88 +++++++++ .../orm/next/fundamentals/transactions.mdx | 124 +++++++++++++ .../orm/next/fundamentals/writing-data.mdx | 160 ++++++++++++++++ apps/docs/content/docs/orm/next/index.mdx | 8 + apps/docs/content/docs/orm/next/meta.json | 4 +- apps/docs/vercel.json | 18 +- 16 files changed, 682 insertions(+), 699 deletions(-) delete mode 100644 apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx delete mode 100644 apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx delete mode 100644 apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx delete mode 100644 apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx delete mode 100644 apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx create mode 100644 apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx rename apps/docs/content/docs/{(index) => orm}/next/fundamentals/meta.json (100%) create mode 100644 apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx create mode 100644 apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx create mode 100644 apps/docs/content/docs/orm/next/fundamentals/transactions.mdx create mode 100644 apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx diff --git a/apps/docs/content/docs/(index)/meta.json b/apps/docs/content/docs/(index)/meta.json index 010c21ef0a..c7375e791a 100644 --- a/apps/docs/content/docs/(index)/meta.json +++ b/apps/docs/content/docs/(index)/meta.json @@ -11,7 +11,6 @@ "---Prisma Next---", "next/quickstart", "next/add-to-existing-project", - "next/fundamentals", "---Prisma ORM---", "...prisma-orm", "---Prisma Postgres---", diff --git a/apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx deleted file mode 100644 index 34fe86f4be..0000000000 --- a/apps/docs/content/docs/(index)/next/fundamentals/advanced-queries.mdx +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Advanced queries -description: Drop below the ORM when a query needs shapes the ORM can't express, with the SQL builder on PostgreSQL and the aggregation pipeline builder on MongoDB. -url: /next/fundamentals/advanced-queries -metaTitle: Advanced queries in Prisma Next -metaDescription: Use the Prisma Next SQL builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB. ---- - -When the ORM can't express a query, drop one level: the SQL builder on PostgreSQL, the aggregation pipeline builder on MongoDB. Both stay fully typed against your contract; neither means writing raw strings. - -The ORM remains the default. Reach for these lanes for explicit joins, computed projections, grouped top-N queries, and pipeline stages. The choice is per query, not per app. - -## The SQL builder (PostgreSQL) - -`db.sql.public.
` builds a *plan*: a typed, serializable description of one SQL statement. Tables use their storage names (lowercase), and you execute the plan through 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 rows = await db.runtime().execute(plan); -``` - -The `.where(...)` callback receives `(fields, fns)`: `fields` holds the column references, `fns` the operator namespace (`eq`, `ne`, `gt`, `lt`, `and`, `count`, and extension-provided operators). - -### Explicit joins - -The ORM does not express arbitrary joins. Alias each side with `.as(...)`, join on any predicate, and project columns from both: - -```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 rows = await db.runtime().execute(plan); -``` - -This is also the workaround for reading through a many-to-many junction table, which [`.include(...)` does not handle yet](/next/fundamentals/relations-and-joins#limitations). - -### Grouped top-N aggregates - -The ORM's `.groupBy(...).aggregate(...)` cannot order or limit the grouped result at the database. The SQL builder can, so use it for "top authors by post count" shapes: - -```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 rows = await db.runtime().execute(plan); -// PostgreSQL returns count as a string; convert with Number(row.posts) -``` - -### Writes with RETURNING - -SQL builder writes take an array of rows and expose `.returning(...)` explicitly: - -```typescript -const plan = db.sql.public.user - .insert([{ email: "sql@prisma.io" }]) - .returning("id", "email") - .build(); - -const [row] = await db.runtime().execute(plan); -// Contract defaults such as generated ids are applied to the inserted rows -``` - -There is no raw-SQL escape hatch (`db.sql.raw` does not exist) and no TypedSQL. If the builder can't express a shape you need, that is worth [reporting](https://pris.ly/discord). - -## The pipeline builder (MongoDB) - -MongoDB reads below the ORM are typed aggregation pipelines. Start with `db.query.from(...)`, chain stages, `.build()`, and execute through the runtime. This is also where MongoDB aggregation lives: the MongoDB ORM has no `.aggregate(...)` or `.groupBy(...)`. - -```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 byAuthor = await runtime.execute(plan); -``` - -`.match(...)` filters with typed field accessors, and `.lookup(...)` gives you a compile-time-checked `$lookup` join: - -```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 rows = await runtime.execute(plan); -``` - -Accumulators (`acc.count()`, `acc.max(...)`) and expression helpers import from `@prisma-next/mongo-query-builder`. - -## How to choose - -| You need | Use | -| --- | --- | -| CRUD, filters, relations, simple aggregates | ORM (`db.orm`) | -| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL 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, `(await db.runtime()).execute(plan)` on MongoDB, and `tx.execute(plan)` inside a [transaction](/next/fundamentals/transactions). - -## Next - -- [Reading data](/next/fundamentals/reading-data): the ORM happy path these lanes back up. -- [Transactions](/next/fundamentals/transactions): run SQL builder plans atomically. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx b/apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx deleted file mode 100644 index f6266d2f9c..0000000000 --- a/apps/docs/content/docs/(index)/next/fundamentals/reading-data.mdx +++ /dev/null @@ -1,174 +0,0 @@ ---- -title: Reading data -description: Read data with the Prisma Next ORM by chaining query methods and running them with .all() or .first(). -url: /next/fundamentals/reading-data -metaTitle: Reading data with Prisma Next -metaDescription: Query PostgreSQL and MongoDB with the Prisma Next ORM. Filter with where, project with select, sort with orderBy, and paginate with take and skip. ---- - -Read data by chaining query methods on a model collection, then running the chain with a terminal: `.all()` for many rows, `.first()` for one. - - - - -```typescript -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 -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(); -``` - - - - -## How it works in Prisma Next - -A query states what you want, the ORM runs it, and the result is typed against your [contract](/orm/next). There is no `findMany`, `findUnique`, or `findFirst`: if you are coming from Prisma 7, `.where(...).all()` replaces `findMany` and `.where(...).first()` replaces `findFirst` and `findUnique`. - -The model access path differs by database: - -- On PostgreSQL, models are addressed by namespace and PascalCase model name: `db.orm.public.Post`. `public` is the default PostgreSQL schema; contracts with more namespaces expose each one the same way. -- On MongoDB, collections are addressed by their lowercased plural root from the contract: `db.orm.posts`. Documents keep their raw `_id`, so filter on `_id`, not `id`. - -`.first()` returns the row or `null`. On PostgreSQL it issues `LIMIT 1`, and `db.orm.public.User.first({ id })` is a shorthand for primary-key lookups. `.all()` has no implicit limit, so reserve it for the genuine many case. - -## Filter with where - -`.where(...)` narrows the result. Pass an object to match fields by equality. Chained `.where(...)` calls AND-compose, which is also how you express ranges. - - - - -On PostgreSQL, `.where(...)` also takes a lambda over a field proxy for richer operators: `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte`, `.like`, `.ilike`, `.in([...])`, `.isNull()`, `.isNotNull()`. - -```typescript -// Object form: equality on named fields -const drafts = await db.orm.public.Post.where({ published: false }).all(); - -// Lambda form: full operator set -const matches = await db.orm.public.Post.where((p) => p.title.ilike("%prisma%")).all(); - -// A range is two chained where clauses -const recent = await db.orm.public.Post - .where((p) => p.createdAt.gte(start)) - .where((p) => p.createdAt.lte(end)) - .all(); -``` - -There is no `.between(...)` operator. For `OR` and `NOT`, the `or`, `and`, and `not` combinators currently import from `@prisma-next/sql-orm-client`; they are planned to move to the `@prisma-next/postgres` facade. - - - - -On MongoDB, use the object form. Values are compared by equality and are codec-aware, so `ObjectId` fields accept both `ObjectId` values and strings. - -```typescript -const alicesPosts = await db.orm.posts - .where({ published: true }) - .where({ authorId: alice._id }) - .all(); -``` - -Richer operators (ranges, `in`, boolean logic) are not yet exposed on the MongoDB facade. When you need them, drop to the [aggregation pipeline builder](/next/fundamentals/advanced-queries). - - - - -## Choose fields with select - -`.select(...)` limits which fields come back, and the result type narrows to match. - - - - -```typescript -const users = await db.orm.public.User.select("id", "email").all(); -// users: Array<{ id: string; email: string }> -``` - - - - -```typescript -const users = await db.orm.users.select("_id", "email").all(); -// users: Array<{ _id: ObjectId; email: string }> -``` - - - - -## Sort and paginate - -`.orderBy(...)` sorts, `.take(n)` limits, and `.skip(n)` offsets. Chain them before the terminal. - - - - -```typescript -const page = await db.orm.public.Post - .orderBy((p) => p.createdAt.desc()) - .take(20) - .skip(20) - .all(); -``` - -Pass an array of lambdas for a composite sort. For stable pagination over large sets, follow `.orderBy(...)` with `.cursor({ createdAt: last.createdAt })` to resume from a known position instead of counting offsets. - - - - -```typescript -const page = await db.orm.posts - .orderBy({ createdAt: -1 }) - .take(20) - .skip(20) - .all(); -``` - -MongoDB sorts use the driver's direction values: `1` ascending, `-1` descending. - - - - -## Count rows - -There is no `.count()` terminal on the collection. On PostgreSQL, count through `.aggregate(...)`: - -```typescript -const result = await db.orm.public.Post - .where({ published: true }) - .aggregate((a) => ({ total: a.count() })); -// result.total: number -``` - -The MongoDB ORM does not expose `.aggregate(...)`. Count with a `$group` stage in the [aggregation pipeline builder](/next/fundamentals/advanced-queries) instead. - -## Common gotchas - -:::warning - -A query result is single-consumption. `await` buffers it into an array once; a second `await` on the same result throws `RUNTIME.ITERATOR_CONSUMED`. Store the array in a variable and reuse the variable. To stream rows one at a time instead of buffering, use `for await (const row of query.all())`. - -::: - -## Next - -- [Writing data](/next/fundamentals/writing-data): create, update, delete, and bulk writes. -- [Relations and joins](/next/fundamentals/relations-and-joins): read related records with `.include(...)`. -- [Advanced queries](/next/fundamentals/advanced-queries): the SQL builder and the MongoDB pipeline builder. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx b/apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx deleted file mode 100644 index 4dabd2887e..0000000000 --- a/apps/docs/content/docs/(index)/next/fundamentals/relations-and-joins.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Relations and joins -description: Read related records with .include() and refine each relation branch with its own where, select, orderBy, and take. -url: /next/fundamentals/relations-and-joins -metaTitle: Relations and joins in Prisma Next -metaDescription: Eager-load related records with the Prisma Next ORM. Includes join on PostgreSQL and lower to $lookup on MongoDB. ---- - -Read related records by adding `.include(...)` to a query. The related rows come back nested on the parent, typed to match. - - - - -```typescript -import { db } from "./prisma/db"; - -// Each published post with its author -const posts = await db.orm.public.Post - .where({ published: true }) - .include("author") - .all(); -// posts[0].author is the full User row -``` - - - - -```typescript -import { db } from "./prisma/db"; - -// Each published post with its author -const posts = await db.orm.posts - .where({ published: true }) - .include("author") - .all(); -// posts[0].author is the referenced user document -``` - - - - -## How it works in Prisma Next - -The relation name in `.include(...)` matches the field name in your contract, so `author` and `posts` come from the `User`/`Post` models, not from table names. What runs underneath differs by database: - -- On PostgreSQL, `.include(...)` compiles into a join on the foreign key. Nothing extra to configure for one level; nested includes use the `lateral` and `jsonAgg` capabilities, which the PostgreSQL adapter advertises by default. -- On MongoDB, `.include(...)` lowers to a `$lookup` stage against the referenced collection. This covers reference-style relations; embedded documents are part of the parent and need no include. - -Use `.include(...)` when the caller needs the related data in the same response. Skip it when you only need the foreign key, which is already on the row. - -## Refine the included branch - -The second argument to `.include(...)` is a callback that receives the relation as its own collection. Chain `.where`, `.select`, `.orderBy`, and `.take` on it, exactly like a top-level query. This is how you fetch "each user with their five newest posts" in one query: - -```typescript -const users = 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 refinement callback is PostgreSQL-tested; on MongoDB, start with the plain `.include("author")` form and drop to the [pipeline builder](/next/fundamentals/advanced-queries) with `$lookup` when you need to reshape the joined documents. - -## Filter parents by their relations - -On PostgreSQL, relation predicates let a `.where(...)` recurse into a relation: `.some(...)` matches parents with at least one matching child, `.none(...)` and `.every(...)` complete the set. - -```typescript -// Users who have at least one published post -const authors = await db.orm.public.User - .where((u) => u.posts.some((p) => p.published.eq(true))) - .all(); -``` - -On MongoDB, filter the child collection directly, or express the shape as a pipeline with `$lookup` and `$match`. - -## Limitations - -:::warning - -Many-to-many relations through a junction table are not wired into `.include(...)` yet. The relation appears in the contract types, but the query planner does not emit the two-step junction join. Traverse the junction table explicitly with the [SQL builder](/next/fundamentals/advanced-queries) until this lands. - -::: - -There is no automatic N+1 detection. If you loop over parents and query children inside the loop, that is one query per parent; one `.include(...)` on the parent query replaces the whole loop. - -## Next - -- [Advanced queries](/next/fundamentals/advanced-queries): explicit joins, junction-table traversal, and `$lookup` pipelines. -- [Reading data](/next/fundamentals/reading-data): filters, projection, sorting, and pagination. -- [Transactions](/next/fundamentals/transactions): group writes that touch several models. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx b/apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx deleted file mode 100644 index 4c872b5a6c..0000000000 --- a/apps/docs/content/docs/(index)/next/fundamentals/transactions.mdx +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Transactions -description: Run several writes so they all succeed or all fail together with db.transaction() on PostgreSQL, and the driver-session escape hatch on MongoDB. -url: /next/fundamentals/transactions -metaTitle: Transactions in Prisma Next -metaDescription: Group writes atomically with db.transaction() on PostgreSQL. MongoDB multi-document atomicity uses the driver session escape hatch. ---- - -Run several writes as one unit with `db.transaction(...)`: they all commit together, or a thrown error rolls them all back. - -```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 rows exist now; the callback's return value passes through -``` - -## How it works in Prisma Next - -`db.transaction(fn)` opens a database transaction and hands your callback a `tx` context. `tx.orm` mirrors `db.orm` but rides the open transaction, so every query inside the callback sees the same consistent state. The transaction commits when the callback returns and rolls back when it throws: - -```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 row was rolled back and does not exist -} -``` - -Use a transaction whenever a 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. Don't reach for it on single writes; every mutation is already atomic on its own. - -[SQL builder](/next/fundamentals/advanced-queries) plans join 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); -}); -``` - -Batch-as-array semantics from Prisma 7 (`$transaction([query1, query2])`) do not exist. Put the calls inside one `db.transaction(...)` callback instead. - -## On MongoDB - -The MongoDB facade does not expose `db.transaction(...)` yet. Prisma Next does not wrap MongoDB multi-document transactions, so state that plainly in your design: each ORM write is atomic per document, and there is no facade-level way to group them. - -When you need multi-document atomicity today, use the escape hatch: share one `MongoClient` between Prisma Next and your code, and run the grouped writes in a driver session. This is tested against a replica set, which MongoDB requires 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(); -``` - -The session writes bypass the contract type-checking that ORM writes get, so keep the escape hatch small: one function per atomic operation, with Prisma Next queries for everything around it. - -## Common gotchas - -:::warning - -Values created inside the callback are only durable after `db.transaction(...)` resolves. Return the ids you need from the callback rather than using them for side effects (emails, queue messages) inside it; if the transaction retries or rolls back, the side effect already happened. - -::: - -## Next - -- [Writing data](/next/fundamentals/writing-data): the mutations you group in a transaction. -- [Advanced queries](/next/fundamentals/advanced-queries): SQL builder plans inside and outside transactions. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx b/apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx deleted file mode 100644 index 96ee2e5f30..0000000000 --- a/apps/docs/content/docs/(index)/next/fundamentals/writing-data.mdx +++ /dev/null @@ -1,154 +0,0 @@ ---- -title: Writing data -description: Create, update, delete, and upsert records with the Prisma Next ORM, including the bulk variants. -url: /next/fundamentals/writing-data -metaTitle: Writing data with Prisma Next -metaDescription: Create, update, delete, and upsert records in PostgreSQL and MongoDB with the Prisma Next ORM, and use createAll, updateAll, and deleteAll for bulk writes. ---- - -Write data with `.create(...)`, `.update(...)`, and `.delete()`. Each mutation returns the affected row, and each has bulk variants for many rows at once. - - - - -```typescript -import { db } from "./prisma/db"; - -// Create takes the row directly and returns it -const user = await db.orm.public.User.create({ email: "jane@prisma.io", name: "Jane" }); - -// Update and delete filter first, then mutate -await db.orm.public.Post.where({ id }).update({ published: true }); -await db.orm.public.Post.where({ id }).delete(); -``` - - - - -```typescript -import { db } from "./prisma/db"; - -// Create takes the document directly and returns it, including the server-assigned _id -const user = await db.orm.users.create({ - email: "jane@prisma.io", - name: "Jane", - createdAt: new Date(), -}); - -// Update and delete filter first, then mutate -await db.orm.posts.where({ _id: id }).update({ published: true }); -await db.orm.posts.where({ _id: id }).delete(); -``` - -On MongoDB, `@default(now())` in the contract is not applied at create time today, so pass timestamp fields explicitly. On PostgreSQL the database fills them in. - - - - -## How it works in Prisma Next - -`.create(...)` takes the row as its argument, not a wrapper object, and returns the inserted row with database-assigned values filled in (generated ids, `now()` defaults). Chain `.select(...)` before `.create(...)` to narrow what comes back. - -`.update(...)` and `.delete()` require a preceding `.where(...)` and operate on **one** matching row, returning it. This is the single-record path. When your filter can match more than one row and you want them all changed, reach for the bulk variants below; the singular forms will not touch the rest. - -## Update one record - - - - -```typescript -const updated = await db.orm.public.User - .where({ email: "jane@prisma.io" }) - .update({ name: "Jane Doe" }); -// updated is the changed row -``` - - - - -A plain object replaces the named top-level fields. A callback gives you MongoDB field operations such as `.set(...)`, `.inc(...)`, and `.push(...)` on the field accessor. - -```typescript -// Replace fields -await db.orm.users.where({ email: "jane@prisma.io" }).update({ name: "Jane Doe" }); - -// Field operations -await db.orm.posts - .where({ title: "Draft thoughts" }) - .update((p) => [p.content.set("Now filled in")]); -``` - - - - -## Upsert - -`.upsert(...)` splits the create and update branches. On PostgreSQL it matches on the model's unique fields. On MongoDB the filter comes from the preceding `.where(...)`. - - - - -```typescript -await db.orm.public.User.upsert({ - create: { email: "eve@prisma.io", name: "Eve" }, - update: { name: "Eve Exists" }, -}); -``` - - - - -```typescript -await db.orm.users.where({ email: "eve@prisma.io" }).upsert({ - create: { email: "eve@prisma.io", name: "Eve", createdAt: new Date() }, - update: { name: "Eve Exists" }, -}); -``` - - - - -## Bulk writes - -Every mutation has an `All` variant that affects every match and returns the affected rows, and a `Count` variant that returns only the number affected. The `Count` forms skip re-reading full rows, so prefer them for large batches. - -The bulk surface is the same on PostgreSQL and MongoDB; the example below shows PostgreSQL model paths, MongoDB uses `db.orm.posts` and friends. - -```typescript -// Insert many: rows back, or only the count -const rows = await db.orm.public.Post.createAll([ - { title: "One", content: null, published: false, authorId: user.id }, - { title: "Two", content: null, published: false, authorId: user.id }, -]); -const inserted = await db.orm.public.Post.createCount([ - { title: "Three", content: null, published: false, authorId: user.id }, -]); - -// Update every match -const published = await db.orm.public.Post - .where({ published: false }) - .updateCount({ published: true }); - -// Delete every match -const removed = await db.orm.public.Post - .where((p) => p.title.ilike("draft%")) - .deleteCount(); -``` - -`updateAll(...)` and `deleteAll()` return the affected rows as an async-iterable result: `await` it for an array, or `for await` to stream. - -## Common gotchas - -:::warning - -`.update(...)` and `.delete()` change one row even when the filter matches many. If you meant "all matches", use `updateAll` / `updateCount` or `deleteAll` / `deleteCount`. When you review generated code (yours or an agent's), check this first. - -::: - -Multi-statement batching in the Prisma 7 style (`$transaction([call1, call2])`) does not exist. Wrap related writes in a [transaction](/next/fundamentals/transactions) on PostgreSQL. - -## Next - -- [Transactions](/next/fundamentals/transactions): make several writes succeed or fail together. -- [Reading data](/next/fundamentals/reading-data): verify what you wrote. -- [Advanced queries](/next/fundamentals/advanced-queries): SQL builder inserts with `RETURNING`. diff --git a/apps/docs/content/docs/(index)/next/index.mdx b/apps/docs/content/docs/(index)/next/index.mdx index 95f56d6586..695e6c1002 100644 --- a/apps/docs/content/docs/(index)/next/index.mdx +++ b/apps/docs/content/docs/(index)/next/index.mdx @@ -56,19 +56,19 @@ Start with the setup page when you want a guided first run. 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. 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..454aa7821a --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx @@ -0,0 +1,142 @@ +--- +title: Advanced queries +description: Use the SQL builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM can't express. +url: /orm/next/fundamentals/advanced-queries +metaTitle: Advanced queries in Prisma Next +metaDescription: Use the Prisma Next SQL builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB. +--- + +When the ORM can't express a query, drop one level: the SQL builder on PostgreSQL, or the pipeline builder on MongoDB. Both stay typed against your contract; neither means writing raw strings. + +The ORM remains the default. Reach for these builders for explicit joins, computed projections, grouped top-N queries, and aggregation pipelines. The choice is per query, not per app. + +## Query with the SQL builder (PostgreSQL) + +Use `db.sql.public.
` to build a query as a *plan*, then execute the plan through the runtime. Tables use their lowercase storage names: + +```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`, `and`, `count`, and operators added by extensions). + +### Join tables explicitly + +Use the SQL builder when you need a join the ORM doesn't express. Alias each side with `.as(...)`, join on any condition, and project columns from both sides: + +```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); +``` + +This is also how you read through a many-to-many junction table while [`.include(...)` does not support it](/orm/next/fundamentals/relations-and-joins#current-limitations): join the junction table to the far side explicitly. + +### Group and rank results + +Use the SQL builder for "top N groups" questions, such as the authors with the most posts. It can order and limit by an aggregate directly in the database, which the ORM's grouped aggregates can't: + +```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); +// 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: + +```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 +``` + +Prisma Next does not run raw SQL strings: every query goes through the typed builder. If the builder can't express a shape you need, [share the use case](https://pris.ly/discord). + +## Query with the pipeline builder (MongoDB) + +Use `db.query.from("")` to build a typed aggregation pipeline, then execute it through the runtime. This is also where MongoDB aggregation lives, since the MongoDB ORM has no `.aggregate(...)`: + +```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); +``` + +Use `.match(...)` to filter with typed field accessors, and `.lookup(...)` for a type-checked `$lookup` join: + +```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); +``` + +Accumulators such as `acc.count()` and `acc.max(...)` import from `@prisma-next/mongo-query-builder`. + +## Choose the right lane + +| You need | Use | +| --- | --- | +| CRUD, filters, relations, simple aggregates | ORM (`db.orm`) | +| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL 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)`. + +## Next + +- [Read data](/orm/next/fundamentals/reading-data): the ORM happy path these builders back up. +- [Run SQL builder plans atomically](/orm/next/fundamentals/transactions) inside a transaction. diff --git a/apps/docs/content/docs/(index)/next/fundamentals/meta.json b/apps/docs/content/docs/orm/next/fundamentals/meta.json similarity index 100% rename from apps/docs/content/docs/(index)/next/fundamentals/meta.json rename to apps/docs/content/docs/orm/next/fundamentals/meta.json 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..7240594deb --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx @@ -0,0 +1,143 @@ +--- +title: Reading data +description: Fetch one record or many with the Prisma Next ORM, then filter, select, sort, and paginate the results. +url: /orm/next/fundamentals/reading-data +metaTitle: Reading data with Prisma Next +metaDescription: Query PostgreSQL and MongoDB with the Prisma Next ORM. Filter with where, project with select, sort with orderBy, and paginate with take and skip. +--- + +Read data by chaining query methods on a model, then running the chain with `.all()` for many records or `.first()` for one. + +```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`). + +If you are coming from Prisma 7: `.where(...).all()` replaces `findMany`, and `.where(...).first()` replaces `findFirst` and `findUnique`. + +## Fetch many records or one + +Use `.all()` when you want every matching record. It returns an array and 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`, and on PostgreSQL it fetches at most one row. For a primary-key lookup on PostgreSQL, pass the key directly: + +```typescript +const user = await db.orm.public.User.first({ id: userId }); +``` + +On MongoDB, look up documents by their `_id` field: `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(); +``` + +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 +const matchingPosts = await db.orm.public.Post + .where((p) => p.title.ilike("%prisma%")) + .all(); +``` + +To combine conditions with OR or NOT on PostgreSQL, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`. + +On MongoDB, use the object form. Comparison operators and boolean logic are not available on MongoDB's `.where(...)` yet; for those queries, use the [pipeline builder](/orm/next/fundamentals/advanced-queries#query-with-the-pipeline-builder-mongodb). + +## 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(); +// Array<{ id: string; email: string }> +``` + +```typescript tab="MongoDB" +const users = await db.orm.users.select("_id", "email").all(); +// Array<{ _id: ObjectId; email: string }> +``` + +## Sort and paginate + +Use `.orderBy(...)` to sort, `.take(n)` to limit, and `.skip(n)` to offset: + +```typescript tab="PostgreSQL" +const page = await db.orm.public.Post + .orderBy((p) => p.createdAt.desc()) + .take(20) + .skip(20) + .all(); +``` + +```typescript tab="MongoDB" +const page = await db.orm.posts + .orderBy({ createdAt: -1 }) + .take(20) + .skip(20) + .all(); +``` + +PostgreSQL sorts with lambdas calling `.asc()` or `.desc()`; pass an array of lambdas for a composite sort. MongoDB sorts with the driver's direction values: `1` for ascending, `-1` for descending. + +For stable pagination over large tables on PostgreSQL, follow `.orderBy(...)` with `.cursor({ createdAt: last.createdAt })` to resume from a known position instead of counting offsets. + +## 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() })); +// result.total: number +``` + +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#query-with-the-pipeline-builder-mongodb). + +## Common mistakes + +:::warning + +A query result can only be consumed once. `await` buffers it into an array; a second `await` on the same result throws `RUNTIME.ITERATOR_CONSUMED`. Store the array in a variable and reuse the variable. + +::: + +- Using `.all()` for a single record. Use `.first()`: it returns one record or `null`, without fetching the rest. +- Expecting `.all()` to limit itself. It returns every match; add `.take(n)` when the table can grow. +- Loading a huge result into memory. Iterate it instead of awaiting: `for await (const row of query.all())`. + +## 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 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..43ac4c6e02 --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx @@ -0,0 +1,88 @@ +--- +title: Relations and joins +description: Read related records in one query with .include(), and shape what comes back per relation. +url: /orm/next/fundamentals/relations-and-joins +metaTitle: Relations and joins in Prisma Next +metaDescription: Eager-load related records with the Prisma Next ORM on PostgreSQL and MongoDB, select fields per relation, and filter parents by relation data. +--- + +Read related records in the same query by adding `.include(...)`. The related records come back nested on the parent, typed to match. + +## Include related records + +Use `.include("")` with the relation's field name from your contract: + +```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 +``` + +Use `.include(...)` when the caller needs the related data in the same response. Skip it when the foreign key on the record is enough. + +If you find yourself querying children inside a loop over parents, replace the loop with one `.include(...)` on the parent query: it turns one query per parent into a single query. + +## Select fields from included records + +Pass a callback as the second argument to shape what each relation returns. 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 }> }> +``` + +On MongoDB, start with the plain `.include("author")` form. When you need to reshape the joined documents, use a `$lookup` stage in the [pipeline builder](/orm/next/fundamentals/advanced-queries#query-with-the-pipeline-builder-mongodb). + +## Filter parent records by relation data + +On PostgreSQL, use relation predicates inside `.where(...)`: `.some(...)` matches parents with at least one matching child, `.none(...)` matches parents with no matching child, 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(); +``` + +On MongoDB, query the child collection directly, or express the shape as a pipeline with `$lookup` and `$match`. + +## Database-specific behavior + +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. + +## Current limitations + +:::warning + +Many-to-many relations are not supported by `.include(...)` yet. For now, query the junction table explicitly with the [SQL builder](/orm/next/fundamentals/advanced-queries#join-tables-explicitly). + +::: + +## Next + +- [Use advanced queries](/orm/next/fundamentals/advanced-queries) for explicit joins, junction tables, 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..5168f95c3b --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx @@ -0,0 +1,124 @@ +--- +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 + +:::warning + +Keep side effects out of the callback. If you send an email or queue a message inside the transaction and it then rolls back, the side effect already happened. Return the IDs you need from the callback and act on them after it resolves. + +::: + +- Passing an array of queries, Prisma 7 style (`$transaction([query1, query2])`). Put the calls inside one `db.transaction(...)` callback instead. +- Querying through `db.orm` inside the callback. Use `tx.orm`, or the query runs outside the transaction. + +## 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..cd5af90404 --- /dev/null +++ b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx @@ -0,0 +1,160 @@ +--- +title: Writing data +description: Create, update, delete, and upsert records with the Prisma Next ORM, 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 the Prisma Next ORM, and use createAll, updateAll, and deleteAll for bulk writes. +--- + +Write data with `.create(...)`, `.update(...)`, and `.delete()` for single records, and their `All` and `Count` variants for many records at once. + +## 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 +``` + +To get back only some fields, chain `.select(...)` before `.create(...)`. + +:::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" }); +``` + +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 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 an async-iterable result: `await` it for an array, or `for await` to process records one at a time. + +## Common mistakes + +:::warning + +`.update(...)` and `.delete()` change one record even when the filter matches many. If you meant "all matches", use `updateAll` / `updateCount` or `deleteAll` / `deleteCount`. + +::: + +- Wrapping the fields in `.create({ data: {...} })`, Prisma 7 style. Pass the fields directly: `.create({ email, name })`. +- Calling `.update(...)` or `.delete()` without `.where(...)`. Both need a filter first. +- Running related writes back to back when they must succeed together. Use a [transaction](/orm/next/fundamentals/transactions). Batch arrays in the Prisma 7 style (`$transaction([...])`) are not supported. + +## 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) 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. + Date: Mon, 6 Jul 2026 12:58:58 +0200 Subject: [PATCH 3/6] docs(next): teach relationships with diagrams, add streaming, restructure builders Deep revision of the Fundamentals pages for first-time readers: - Relations and joins now teaches one-to-one, one-to-many, and many-to-many each as concept -> animated diagram -> model -> query -> result shape -> pitfalls, using three new ConceptAnimation flow scenes. Many-to-many is documented as a working pattern (explicit junction model + nested include, verified end to end) instead of a limitation. - Reading data gains a tested "Stream large results" section: await buffers and is reusable, for-await streams and is single-use, mixing the two throws. (Verified on both databases; a repeated await does NOT throw, correcting the earlier claim.) - Advanced queries restructured into parallel "PostgreSQL: SQL query builder" and "MongoDB: Pipeline builder" sections, each with what-it-is, when to use it, when to prefer the ORM API, and tested examples per use case. - Common mistakes rewritten as narrated subsections: what you meant, what went wrong, the fix, and why it is safer. Type-level claims (no data wrapper, mutations require .where) verified with @ts-expect-error assertions; all documented snippets compile under strict TypeScript. - Naming: "Prisma Next ORM" replaced with Prisma Next; the query lane is called the ORM API. Test schema extended with Profile (1:1) and Tag/PostTag (M:N junction); 1:1 back-reference fields are unsupported by the PSL provider, so the 1:1 section documents querying from the foreign-key side. Co-Authored-By: Claude Fable 5 --- .../next/fundamentals/advanced-queries.mdx | 96 ++++-- .../orm/next/fundamentals/reading-data.mdx | 95 +++++- .../next/fundamentals/relations-and-joins.mdx | 147 +++++++-- .../orm/next/fundamentals/transactions.mdx | 42 ++- .../orm/next/fundamentals/writing-data.mdx | 73 ++++- .../concept-animation/flow-presets.ts | 293 +++++++++++++++++- .../components/concept-animation/index.tsx | 8 +- 7 files changed, 684 insertions(+), 70 deletions(-) diff --git a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx index 454aa7821a..fe464b9818 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx @@ -1,18 +1,31 @@ --- title: Advanced queries -description: Use the SQL builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM can't express. +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 builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB. +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 can't express a query, drop one level: the SQL builder on PostgreSQL, or the pipeline builder on MongoDB. Both stay typed against your contract; neither means writing raw strings. +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 ORM remains the default. Reach for these builders for explicit joins, computed projections, grouped top-N queries, and aggregation pipelines. The choice is per query, not per app. +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. -## Query with the SQL builder (PostgreSQL) +## PostgreSQL: SQL query builder -Use `db.sql.public.
` to build a query as a *plan*, then execute the plan through the runtime. Tables use their lowercase storage names: +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"; @@ -26,11 +39,11 @@ const plan = db.sql.public.post 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`, `and`, `count`, and operators added by extensions). +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 explicitly +### Join tables with precise control -Use the SQL builder when you need a join the ORM doesn't express. Alias each side with `.as(...)`, join on any condition, and project columns from both sides: +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 @@ -46,13 +59,26 @@ const plan = db.sql.public.post .build(); const postsWithAuthors = await db.runtime().execute(plan); +// Array<{ postId, title, authorEmail }> ``` -This is also how you read through a many-to-many junction table while [`.include(...)` does not support it](/orm/next/fundamentals/relations-and-joins#current-limitations): join the junction table to the far side explicitly. +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); +// [{ postTitle: "Hello Prisma Next", tagName: "typescript" }, ...] +``` ### Group and rank results -Use the SQL builder for "top N groups" questions, such as the authors with the most posts. It can order and limit by an aggregate directly in the database, which the ORM's grouped aggregates can't: +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 @@ -71,7 +97,7 @@ const topAuthors = await db.runtime().execute(plan); ### Write with RETURNING -SQL builder writes take an array of rows. Use `.returning(...)` to choose which columns come back: +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 @@ -85,9 +111,22 @@ const [insertedUser] = await db.runtime().execute(plan); Prisma Next does not run raw SQL strings: every query goes through the typed builder. If the builder can't express a shape you need, [share the use case](https://pris.ly/discord). -## Query with the pipeline builder (MongoDB) +## 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 -Use `db.query.from("")` to build a typed aggregation pipeline, then execute it through the runtime. This is also where MongoDB aggregation lives, since the MongoDB ORM has no `.aggregate(...)`: +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"; @@ -106,9 +145,28 @@ const plan = db.query .build(); const postsByAuthor = await runtime.execute(plan); +// [{ _id: ObjectId, postCount: 3 }, ...] ``` -Use `.match(...)` to filter with typed field accessors, and `.lookup(...)` for a type-checked `$lookup` join: +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 @@ -122,16 +180,15 @@ const plan = db.query .build(); const postsWithAuthors = await runtime.execute(plan); +// Each post carries an "author" array with the matching user documents ``` -Accumulators such as `acc.count()` and `acc.max(...)` import from `@prisma-next/mongo-query-builder`. - ## Choose the right lane | You need | Use | | --- | --- | -| CRUD, filters, relations, simple aggregates | ORM (`db.orm`) | -| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL builder (`db.sql.public.
`) | +| 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)`. @@ -139,4 +196,5 @@ Plans execute through `db.runtime().execute(plan)` on PostgreSQL and `(await db. ## 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/reading-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx index 7240594deb..81b2b112f2 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx @@ -1,9 +1,9 @@ --- title: Reading data -description: Fetch one record or many with the Prisma Next ORM, then filter, select, sort, and paginate the results. +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 the Prisma Next ORM. Filter with where, project with select, sort with orderBy, and paginate with take and skip. +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. --- Read data by chaining query methods on a model, then running the chain with `.all()` for many records or `.first()` for one. @@ -30,7 +30,7 @@ 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`). -If you are coming from Prisma 7: `.where(...).all()` replaces `findMany`, and `.where(...).first()` replaces `findFirst` and `findUnique`. +Coming from Prisma 7? `.where(...).all()` replaces `findMany`, and `.where(...).first()` replaces `findFirst` and `findUnique`. ## Fetch many records or one @@ -71,7 +71,7 @@ const matchingPosts = await db.orm.public.Post To combine conditions with OR or NOT on PostgreSQL, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`. -On MongoDB, use the object form. Comparison operators and boolean logic are not available on MongoDB's `.where(...)` yet; for those queries, use the [pipeline builder](/orm/next/fundamentals/advanced-queries#query-with-the-pipeline-builder-mongodb). +On MongoDB, use the object form. Comparison operators and boolean logic are not available on MongoDB's `.where(...)` yet; for those queries, use the [pipeline builder](/orm/next/fundamentals/advanced-queries#mongodb-pipeline-builder). ## Select fields @@ -122,19 +122,92 @@ const result = await db.orm.public.Post // result.total: number ``` -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#query-with-the-pipeline-builder-mongodb). +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 + +A query result is more than a promise: it can also be iterated record by record. That gives you two ways 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 + +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; +// Error: AsyncIterableResult iterator has already been consumed +``` + +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 -:::warning +### 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. -A query result can only be consumed once. `await` buffers it into an array; a second `await` on the same result throws `RUNTIME.ITERATOR_CONSUMED`. Store the array in a variable and reuse the variable. +### 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: -- Using `.all()` for a single record. Use `.first()`: it returns one record or `null`, without fetching the rest. -- Expecting `.all()` to limit itself. It returns every match; add `.take(n)` when the table can grow. -- Loading a huge result into memory. Iterate it instead of awaiting: `for await (const row of query.all())`. +```typescript +const posts = await db.orm.public.Post.all(); +// posts is a plain array now; read it as often as you like +``` ## Next 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 index 43ac4c6e02..99e91bed4c 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx @@ -1,17 +1,13 @@ --- title: Relations and joins -description: Read related records in one query with .include(), and shape what comes back per relation. +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: Eager-load related records with the Prisma Next ORM on PostgreSQL and MongoDB, select fields per relation, and filter parents by relation data. +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. -## Include related records - -Use `.include("")` with the relation's field name from your contract: - ```typescript tab="PostgreSQL" import { db } from "./prisma/db"; @@ -32,13 +28,77 @@ const posts = await db.orm.posts // posts[0].author is the referenced user document ``` -Use `.include(...)` when the caller needs the related data in the same response. Skip it when the foreign key on the record is enough. +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 } } +``` -If you find yourself querying children inside a loop over parents, replace the loop with one `.include(...)` on the parent query: it turns one query per parent into a single query. +`.first()` returns `null` when the profile doesn't exist, so a user without a profile is a `null` check, not an error. -## Select fields from included records +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 builder](/orm/next/fundamentals/advanced-queries#join-tables-with-precise-control). + +## 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 }> +``` -Pass a callback as the second argument to shape what each relation returns. 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: +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 @@ -54,35 +114,84 @@ const usersWithRecentPosts = await db.orm.public.User // Array<{ id, email, posts: Array<{ id, title, createdAt }> }> ``` -On MongoDB, start with the plain `.include("author")` form. When you need to reshape the joined documents, use a `$lookup` stage in the [pipeline builder](/orm/next/fundamentals/advanced-queries#query-with-the-pipeline-builder-mongodb). +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(); +// Array<{ ..., tags: Array<{ id, postId, tagId, tag: { id, name } }> }> +``` + +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, use relation predicates inside `.where(...)`: `.some(...)` matches parents with at least one matching child, `.none(...)` matches parents with no matching child, and `.every(...)` requires all children to match. +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`. -## Database-specific behavior +## 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. -## Current limitations - -:::warning +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. -Many-to-many relations are not supported by `.include(...)` yet. For now, query the junction table explicitly with the [SQL builder](/orm/next/fundamentals/advanced-queries#join-tables-explicitly). +## 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. ## Next -- [Use advanced queries](/orm/next/fundamentals/advanced-queries) for explicit joins, junction tables, and `$lookup` pipelines. +- [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 index 5168f95c3b..358720b8b7 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx @@ -109,14 +109,46 @@ Writes made through the driver skip the type-checking that Prisma Next queries g ## Common mistakes -:::warning +### Side effects inside the callback -Keep side effects out of the callback. If you send an email or queue a message inside the transaction and it then rolls back, the side effect already happened. Return the IDs you need from the callback and act on them after it resolves. +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 -- Passing an array of queries, Prisma 7 style (`$transaction([query1, query2])`). Put the calls inside one `db.transaction(...)` callback instead. -- Querying through `db.orm` inside the callback. Use `tx.orm`, or the query runs outside the transaction. +Prisma 7 supported `$transaction([query1, query2])`. Prisma Next has no `$transaction` and no array form. Write the queries inside one `db.transaction(...)` callback; you get the same atomicity, plus the ability to use one query's result in the next. ## Next diff --git a/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx index cd5af90404..2bd0c6fb42 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx @@ -1,9 +1,9 @@ --- title: Writing data -description: Create, update, delete, and upsert records with the Prisma Next ORM, one at a time or in bulk. +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 the Prisma Next ORM, and use createAll, updateAll, and deleteAll for bulk writes. +metaDescription: Create, update, delete, and upsert records in PostgreSQL and MongoDB with Prisma Next, and use createAll, updateAll, and deleteAll for bulk writes. --- Write data with `.create(...)`, `.update(...)`, and `.delete()` for single records, and their `All` and `Count` variants for many records at once. @@ -35,6 +35,8 @@ const user = await db.orm.users.create({ To get back only some fields, chain `.select(...)` before `.create(...)`. +Coming from Prisma 7? There is no `data` wrapper. Prisma 7 wrote `.create({ data: { email, name } })`; Prisma Next takes the fields directly: `.create({ email, name })`. + :::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. @@ -139,22 +141,73 @@ Each mutation comes in three forms. Pick by what you need back: | `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 an async-iterable result: `await` it for an array, or `for await` to process records one at a time. +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). ## Common mistakes -:::warning +### Updating or deleting more than one record -`.update(...)` and `.delete()` change one record even when the filter matches many. If you meant "all matches", use `updateAll` / `updateCount` or `deleteAll` / `deleteCount`. +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: + +```typescript +await db.orm.public.User.create({ data: { email, name } }); +``` + +Prisma Next has no `data` wrapper, so this creates a record with a field literally named `data`, which fails type-checking against your contract. Pass the fields directly: + +```typescript +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 -- Wrapping the fields in `.create({ data: {...} })`, Prisma 7 style. Pass the fields directly: `.create({ email, name })`. -- Calling `.update(...)` or `.delete()` without `.where(...)`. Both need a filter first. -- Running related writes back to back when they must succeed together. Use a [transaction](/orm/next/fundamentals/transactions). Batch arrays in the Prisma 7 style (`$transaction([...])`) are not supported. +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. ## 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) for inserts and updates with explicit `RETURNING` clauses. +- [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/src/components/concept-animation/flow-presets.ts b/apps/docs/src/components/concept-animation/flow-presets.ts index 75aa65b67c..28a2b91e3d 100644 --- a/apps/docs/src/components/concept-animation/flow-presets.ts +++ b/apps/docs/src/components/concept-animation/flow-presets.ts @@ -1,4 +1,3 @@ -import type { ConceptName } from "./presets"; /** * A flow scene is a fixed box-and-arrow diagram drawn in a viewBox. Every node @@ -520,6 +519,291 @@ const githubConnection: FlowScene = { ], }; + +// --------------------------------------------------------------------------- +// Relationship scenes for the Prisma Next Fundamentals docs. +// Row colors double as a field legend: production = primary key, +// override = foreign key, preview = regular field. +// --------------------------------------------------------------------------- + +const RELATION_LEGEND: { origin: RowOrigin; label: string }[] = [ + { origin: "production", label: "primary key" }, + { origin: "override", label: "foreign key" }, +]; + +const relationOneToOne: FlowScene = { + label: "One-to-one: a profile belongs to exactly one user", + width: 680, + height: 220, + legend: RELATION_LEGEND, + nodes: [ + { + id: "user", + label: "User", + sub: "one record", + subBelow: true, + variant: "project", + x: 40, + y: 48, + w: 240, + h: 104, + rows: [ + { key: "id", value: "u_01", origin: "production" }, + { key: "email", value: "alice@prisma.io", origin: "preview" }, + ], + }, + { + id: "profile", + label: "Profile", + sub: "at most one per user", + subBelow: true, + variant: "scope", + x: 400, + y: 40, + w: 240, + h: 128, + rows: [ + { key: "id", value: "p_01", origin: "production" }, + { key: "userId", value: "u_01 · unique", origin: "override" }, + { key: "bio", value: "Writes about…", origin: "preview" }, + ], + }, + ], + edges: [ + { + id: "fk", + from: "profile", + fromSide: "l", + to: "user", + toSide: "r", + label: "userId → id", + }, + ], + steps: [ + { + title: "1. Two models", + caption: + "A profile stores extra data about one user, in its own table or collection. On its own, nothing connects the two records yet.", + nodes: ["user", "profile"], + edges: [], + }, + { + title: "2. A unique foreign key", + caption: + "Profile.userId holds the id of its user: that is the foreign key. Marking it unique is what makes the relationship one-to-one, because two profiles can never point at the same user.", + nodes: ["user", "profile"], + edges: ["fk"], + emphasize: ["profile"], + }, + { + title: "3. Query from the profile", + caption: + "The model that holds the foreign key declares the relation, so you query from that side: Profile.include(\"user\") follows userId and attaches the matching user to the result.", + nodes: ["user", "profile"], + edges: ["fk"], + emphasize: ["user"], + }, + ], +}; + +const relationOneToMany: FlowScene = { + label: "One-to-many: one user has many posts", + width: 680, + height: 300, + legend: RELATION_LEGEND, + nodes: [ + { + id: "user", + label: "User", + sub: "the one side", + subBelow: true, + variant: "project", + x: 40, + y: 98, + w: 220, + h: 104, + rows: [ + { key: "id", value: "u_01", origin: "production" }, + { key: "email", value: "alice@prisma.io", origin: "preview" }, + ], + }, + { + id: "p1", + label: "Post", + sub: "authorId = u_01", + variant: "branch", + x: 420, + y: 24, + w: 220, + h: 64, + }, + { + id: "p2", + label: "Post", + sub: "authorId = u_01", + variant: "branch", + x: 420, + y: 118, + w: 220, + h: 64, + }, + { + id: "p3", + label: "Post", + sub: "authorId = u_01", + variant: "branch", + x: 420, + y: 212, + w: 220, + h: 64, + }, + ], + edges: [ + { id: "e1", from: "p1", fromSide: "l", to: "user", toSide: "r", toDy: -24 }, + { id: "e2", from: "p2", fromSide: "l", to: "user", toSide: "r" }, + { id: "e3", from: "p3", fromSide: "l", to: "user", toSide: "r", toDy: 24 }, + ], + steps: [ + { + title: "1. The foreign key", + caption: + "Each post stores the id of its author in authorId. One post always has exactly one author.", + nodes: ["user", "p1"], + edges: ["e1"], + emphasize: ["p1"], + }, + { + title: "2. Many rows, same key", + caption: + "Nothing stops many posts from carrying the same authorId. That is the whole mechanism: one-to-many is many child records pointing at one parent.", + nodes: ["user", "p1", "p2", "p3"], + edges: ["e1", "e2", "e3"], + emphasize: ["p2", "p3"], + }, + { + title: "3. Query either direction", + caption: + "User.include(\"posts\") gathers every post with a matching authorId into an array on the user. Post.include(\"author\") follows the key the other way and attaches one user to each post.", + nodes: ["user", "p1", "p2", "p3"], + edges: ["e1", "e2", "e3"], + emphasize: ["user"], + }, + ], +}; + +const relationManyToMany: FlowScene = { + label: "Many-to-many: posts and tags connect through a junction model", + width: 700, + height: 300, + groupLabels: [{ text: "Junction model", x: 280, y: 18 }], + legend: RELATION_LEGEND, + nodes: [ + { + id: "post1", + label: "Post", + sub: "Hello Prisma Next", + variant: "project", + x: 24, + y: 46, + w: 190, + h: 64, + }, + { + id: "post2", + label: "Post", + sub: "Typed queries", + variant: "project", + x: 24, + y: 196, + w: 190, + h: 64, + }, + { + id: "pt1", + label: "PostTag", + sub: "postId + tagId", + variant: "neutral", + x: 280, + y: 34, + w: 150, + h: 56, + }, + { + id: "pt2", + label: "PostTag", + sub: "postId + tagId", + variant: "neutral", + x: 280, + y: 126, + w: 150, + h: 56, + }, + { + id: "pt3", + label: "PostTag", + sub: "postId + tagId", + variant: "neutral", + x: 280, + y: 218, + w: 150, + h: 56, + }, + { + id: "tag1", + label: "Tag", + sub: "typescript", + variant: "source", + x: 496, + y: 46, + w: 180, + h: 64, + }, + { + id: "tag2", + label: "Tag", + sub: "databases", + variant: "source", + x: 496, + y: 196, + w: 180, + h: 64, + }, + ], + edges: [ + { id: "a1", from: "pt1", fromSide: "l", to: "post1", toSide: "r" }, + { id: "b1", from: "pt1", fromSide: "r", to: "tag1", toSide: "l" }, + { id: "a2", from: "pt2", fromSide: "l", to: "post1", toSide: "r", toDy: 18 }, + { id: "b2", from: "pt2", fromSide: "r", to: "tag2", toSide: "l", toDy: -18 }, + { id: "a3", from: "pt3", fromSide: "l", to: "post2", toSide: "r" }, + { id: "b3", from: "pt3", fromSide: "r", to: "tag1", toSide: "l", toDy: 18 }, + ], + steps: [ + { + title: "1. Both sides need many", + caption: + "A post can carry many tags, and a tag appears on many posts. Neither table can hold the other's foreign key without losing one of those directions.", + nodes: ["post1", "post2", "tag1", "tag2"], + edges: [], + }, + { + title: "2. The junction model", + caption: + "A junction model solves it: each PostTag record links one post to one tag. Three link records here connect two posts and two tags in every combination the data needs.", + nodes: ["post1", "post2", "pt1", "pt2", "pt3", "tag1", "tag2"], + edges: ["a1", "b1", "a2", "b2", "a3", "b3"], + emphasize: ["pt1", "pt2", "pt3"], + }, + { + title: "3. Traverse in two hops", + caption: + "Queries follow the same two hops: Post.include(\"tags\") fetches the link records, and nesting include(\"tag\") inside it attaches each tag. One query, both hops.", + nodes: ["post1", "post2", "pt1", "pt2", "pt3", "tag1", "tag2"], + edges: ["a1", "b1", "a2", "b2", "a3", "b3"], + emphasize: ["post1", "tag1", "tag2"], + }, + ], +}; + /** * Names that render as visual flow diagrams. Any name not listed here falls * back to the Code Hike token animation in presets.ts. @@ -528,4 +812,9 @@ export const FLOW_SCENES = { "compute-model": computeModel, "env-layers": envLayers, "github-connection": githubConnection, -} satisfies Partial>; + "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), From 90a65d303ac63fc70f57c449d80f876189476813 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:52:55 +0200 Subject: [PATCH 4/6] docs(next): add context, result shapes, and agent prompts to Fundamentals Style pass emulating the v6 CRUD reference: - Section-linked page intros and an expandable example schema (PostgreSQL/MongoDB tabs) on Reading and Writing data. - Queries now show their result shape in no-copy blocks, taken from the validation runs. - Prisma 7 migration notes are diff blocks (findMany -> .all(), data wrapper -> direct fields, $transaction array -> callback). - MongoDB filter section shows how to reach ranges and in() through the pipeline builder (newly tested: .match gte/lte chaining and .in() both work); PostgreSQL filters gain or() example (tested). - Sort section shows the composite orderBy array and explains offset vs cursor pagination before the cursor example. - Streaming section now defines streaming and its benefits before the consumption rules. - "Choose the right lane" renamed to "Choose the right query API". - Each page ends with "Prompt your coding agent": copyable prompts per section that reference the scaffolded Prisma Next skills. Co-Authored-By: Claude Fable 5 --- .../next/fundamentals/advanced-queries.mdx | 38 +++- .../orm/next/fundamentals/reading-data.mdx | 194 ++++++++++++++++-- .../next/fundamentals/relations-and-joins.mdx | 24 ++- .../orm/next/fundamentals/transactions.mdx | 30 ++- .../orm/next/fundamentals/writing-data.mdx | 110 +++++++++- 5 files changed, 361 insertions(+), 35 deletions(-) diff --git a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx index fe464b9818..b48b27312f 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx @@ -73,7 +73,14 @@ const plan = db.sql.public.postTag .build(); const postTagPairs = await db.runtime().execute(plan); -// [{ postTitle: "Hello Prisma Next", tagName: "typescript" }, ...] +``` + +```js no-copy +[ + { postTitle: 'Hello Prisma Next', tagName: 'databases' }, + { postTitle: 'Hello Prisma Next', tagName: 'typescript' }, + { postTitle: 'Typed queries', tagName: 'typescript' } +] ``` ### Group and rank results @@ -92,9 +99,17 @@ const plan = db.sql.public.post .build(); const topAuthors = await db.runtime().execute(plan); -// PostgreSQL returns counts as strings; convert with Number(row.posts) ``` +```js no-copy +[ + { authorId: 'zmlhqodhz2tj5pztggwa50nm', posts: '2' }, + { authorId: 'yvp1col4louhkzxyycg4tnur', 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: @@ -145,7 +160,13 @@ const plan = db.query .build(); const postsByAuthor = await runtime.execute(plan); -// [{ _id: ObjectId, postCount: 3 }, ...] +``` + +```js no-copy +[ + { _id: new ObjectId('6a4b70352eabdda01b0e6af5'), postCount: 3 }, + { _id: new ObjectId('6a4b70352eabdda01b0e6af4'), postCount: 2 } +] ``` Accumulators such as `acc.count()` and `acc.max(...)` import from `@prisma-next/mongo-query-builder`. @@ -183,7 +204,7 @@ const postsWithAuthors = await runtime.execute(plan); // Each post carries an "author" array with the matching user documents ``` -## Choose the right lane +## Choose the right query API | You need | Use | | --- | --- | @@ -193,6 +214,15 @@ const postsWithAuthors = await runtime.execute(plan); 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. diff --git a/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx index 81b2b112f2..77b30c03c2 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx @@ -6,7 +6,9 @@ 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. --- -Read data by chaining query methods on a model, then running the chain with `.all()` for many records or `.first()` for one. +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"; @@ -30,19 +32,96 @@ 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`). -Coming from Prisma 7? `.where(...).all()` replaces `findMany`, and `.where(...).first()` replaces `findFirst` and `findUnique`. +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 and applies no limit of its own, so combine it with [`take`](#sort-and-paginate) on tables that can grow. +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: 'zmlhqodhz2tj5pztggwa50nm', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z }, + { id: 'yvp1col4louhkzxyycg4tnur', 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`, and on PostgreSQL it fetches at most one row. For a primary-key lookup on PostgreSQL, pass the key directly: +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.first({ id: userId }); +const user = await db.orm.public.User.where({ email: "alice@prisma.io" }).first(); +``` + +```js no-copy +{ id: 'zmlhqodhz2tj5pztggwa50nm', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z } ``` -On MongoDB, look up documents by their `_id` field: `db.orm.users.where({ _id: id }).first()`. +For a primary-key lookup on PostgreSQL, pass the key directly: `db.orm.public.User.first({ id: userId })`. On MongoDB, look up documents by their `_id` field: `db.orm.users.where({ _id: id }).first()`. ## Filter records @@ -61,17 +140,51 @@ const recentPosts = await db.orm.public.Post .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 on PostgreSQL, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`. +To combine conditions with OR or NOT, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`: -On MongoDB, use the object form. Comparison operators and boolean logic are not available on MongoDB's `.where(...)` yet; for those queries, use the [pipeline builder](/orm/next/fundamentals/advanced-queries#mongodb-pipeline-builder). +```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 @@ -79,19 +192,25 @@ Use `.select(...)` to fetch only the fields you need. The result type narrows to ```typescript tab="PostgreSQL" const users = await db.orm.public.User.select("id", "email").all(); -// Array<{ id: string; email: string }> ``` ```typescript tab="MongoDB" const users = await db.orm.users.select("_id", "email").all(); -// Array<{ _id: ObjectId; email: string }> +``` + +```js no-copy +[ + { id: 'zmlhqodhz2tj5pztggwa50nm', email: 'alice@prisma.io' }, + { id: 'yvp1col4louhkzxyycg4tnur', email: 'bob@prisma.io' } +] ``` ## Sort and paginate -Use `.orderBy(...)` to sort, `.take(n)` to limit, and `.skip(n)` to offset: +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) @@ -100,6 +219,7 @@ const page = await db.orm.public.Post ``` ```typescript tab="MongoDB" +// Second page of posts, newest first const page = await db.orm.posts .orderBy({ createdAt: -1 }) .take(20) @@ -107,9 +227,29 @@ const page = await db.orm.posts .all(); ``` -PostgreSQL sorts with lambdas calling `.asc()` or `.desc()`; pass an array of lambdas for a composite sort. MongoDB sorts with the driver's direction values: `1` for ascending, `-1` for descending. +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: + +```typescript +const page1 = await db.orm.public.Post + .orderBy((p) => p.createdAt.desc()) + .take(20) + .all(); -For stable pagination over large tables on PostgreSQL, follow `.orderBy(...)` with `.cursor({ createdAt: last.createdAt })` to resume from a known position instead of counting offsets. +const last = page1[page1.length - 1]!; +const page2 = await db.orm.public.Post + .orderBy((p) => p.createdAt.desc()) + .cursor({ createdAt: last.createdAt }) + .take(20) + .all(); +``` ## Count records @@ -119,14 +259,19 @@ Count through `.aggregate(...)` on PostgreSQL. It returns an object with the key const result = await db.orm.public.Post .where({ published: true }) .aggregate((a) => ({ total: a.count() })); -// result.total: number +``` + +```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 -A query result is more than a promise: it can also be iterated record by record. That gives you two ways to consume it. +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. @@ -155,7 +300,7 @@ for await (const post of db.orm.public.Post.all()) { ### A streamed result can only be read once -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: +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(); @@ -165,7 +310,11 @@ for await (const post of result) { } await result; -// Error: AsyncIterableResult iterator has already been consumed +``` + +```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: @@ -209,8 +358,17 @@ 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 builder or a MongoDB pipeline. +- [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 index 99e91bed4c..3565a0cf4a 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx @@ -147,7 +147,20 @@ const postsWithTags = await db.orm.public.Post .where({ published: true }) .include("tags", (postTag) => postTag.include("tag")) .all(); -// Array<{ ..., tags: Array<{ id, postId, tagId, tag: { id, name } }> }> +``` + +```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)`. @@ -190,6 +203,15 @@ The relationship shapes above apply to reference-style relations on both databas - 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. diff --git a/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx b/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx index 358720b8b7..d288cc01f4 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/transactions.mdx @@ -148,7 +148,35 @@ Queries on `db` run on their own connection, outside the open transaction. They ### Passing an array of queries -Prisma 7 supported `$transaction([query1, query2])`. Prisma Next has no `$transaction` and no array form. Write the queries inside one `db.transaction(...)` callback; you get the same atomicity, plus the ability to use one query's result in the next. +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 diff --git a/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx index 2bd0c6fb42..b23dcf44f5 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx @@ -6,7 +6,59 @@ 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. --- -Write data with `.create(...)`, `.update(...)`, and `.delete()` for single records, and their `All` and `Count` variants for many records at once. +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 @@ -33,9 +85,25 @@ const user = await db.orm.users.create({ // 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: 'i3czppyhmfv77p79azedy0bl', + email: 'jane@prisma.io', + name: 'Jane', + createdAt: 2026-07-06T09:09:56.119Z +} +``` + To get back only some fields, chain `.select(...)` before `.create(...)`. -Coming from Prisma 7? There is no `data` wrapper. Prisma 7 wrote `.create({ data: { email, name } })`; Prisma Next takes the fields directly: `.create({ email, name })`. +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 @@ -59,6 +127,15 @@ const updatedUser = await db.orm.users .update({ name: "Jane Doe" }); ``` +```js no-copy +{ + id: 'i3czppyhmfv77p79azedy0bl', + 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: @@ -129,6 +206,13 @@ const deletedCount = await db.orm.public.Post .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 @@ -167,16 +251,11 @@ Use `updateAll` or `deleteAll` when you also need the changed records back, and ### Wrapping create fields in a data object -You wrote the Prisma 7 shape: +You wrote the Prisma 7 shape, and it fails type-checking, because your contract has no field named `data`: -```typescript -await db.orm.public.User.create({ data: { email, name } }); -``` - -Prisma Next has no `data` wrapper, so this creates a record with a field literally named `data`, which fails type-checking against your contract. Pass the fields directly: - -```typescript -await db.orm.public.User.create({ email, name }); +```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. @@ -206,6 +285,15 @@ If the second write fails, the first has already committed, and you're left with 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(...)`. From d7a66b31cfccba78dee2058ac8d25ed4379fe6a6 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Mon, 6 Jul 2026 16:17:08 +0200 Subject: [PATCH 5/6] docs(next): address review feedback, show examples for prose-only guidance - Cursor pagination example now carries the id tiebreaker in both the composite orderBy and the cursor, with a sentence on why a cursor on a non-unique field alone can skip or repeat records. Verified against rows sharing the same createdAt: pages come back with zero overlap. - .select(...).create(...) now has a tested example with its narrowed result shape, and the same show-don't-tell treatment applies to the primary-key lookup (tabs), the updateAll rows result, and the 1:1 user-side SQL builder join that was previously only linked. - Result-block ids replaced with format-preserving placeholders so cspell passes (the random cuid/ObjectId fragments tripped it). The alicesPosts naming flag was already resolved by an earlier rewrite. Co-Authored-By: Claude Fable 5 --- .../next/fundamentals/advanced-queries.mdx | 8 ++--- .../orm/next/fundamentals/reading-data.mdx | 30 ++++++++++++------ .../next/fundamentals/relations-and-joins.mdx | 16 +++++++++- .../orm/next/fundamentals/writing-data.mdx | 31 ++++++++++++++++--- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx index b48b27312f..284dbfb841 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx @@ -103,8 +103,8 @@ const topAuthors = await db.runtime().execute(plan); ```js no-copy [ - { authorId: 'zmlhqodhz2tj5pztggwa50nm', posts: '2' }, - { authorId: 'yvp1col4louhkzxyycg4tnur', posts: '1' } + { authorId: 'cuid20000000000000000001', posts: '2' }, + { authorId: 'cuid20000000000000000002', posts: '1' } ] ``` @@ -164,8 +164,8 @@ const postsByAuthor = await runtime.execute(plan); ```js no-copy [ - { _id: new ObjectId('6a4b70352eabdda01b0e6af5'), postCount: 3 }, - { _id: new ObjectId('6a4b70352eabdda01b0e6af4'), postCount: 2 } + { _id: new ObjectId('650000000000000000000001'), postCount: 3 }, + { _id: new ObjectId('650000000000000000000002'), postCount: 2 } ] ``` diff --git a/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx index 77b30c03c2..03acab6a99 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx @@ -104,8 +104,8 @@ const users = await db.orm.public.User.all(); ```js no-copy [ - { id: 'zmlhqodhz2tj5pztggwa50nm', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z }, - { id: 'yvp1col4louhkzxyycg4tnur', email: 'bob@prisma.io', name: 'Bob', createdAt: 2026-07-06T09:03:14.112Z } + { 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 } ] ``` @@ -118,10 +118,18 @@ const user = await db.orm.public.User.where({ email: "alice@prisma.io" }).first( ``` ```js no-copy -{ id: 'zmlhqodhz2tj5pztggwa50nm', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z } +{ id: 'cuid20000000000000000001', email: 'alice@prisma.io', name: 'Alice', createdAt: 2026-07-06T09:03:13.808Z } ``` -For a primary-key lookup on PostgreSQL, pass the key directly: `db.orm.public.User.first({ id: userId })`. On MongoDB, look up documents by their `_id` field: `db.orm.users.where({ _id: id }).first()`. +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 @@ -200,8 +208,8 @@ const users = await db.orm.users.select("_id", "email").all(); ```js no-copy [ - { id: 'zmlhqodhz2tj5pztggwa50nm', email: 'alice@prisma.io' }, - { id: 'yvp1col4louhkzxyycg4tnur', email: 'bob@prisma.io' } + { id: 'cuid20000000000000000001', email: 'alice@prisma.io' }, + { id: 'cuid20000000000000000002', email: 'bob@prisma.io' } ] ``` @@ -235,18 +243,20 @@ const posts = await db.orm.public.Post .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: +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()) + .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()) - .cursor({ createdAt: last.createdAt }) + .orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()]) + .cursor({ createdAt: last.createdAt, id: last.id }) .take(20) .all(); ``` 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 index 3565a0cf4a..7bda40b5f8 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/relations-and-joins.mdx @@ -61,7 +61,21 @@ const profileWithUser = await db.orm.public.Profile `.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 builder](/orm/next/fundamentals/advanced-queries#join-tables-with-precise-control). +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 diff --git a/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx index b23dcf44f5..09081d9acf 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/writing-data.mdx @@ -89,14 +89,24 @@ The returned record is complete, so you can use the generated values right away: ```js no-copy { - id: 'i3czppyhmfv77p79azedy0bl', + 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(...)`. +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: @@ -129,7 +139,7 @@ const updatedUser = await db.orm.users ```js no-copy { - id: 'i3czppyhmfv77p79azedy0bl', + id: 'cuid20000000000000000003', email: 'jane@prisma.io', name: 'Jane Doe', createdAt: 2026-07-06T09:09:56.119Z @@ -225,7 +235,20 @@ Each mutation comes in three forms. Pick by what you need back: | `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). +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 From 70661c8a57887f935c94e8221359cf9baf50f4c3 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:20:48 +0200 Subject: [PATCH 6/6] docs(next): park cutover redirects as comments, document section conventions - Remove the nine live Prisma 7 -> Prisma Next redirects from apps/docs/vercel.json and park them, commented out, in next.config.mjs under a "Prisma Next URL cutover (DR-8687)" block. They ship when /orm/next becomes /orm; until then section owners append their commented map to the same block so the full cutover builds up in one reviewable place. - Add .claude/skills/docs-writer/references/prisma-next.md so the parallel section PRs follow the same conventions: page location, the commented-redirects rule, tested-example requirements, tab and diagram usage, naming, and validation commands. Linked from SKILL.md. - Document raw SQL precisely on Advanced queries: standalone raw statements do not run, but fns.raw fragments inside the SQL query builder do (tested with a computed UPPER(email) projection). Co-Authored-By: Claude Fable 5 --- .claude/skills/docs-writer/SKILL.md | 2 + .../docs-writer/references/prisma-next.md | 40 +++++++++++++++++ .../next/fundamentals/advanced-queries.mdx | 17 ++++++- apps/docs/next.config.mjs | 23 ++++++++++ apps/docs/vercel.json | 45 ------------------- 5 files changed, 81 insertions(+), 46 deletions(-) create mode 100644 .claude/skills/docs-writer/references/prisma-next.md 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/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx index 284dbfb841..7cf93f6323 100644 --- a/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx +++ b/apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx @@ -124,7 +124,22 @@ const [insertedUser] = await db.runtime().execute(plan); // Contract defaults such as generated IDs are applied ``` -Prisma Next does not run raw SQL strings: every query goes through the typed builder. If the builder can't express a shape you need, [share the use case](https://pris.ly/discord). +### 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 diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index dfc0f742f5..f1f1fbfc13 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -297,6 +297,29 @@ const config = { destination: "/next/add-to-existing-project/:path*", permanent: false, }, + // ── Prisma Next URL cutover (DR-8687) — DO NOT ENABLE YET ───────────── + // The redirects below retire live Prisma 7 URLs, so they ship only when + // Prisma Next becomes the default docs version (the /orm/next tree moves + // to /orm). Until then, keep your section's redirects here, commented + // out, so the full cutover map builds up in one reviewable place. + // Section owners: append your block below with a DR reference. + // + // DR-8681 Fundamentals: + // { source: "/orm/prisma-client", destination: "/orm/next/fundamentals/reading-data", permanent: false }, + // { source: "/orm/prisma-client/queries/crud", destination: "/orm/next/fundamentals/reading-data", permanent: false }, + // { source: "/orm/prisma-client/queries/select-fields", destination: "/orm/next/fundamentals/reading-data", permanent: false }, + // { source: "/orm/prisma-client/queries/filtering-and-sorting", destination: "/orm/next/fundamentals/reading-data", permanent: false }, + // { source: "/orm/prisma-client/queries/pagination", destination: "/orm/next/fundamentals/reading-data", permanent: false }, + // { source: "/orm/prisma-client/queries/aggregation-grouping-summarizing", destination: "/orm/next/fundamentals/reading-data", permanent: false }, + // { source: "/orm/prisma-client/queries/relation-queries", destination: "/orm/next/fundamentals/relations-and-joins", permanent: false }, + // { source: "/orm/prisma-client/queries/transactions", destination: "/orm/next/fundamentals/transactions", permanent: false }, + // { source: "/orm/prisma-client/using-raw-sql", destination: "/orm/next/fundamentals/advanced-queries", permanent: false }, + // + // No Prisma Next equivalent yet (stay on the Prisma 7 tree, flag to the + // SEO owner at cutover): /orm/prisma-client/queries/full-text-search, + // /orm/prisma-client/queries/advanced/query-optimization-performance, + // /orm/prisma-client/queries/excluding-fields. + // ─────────────────────────────────────────────────────────────────────── ]; }, async rewrites() { diff --git a/apps/docs/vercel.json b/apps/docs/vercel.json index eac9e9c9a9..aff111ca08 100644 --- a/apps/docs/vercel.json +++ b/apps/docs/vercel.json @@ -6084,51 +6084,6 @@ "source": "/docs/cli/console/platform", "destination": "/docs/cli/console", "permanent": true - }, - { - "source": "/docs/orm/prisma-client", - "destination": "/docs/orm/next/fundamentals/reading-data", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/queries/crud", - "destination": "/docs/orm/next/fundamentals/reading-data", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/queries/select-fields", - "destination": "/docs/orm/next/fundamentals/reading-data", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/queries/filtering-and-sorting", - "destination": "/docs/orm/next/fundamentals/reading-data", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/queries/pagination", - "destination": "/docs/orm/next/fundamentals/reading-data", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/queries/aggregation-grouping-summarizing", - "destination": "/docs/orm/next/fundamentals/reading-data", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/queries/relation-queries", - "destination": "/docs/orm/next/fundamentals/relations-and-joins", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/queries/transactions", - "destination": "/docs/orm/next/fundamentals/transactions", - "permanent": false - }, - { - "source": "/docs/orm/prisma-client/using-raw-sql", - "destination": "/docs/orm/next/fundamentals/advanced-queries", - "permanent": false } ] }