You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: apps/docs/content/docs/orm/next/fundamentals/advanced-queries.mdx
+77-19Lines changed: 77 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,18 +1,31 @@
1
1
---
2
2
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.
4
4
url: /orm/next/fundamentals/advanced-queries
5
5
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.
7
7
---
8
8
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.
10
10
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.
12
12
13
-
## Query with the SQL builder (PostgreSQL)
13
+
## PostgreSQL: SQL query builder
14
14
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:
16
29
17
30
```typescript
18
31
import { db } from"./prisma/db";
@@ -26,11 +39,11 @@ const plan = db.sql.public.post
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).
30
43
31
-
### Join tables explicitly
44
+
### Join tables with precise control
32
45
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:
34
47
35
48
```typescript
36
49
const plan =db.sql.public.post
@@ -46,13 +59,26 @@ const plan = db.sql.public.post
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:
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:
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).
87
113
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
89
128
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:
|`$group`, `$lookup` with reshaping, any MongoDB aggregation | Pipeline builder (`db.query.from(...)`) |
136
193
137
194
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)`.
138
195
139
196
## Next
140
197
141
198
-[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.
142
200
-[Run SQL builder plans atomically](/orm/next/fundamentals/transactions) inside a transaction.
Copy file name to clipboardExpand all lines: apps/docs/content/docs/orm/next/fundamentals/reading-data.mdx
+84-11Lines changed: 84 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,9 +1,9 @@
1
1
---
2
2
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.
4
4
url: /orm/next/fundamentals/reading-data
5
5
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.
7
7
---
8
8
9
9
Read data by chaining query methods on a model, then running the chain with `.all()` for many records or `.first()` for one.
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`).
32
32
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`.
To combine conditions with OR or NOT on PostgreSQL, use the `or`, `and`, and `not` helpers, currently exported from `@prisma-next/sql-orm-client`.
73
73
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).
75
75
76
76
## Select fields
77
77
@@ -122,19 +122,92 @@ const result = await db.orm.public.Post
122
122
// result.total: number
123
123
```
124
124
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 =awaitdb.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
+
forawait (const post ofdb.orm.public.Post.all()) {
144
+
awaitexportToSearchIndex(post);
145
+
}
146
+
```
147
+
148
+
You can also leave the loop early; unprocessed records are never buffered:
149
+
150
+
```typescript
151
+
forawait (const post ofdb.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
+
forawait (const post ofresult) {
164
+
// ...
165
+
}
166
+
167
+
awaitresult;
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 =awaitdb.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.
126
181
127
182
## Common mistakes
128
183
129
-
:::warning
184
+
### Fetching everything to use one record
185
+
186
+
You wanted one record, so you queried and took the first element:
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 =awaitdb.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.
130
202
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
132
204
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:
134
206
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 =awaitdb.orm.public.Post.all();
209
+
// posts is a plain array now; read it as often as you like
0 commit comments