Skip to content

Commit 6d29d03

Browse files
ankur-archclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 101699f commit 6d29d03

7 files changed

Lines changed: 684 additions & 70 deletions

File tree

apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
---
22
title: Advanced queries
3-
description: Use the SQL builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM can't express.
3+
description: Use the SQL query builder on PostgreSQL and the pipeline builder on MongoDB for queries the ORM API can't express.
44
url: /orm/next/fundamentals/advanced-queries
55
metaTitle: Advanced queries in Prisma Next
6-
metaDescription: Use the Prisma Next SQL builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB.
6+
metaDescription: Use the Prisma Next SQL query builder for explicit joins, grouped aggregates, and RETURNING, and the typed aggregation pipeline builder on MongoDB.
77
---
88

9-
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.
9+
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.
1010

11-
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.
11+
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.
1212

13-
## Query with the SQL builder (PostgreSQL)
13+
## PostgreSQL: SQL query builder
1414

15-
Use `db.sql.public.<table>` to build a query as a *plan*, then execute the plan through the runtime. Tables use their lowercase storage names:
15+
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.
16+
17+
**Use it when:**
18+
19+
- The query is easier to say in SQL: joins with conditions, computed columns, set-shaped results.
20+
- You need PostgreSQL behavior the ORM API doesn't surface, such as `RETURNING` on a bulk insert.
21+
- An aggregation needs precise control, like ordering and limiting by an aggregate in the database.
22+
- A query is performance-sensitive and you want to decide its exact shape.
23+
24+
**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.
25+
26+
### Build and run a plan
27+
28+
Start from a table with `db.sql.public.<table>` (tables use lowercase storage names), chain clauses, and call `.build()`. Execute the plan with the runtime:
1629

1730
```typescript
1831
import { db } from "./prisma/db";
@@ -26,11 +39,11 @@ const plan = db.sql.public.post
2639
const publishedPosts = await db.runtime().execute(plan);
2740
```
2841

29-
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).
42+
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).
3043

31-
### Join tables explicitly
44+
### Join tables with precise control
3245

33-
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:
46+
Alias each side with `.as(...)`, join on any condition, and project columns from both sides into a flat result:
3447

3548
```typescript
3649
const plan = db.sql.public.post
@@ -46,13 +59,26 @@ const plan = db.sql.public.post
4659
.build();
4760

4861
const postsWithAuthors = await db.runtime().execute(plan);
62+
// Array<{ postId, title, authorEmail }>
4963
```
5064

51-
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.
65+
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:
66+
67+
```typescript
68+
const plan = db.sql.public.postTag
69+
.as("pt")
70+
.innerJoin(db.sql.public.tag.as("t"), (f, fns) => fns.eq(f.pt.tagId, f.t.id))
71+
.innerJoin(db.sql.public.post.as("p"), (f, fns) => fns.eq(f.pt.postId, f.p.id))
72+
.select((f) => ({ postTitle: f.p.title, tagName: f.t.name }))
73+
.build();
74+
75+
const postTagPairs = await db.runtime().execute(plan);
76+
// [{ postTitle: "Hello Prisma Next", tagName: "typescript" }, ...]
77+
```
5278

5379
### Group and rank results
5480

55-
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:
81+
Answer "top N groups" questions, such as the authors with the most posts, by ordering and limiting on an aggregate directly in the database:
5682

5783
```typescript
5884
const plan = db.sql.public.post
@@ -71,7 +97,7 @@ const topAuthors = await db.runtime().execute(plan);
7197

7298
### Write with RETURNING
7399

74-
SQL builder writes take an array of rows. Use `.returning(...)` to choose which columns come back:
100+
SQL builder writes take an array of rows. Use `.returning(...)` to choose which columns come back from the same statement:
75101

76102
```typescript
77103
const plan = db.sql.public.user
@@ -85,9 +111,22 @@ const [insertedUser] = await db.runtime().execute(plan);
85111

86112
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).
87113

88-
## Query with the pipeline builder (MongoDB)
114+
## MongoDB: Pipeline builder
115+
116+
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.
117+
118+
**Use it when:**
119+
120+
- You need an aggregation: counts, grouping, or summaries per key.
121+
- You want to join and reshape documents across collections with `$lookup`.
122+
- A query needs MongoDB pipeline stages that don't map to the ORM API, such as multi-stage filtering and projection.
123+
- You need operators the MongoDB `.where(...)` doesn't cover yet, like ranges or boolean logic.
124+
125+
**Prefer the ORM API when** the query is document CRUD or a reference-relation read; `.include(...)` already covers the common `$lookup` case.
126+
127+
### Build and run a pipeline
89128

90-
Use `db.query.from("<collection>")` 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(...)`:
129+
Start from a collection with `db.query.from(...)`, chain stages, and call `.build()`. Execute the plan through the runtime:
91130

92131
```typescript
93132
import { acc } from "@prisma-next/mongo-query-builder";
@@ -106,9 +145,28 @@ const plan = db.query
106145
.build();
107146

108147
const postsByAuthor = await runtime.execute(plan);
148+
// [{ _id: ObjectId, postCount: 3 }, ...]
109149
```
110150

111-
Use `.match(...)` to filter with typed field accessors, and `.lookup(...)` for a type-checked `$lookup` join:
151+
Accumulators such as `acc.count()` and `acc.max(...)` import from `@prisma-next/mongo-query-builder`.
152+
153+
### Filter and group in stages
154+
155+
Chain `.match(...)` before `.group(...)` to aggregate over a subset, the pipeline equivalent of `WHERE` before `GROUP BY`:
156+
157+
```typescript
158+
const plan = db.query
159+
.from("posts")
160+
.match((f) => f.published.eq(false))
161+
.group((f) => ({ _id: f.authorId, draftCount: acc.count() }))
162+
.build();
163+
164+
const draftsByAuthor = await runtime.execute(plan);
165+
```
166+
167+
### Join collections with $lookup
168+
169+
Use `.lookup(...)` for a type-checked join against another collection. The joined documents arrive under the name you give `.as(...)`:
112170

113171
```typescript
114172
const plan = db.query
@@ -122,21 +180,21 @@ const plan = db.query
122180
.build();
123181

124182
const postsWithAuthors = await runtime.execute(plan);
183+
// Each post carries an "author" array with the matching user documents
125184
```
126185

127-
Accumulators such as `acc.count()` and `acc.max(...)` import from `@prisma-next/mongo-query-builder`.
128-
129186
## Choose the right lane
130187

131188
| You need | Use |
132189
| --- | --- |
133-
| CRUD, filters, relations, simple aggregates | ORM (`db.orm`) |
134-
| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL builder (`db.sql.public.<table>`) |
190+
| CRUD, filters, relations, simple aggregates | ORM API (`db.orm`) |
191+
| Explicit join, computed projection, grouped top-N, `RETURNING` | SQL query builder (`db.sql.public.<table>`) |
135192
| `$group`, `$lookup` with reshaping, any MongoDB aggregation | Pipeline builder (`db.query.from(...)`) |
136193

137194
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)`.
138195

139196
## Next
140197

141198
- [Read data](/orm/next/fundamentals/reading-data): the ORM happy path these builders back up.
199+
- [Understand relationships](/orm/next/fundamentals/relations-and-joins) before reaching for explicit joins.
142200
- [Run SQL builder plans atomically](/orm/next/fundamentals/transactions) inside a transaction.

apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
title: Reading data
3-
description: Fetch one record or many with the Prisma Next ORM, then filter, select, sort, and paginate the results.
3+
description: Fetch one record or many with Prisma Next, then filter, select, sort, paginate, and stream the results.
44
url: /orm/next/fundamentals/reading-data
55
metaTitle: Reading data with Prisma Next
6-
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.
6+
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.
77
---
88

99
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();
3030

3131
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`).
3232

33-
If you are coming from Prisma 7: `.where(...).all()` replaces `findMany`, and `.where(...).first()` replaces `findFirst` and `findUnique`.
33+
Coming from Prisma 7? `.where(...).all()` replaces `findMany`, and `.where(...).first()` replaces `findFirst` and `findUnique`.
3434

3535
## Fetch many records or one
3636

@@ -71,7 +71,7 @@ const matchingPosts = await db.orm.public.Post
7171

7272
To combine conditions with OR or NOT on PostgreSQL, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`.
7373

74-
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).
74+
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).
7575

7676
## Select fields
7777

@@ -122,19 +122,92 @@ const result = await db.orm.public.Post
122122
// result.total: number
123123
```
124124

125-
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).
125+
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).
126+
127+
## Stream large results
128+
129+
A query result is more than a promise: it can also be iterated record by record. That gives you two ways to consume it.
130+
131+
`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.
132+
133+
```typescript
134+
const posts = await db.orm.public.Post.all();
135+
136+
console.log(posts.length);
137+
console.log(posts[0]);
138+
```
139+
140+
`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:
141+
142+
```typescript
143+
for await (const post of db.orm.public.Post.all()) {
144+
await exportToSearchIndex(post);
145+
}
146+
```
147+
148+
You can also leave the loop early; unprocessed records are never buffered:
149+
150+
```typescript
151+
for await (const post of db.orm.public.Post.all()) {
152+
if (isMatch(post)) break;
153+
}
154+
```
155+
156+
### A streamed result can only be read once
157+
158+
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:
159+
160+
```typescript
161+
const result = db.orm.public.Post.all();
162+
163+
for await (const post of result) {
164+
// ...
165+
}
166+
167+
await result;
168+
// Error: AsyncIterableResult iterator has already been consumed
169+
```
170+
171+
If you need the data more than once, `await` the query into an array and reuse the array:
172+
173+
```typescript
174+
const posts = await db.orm.public.Post.all();
175+
176+
const published = posts.filter((p) => p.published);
177+
const titles = posts.map((p) => p.title);
178+
```
179+
180+
Streaming behaves the same on PostgreSQL and MongoDB.
126181

127182
## Common mistakes
128183

129-
:::warning
184+
### Fetching everything to use one record
185+
186+
You wanted one record, so you queried and took the first element:
187+
188+
```typescript
189+
const users = await db.orm.public.User.where({ email }).all();
190+
const user = users[0];
191+
```
192+
193+
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:
194+
195+
```typescript
196+
const user = await db.orm.public.User.where({ email }).first();
197+
```
198+
199+
### Forgetting that .all() has no limit
200+
201+
`.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.
130202

131-
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.
203+
### Reusing a streamed result
132204

133-
:::
205+
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:
134206

135-
- Using `.all()` for a single record. Use `.first()`: it returns one record or `null`, without fetching the rest.
136-
- Expecting `.all()` to limit itself. It returns every match; add `.take(n)` when the table can grow.
137-
- Loading a huge result into memory. Iterate it instead of awaiting: `for await (const row of query.all())`.
207+
```typescript
208+
const posts = await db.orm.public.Post.all();
209+
// posts is a plain array now; read it as often as you like
210+
```
138211

139212
## Next
140213

0 commit comments

Comments
 (0)