@prisma/orm-mongo 8.0.0-rc.9-dev.7 → 8.0.0-rc.9-dev.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/skills/prisma-8/SKILL.md +16 -13
- package/skills/prisma-8/references/contract.md +60 -31
- package/skills/prisma-8/references/debug.md +44 -41
- package/skills/prisma-8/references/migration-model.md +2 -2
- package/skills/prisma-8/references/migration-review.md +28 -15
- package/skills/prisma-8/references/migrations.md +85 -69
- package/skills/prisma-8/references/queries-mongo.md +16 -16
- package/skills/prisma-8/references/queries-postgres.md +78 -78
- package/skills/prisma-8/references/queries.md +54 -28
- package/skills/prisma-8/references/quickstart.md +32 -41
- package/skills/prisma-8/references/runtime.md +76 -54
- package/skills/prisma-8/references/supabase.md +15 -28
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.9-to-8.0.0-rc.10/instructions.md +1 -0
|
@@ -8,8 +8,8 @@ Shared concepts (result consumption, script teardown, cross-target pitfalls, cap
|
|
|
8
8
|
|
|
9
9
|
**Postgres** (`postgres<Contract>(...)` from `@internal/postgres/runtime`):
|
|
10
10
|
|
|
11
|
-
- **`db.orm.<Model>`** — ORM, PascalCase model name (`db.orm.User`). Fluent `.where(...).select(...).orderBy(...).all()`, fully typed against `Contract`. Default lane for CRUD with relations.
|
|
12
|
-
- **`db.sql.<table>`** — SQL builder, lowercase storage name (`db.sql.user`). Produces a *plan
|
|
11
|
+
- **`db.orm.<ns>.<Model>`** — ORM, PascalCase model name (`db.orm.public.User`). Fluent `.where(...).select(...).orderBy(...).all()`, fully typed against `Contract`. Default lane for CRUD with relations.
|
|
12
|
+
- **`db.sql.<ns>.<table>`** — SQL builder, lowercase storage name (`db.sql.public.user`). Produces a *plan*. A plan that returns rows (`select`, or a write with `.returning(...)`) runs through `db.runtime().query(plan)`; a write with no `RETURNING` runs through `db.runtime().execute(plan)`, which resolves `{ affectedRows }`. Use when the ORM is too high-level — explicit `JOIN`, computed projections, set operations, window functions.
|
|
13
13
|
|
|
14
14
|
Reach for the ORM first; drop to `db.sql` when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app.
|
|
15
15
|
|
|
@@ -17,33 +17,33 @@ Reach for the ORM first; drop to `db.sql` when the ORM can't express the shape.
|
|
|
17
17
|
|
|
18
18
|
| Need | Choose | Why |
|
|
19
19
|
| --- | --- | --- |
|
|
20
|
-
| Standard CRUD with relations | **ORM (`db.orm.<Model>`)** | Highest ergonomics; fully typed; model-shaped. |
|
|
20
|
+
| Standard CRUD with relations | **ORM (`db.orm.<ns>.<Model>`)** | Highest ergonomics; fully typed; model-shaped. |
|
|
21
21
|
| Eager-load related records | **ORM `.include(...)`** | Composes with `.where` / `.select` / `.orderBy` / `.limit` per branch. |
|
|
22
22
|
| Aggregate (count, sum, avg) | **ORM `.aggregate(...)`** | Typed result; works with grouping (`.groupBy(...).aggregate(...)`). |
|
|
23
|
-
| `INSERT ... RETURNING` / `UPDATE ... RETURNING` typed result | **ORM mutations** (returns updated rows) or **`db.sql.<t>.insert(...).returning(...)`** | ORM returns inserted/updated rows; SQL builder exposes `.returning(...)` explicitly. |
|
|
24
|
-
| Computed projection (e.g. `ST_DistanceSphere(location, point) AS meters`) alongside model fields | **SQL builder (`db.sql.<t>`)** | The ORM projects model fields; arbitrary expression projection is the SQL builder's seam. |
|
|
23
|
+
| `INSERT ... RETURNING` / `UPDATE ... RETURNING` typed result | **ORM mutations** (returns updated rows) or **`db.sql.<ns>.<t>.insert(...).returning(...)`** | ORM returns inserted/updated rows; SQL builder exposes `.returning(...)` explicitly. |
|
|
24
|
+
| Computed projection (e.g. `ST_DistanceSphere(location, point) AS meters`) alongside model fields | **SQL builder (`db.sql.<ns>.<t>`)** | The ORM projects model fields; arbitrary expression projection is the SQL builder's seam. |
|
|
25
25
|
| Complex `JOIN`, set operation, window function | **SQL builder** | The ORM doesn't express arbitrary joins. |
|
|
26
|
-
| Postgres-specific feature (`LATERAL`, `FILTER`, custom aggregates) | **SQL builder**, falling back to extension operators when the extension provides them | DSL first; extensions can contribute operators (`postgis`, `pgvector
|
|
26
|
+
| Postgres-specific feature (`LATERAL`, `FILTER`, custom aggregates) | **SQL builder**, falling back to extension operators when the extension provides them | DSL first; extensions can contribute operators (`postgis`, `pgvector`). |
|
|
27
27
|
|
|
28
28
|
## Workflow — ORM reads
|
|
29
29
|
|
|
30
|
-
The concept: `db.orm.<Model>` returns a *collection* you compose method-by-method. Each call returns a new collection (immutable chaining); the terminal verb (`.all()` / `.first()` / `.count()`
|
|
30
|
+
The concept: `db.orm.<ns>.<Model>` returns a *collection* you compose method-by-method. Each call returns a new collection (immutable chaining); the terminal verb (`.all()` / `.first()` / `.aggregate(...)`) issues the query. There is no `.count()` terminal on the collection — `count()` is an `include` reducer and an `aggregate(...)` operation (both below). Predicates are lambdas over a field proxy: `u.field.<op>(value)`.
|
|
31
31
|
|
|
32
32
|
```typescript
|
|
33
33
|
// src/queries/users.ts — one directory deep under src/, so the import is '../prisma/db'
|
|
34
34
|
import { db } from '../prisma/db';
|
|
35
35
|
|
|
36
36
|
// Find one record by primary key shorthand.
|
|
37
|
-
const user = await db.orm.User.first({ id: userId });
|
|
37
|
+
const user = await db.orm.public.User.first({ id: userId });
|
|
38
38
|
// Returns the full row or `null`.
|
|
39
39
|
|
|
40
40
|
// Find one matching a predicate.
|
|
41
|
-
const alice = await db.orm.User
|
|
41
|
+
const alice = await db.orm.public.User
|
|
42
42
|
.where((u) => u.email.eq('alice@example.com'))
|
|
43
43
|
.first();
|
|
44
44
|
|
|
45
45
|
// Find many with projection, sort, and limit.
|
|
46
|
-
const recentUsers = await db.orm.User
|
|
46
|
+
const recentUsers = await db.orm.public.User
|
|
47
47
|
.select('id', 'email', 'createdAt')
|
|
48
48
|
.orderBy((u) => u.createdAt.desc())
|
|
49
49
|
.limit(10)
|
|
@@ -54,38 +54,38 @@ const recentUsers = await db.orm.User
|
|
|
54
54
|
|
|
55
55
|
```typescript
|
|
56
56
|
// Lambda form — full expression power.
|
|
57
|
-
db.orm.User.where((u) => u.email.eq('alice@example.com'));
|
|
57
|
+
db.orm.public.User.where((u) => u.email.eq('alice@example.com'));
|
|
58
58
|
|
|
59
59
|
// Shorthand object form — equality on the named fields.
|
|
60
|
-
db.orm.User.where({ kind: 'admin' });
|
|
60
|
+
db.orm.public.User.where({ kind: 'admin' });
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Operators on the field proxy include `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte`, `.like`, `.ilike`, `.in([...])`, `.isNull()`, `.isNotNull()`. Extensions add target-specific operators on extension-typed columns (`pgvector`'s `.cosineDistance(...)`, `postgis`'s `.within(...)` / `.intersectsBbox(...)` / `.distanceSphere(...)
|
|
63
|
+
Operators on the field proxy include `.eq`, `.neq`, `.lt`, `.lte`, `.gt`, `.gte`, `.like`, `.ilike`, `.in([...])`, `.isNull()`, `.isNotNull()`. Extensions add target-specific operators on extension-typed columns (`pgvector`'s `.cosineDistance(...)`, `postgis`'s `.within(...)` / `.intersectsBbox(...)` / `.distanceSphere(...)`).
|
|
64
64
|
|
|
65
65
|
**There is no `.between(a, b)` operator.** Express ranges either as two chained `.where(...)` clauses (the idiomatic form — clauses AND-compose) or with the `and(...)` combinator inside one clause:
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
68
|
// Chained .where() — each clause AND-composes with the previous one.
|
|
69
|
-
await db.orm.Sale
|
|
69
|
+
await db.orm.public.Sale
|
|
70
70
|
.where((s) => s.day.gte(start))
|
|
71
71
|
.where((s) => s.day.lte(end))
|
|
72
72
|
.all();
|
|
73
73
|
|
|
74
74
|
// Equivalent with an explicit `and(...)` inside one clause.
|
|
75
|
-
import { and } from '@
|
|
76
|
-
await db.orm.Sale
|
|
75
|
+
import { and } from '@prisma/orm-postgres/orm-client';
|
|
76
|
+
await db.orm.public.Sale
|
|
77
77
|
.where((s) => and(s.day.gte(start), s.day.lte(end)))
|
|
78
78
|
.all();
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
The two forms emit the same SQL. Pick chained `.where()` when each clause adds a separate condition that reads as its own thought; pick `and(...)` when one logical predicate happens to have two parts and you want the visual grouping. Don't reach for a `between` helper — there isn't one.
|
|
82
82
|
|
|
83
|
-
**Combinators** (`and`, `or`, `not`) compose predicates, and **relation predicates** (`.some(...)`, `.none(...)`, `.every(...)`) recurse into a relation.
|
|
83
|
+
**Combinators** (`and`, `or`, `not`) compose predicates, and **relation predicates** (`.some(...)`, `.none(...)`, `.every(...)`) recurse into a relation. The combinators are exported from the façade's `orm-client` subpath:
|
|
84
84
|
|
|
85
85
|
```typescript
|
|
86
|
-
import { and, or, not } from '@
|
|
86
|
+
import { and, or, not } from '@prisma/orm-postgres/orm-client';
|
|
87
87
|
|
|
88
|
-
await db.orm.User
|
|
88
|
+
await db.orm.public.User
|
|
89
89
|
.where((u) =>
|
|
90
90
|
and(
|
|
91
91
|
or(u.kind.eq('admin'), u.email.ilike('%@example.com')),
|
|
@@ -98,7 +98,7 @@ await db.orm.User
|
|
|
98
98
|
**Sorting and pagination.** `.orderBy(...)` accepts a single lambda or an array of lambdas (each calling `.asc()` / `.desc()` on a field). `.limit(n)` limits; `.offset(n)` offsets.
|
|
99
99
|
|
|
100
100
|
```typescript
|
|
101
|
-
await db.orm.Post
|
|
101
|
+
await db.orm.public.Post
|
|
102
102
|
.where((p) => p.authorId.eq(userId))
|
|
103
103
|
.orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()])
|
|
104
104
|
.limit(20)
|
|
@@ -108,13 +108,13 @@ await db.orm.Post
|
|
|
108
108
|
**Cursor pagination.** Call `.cursor({ field: lastValue })` after `.orderBy(...)` to resume from a known position. The cursor requires a prior `orderBy` — the type system enforces this. Direction (forward or backward) follows the sort: ascending order means "greater than the cursor value", descending means "less than".
|
|
109
109
|
|
|
110
110
|
```typescript
|
|
111
|
-
const page1 = await db.orm.Post
|
|
111
|
+
const page1 = await db.orm.public.Post
|
|
112
112
|
.orderBy((p) => p.createdAt.desc())
|
|
113
113
|
.limit(20)
|
|
114
114
|
.all();
|
|
115
115
|
|
|
116
116
|
const last = page1[page1.length - 1]!;
|
|
117
|
-
const page2 = await db.orm.Post
|
|
117
|
+
const page2 = await db.orm.public.Post
|
|
118
118
|
.orderBy((p) => p.createdAt.desc())
|
|
119
119
|
.cursor({ createdAt: last.createdAt })
|
|
120
120
|
.limit(20)
|
|
@@ -130,7 +130,7 @@ Cursor keys must match fields in the active `orderBy`. For a composite `orderBy`
|
|
|
130
130
|
The concept: `.include('<relation>', (branch) => branch.<chain>)` adds a relation branch to the parent query. The branch is its own collection — compose `.where` / `.select` / `.orderBy` / `.limit` on it just like the parent.
|
|
131
131
|
|
|
132
132
|
```typescript
|
|
133
|
-
await db.orm.User
|
|
133
|
+
await db.orm.public.User
|
|
134
134
|
.select('id', 'email')
|
|
135
135
|
.include('posts', (post) =>
|
|
136
136
|
post
|
|
@@ -146,25 +146,25 @@ await db.orm.User
|
|
|
146
146
|
**Reduce a to-many relation to a scalar.** A refinement callback may return a *reducer* — `count()`, `sum(field)`, `avg(field)`, `min(field)`, `max(field)`, plus the lossless `countBigInt()`, `sumBigInt(field)`, and `avgDecimal(field)` — instead of a collection. The parent's relation field then carries that one value rather than an array. Reducers exist only inside an `include(...)` callback; calling one elsewhere throws.
|
|
147
147
|
|
|
148
148
|
```typescript
|
|
149
|
-
await db.orm.User.include('posts', (posts) => posts.count()).all();
|
|
149
|
+
await db.orm.public.User.include('posts', (posts) => posts.count()).all();
|
|
150
150
|
// → Array<{ ...user, posts: number }> — a parent with no posts reads 0
|
|
151
151
|
|
|
152
|
-
await db.orm.User.include('posts', (posts) => posts.sum('views')).all();
|
|
152
|
+
await db.orm.public.User.include('posts', (posts) => posts.sum('views')).all();
|
|
153
153
|
// → Array<{ ...user, posts: number | null }>
|
|
154
154
|
|
|
155
|
-
await db.orm.User.include('posts', (posts) => posts.avg('views')).all();
|
|
155
|
+
await db.orm.public.User.include('posts', (posts) => posts.avg('views')).all();
|
|
156
156
|
// → Array<{ ...user, posts: number | null }>
|
|
157
157
|
|
|
158
|
-
await db.orm.User.include('posts', (posts) => posts.min('views')).all();
|
|
159
|
-
await db.orm.User.include('posts', (posts) => posts.max('views')).all();
|
|
158
|
+
await db.orm.public.User.include('posts', (posts) => posts.min('views')).all();
|
|
159
|
+
await db.orm.public.User.include('posts', (posts) => posts.max('views')).all();
|
|
160
160
|
// → Array<{ ...user, posts: number | null }>
|
|
161
161
|
|
|
162
162
|
// The lossless form, for a total that may outgrow a JS number:
|
|
163
|
-
await db.orm.User.include('posts', (posts) => posts.sumBigInt('views')).all();
|
|
163
|
+
await db.orm.public.User.include('posts', (posts) => posts.sumBigInt('views')).all();
|
|
164
164
|
// → Array<{ ...user, posts: bigint | null }>
|
|
165
165
|
|
|
166
166
|
// Several sub-views of one relation at once:
|
|
167
|
-
await db.orm.User.include('posts', (posts) =>
|
|
167
|
+
await db.orm.public.User.include('posts', (posts) =>
|
|
168
168
|
posts.combine({ recent: posts.limit(3), total: posts.count() }),
|
|
169
169
|
).all();
|
|
170
170
|
// → Array<{ ...user, posts: { recent: Post[]; total: number } }>
|
|
@@ -178,27 +178,27 @@ Nested `1:N → 1:N` includes (e.g. `User → posts → comments`) require the c
|
|
|
178
178
|
|
|
179
179
|
```typescript
|
|
180
180
|
// Create — returns the inserted row.
|
|
181
|
-
const user = await db.orm.User.create({ id, email, displayName, kind, createdAt });
|
|
181
|
+
const user = await db.orm.public.User.create({ id, email, displayName, kind, createdAt });
|
|
182
182
|
|
|
183
183
|
// Create with selected return — narrows the return shape.
|
|
184
|
-
const summary = await db.orm.User
|
|
184
|
+
const summary = await db.orm.public.User
|
|
185
185
|
.select('id', 'email', 'kind')
|
|
186
186
|
.create({ id, email, displayName, kind, createdAt });
|
|
187
187
|
|
|
188
188
|
// Update by predicate.
|
|
189
|
-
await db.orm.User.where({ id }).update({ email: newEmail });
|
|
189
|
+
await db.orm.public.User.where({ id }).update({ email: newEmail });
|
|
190
190
|
|
|
191
191
|
// Update with selected return.
|
|
192
|
-
await db.orm.User
|
|
192
|
+
await db.orm.public.User
|
|
193
193
|
.where({ id })
|
|
194
194
|
.select('id', 'email', 'kind')
|
|
195
195
|
.update({ email: newEmail });
|
|
196
196
|
|
|
197
197
|
// Delete by predicate.
|
|
198
|
-
await db.orm.User.where({ id }).delete();
|
|
198
|
+
await db.orm.public.User.where({ id }).delete();
|
|
199
199
|
|
|
200
200
|
// Upsert — typed by the create branch's shape.
|
|
201
|
-
await db.orm.User
|
|
201
|
+
await db.orm.public.User
|
|
202
202
|
.select('id', 'email', 'kind', 'createdAt')
|
|
203
203
|
.upsert({
|
|
204
204
|
create: { id, email, displayName, kind, createdAt: new Date() },
|
|
@@ -211,18 +211,18 @@ The ORM returns inserted / updated rows by default. The `.returning(...)` select
|
|
|
211
211
|
## Workflow — Aggregates
|
|
212
212
|
|
|
213
213
|
```typescript
|
|
214
|
-
const totals = await db.orm.User.aggregate((aggregate) => ({
|
|
214
|
+
const totals = await db.orm.public.User.aggregate((aggregate) => ({
|
|
215
215
|
totalUsers: aggregate.count(),
|
|
216
216
|
}));
|
|
217
217
|
|
|
218
|
-
const adminTotals = await db.orm.User
|
|
218
|
+
const adminTotals = await db.orm.public.User
|
|
219
219
|
.where({ kind: 'admin' })
|
|
220
220
|
.aggregate((aggregate) => ({
|
|
221
221
|
adminUsers: aggregate.count(),
|
|
222
222
|
}));
|
|
223
223
|
|
|
224
224
|
// Group-by + aggregate.
|
|
225
|
-
const byKind = await db.orm.User
|
|
225
|
+
const byKind = await db.orm.public.User
|
|
226
226
|
.groupBy('kind')
|
|
227
227
|
.having((having) => having.count().gte(minUsers))
|
|
228
228
|
.aggregate((aggregate) => ({
|
|
@@ -261,7 +261,7 @@ const byKind = await db.orm.User
|
|
|
261
261
|
Nullability isn't a typing bug — it's faithful to what the database returns. Coalesce client-side when you want zero-fill:
|
|
262
262
|
|
|
263
263
|
```typescript
|
|
264
|
-
const revenue = await db.orm.Sale
|
|
264
|
+
const revenue = await db.orm.public.Sale
|
|
265
265
|
.where((s) => s.day.gte(start))
|
|
266
266
|
.aggregate((a) => ({ total: a.sum('amount') }));
|
|
267
267
|
// revenue.total: number | null
|
|
@@ -271,22 +271,22 @@ const safe = revenue.total ?? 0; // ← apply at the consumption site, not in
|
|
|
271
271
|
|
|
272
272
|
If `?? 0` is showing up on every aggregate, that's a signal you're calling `sum` (or peers) over potentially-empty filters — which is exactly when SQL returns NULL. The pattern is correct; the typing is honest.
|
|
273
273
|
|
|
274
|
-
## Workflow — SQL builder (`db.sql.<table>`)
|
|
274
|
+
## Workflow — SQL builder (`db.sql.<ns>.<table>`)
|
|
275
275
|
|
|
276
|
-
The concept: `db.sql.<table>` is a table-shaped builder that produces a *plan*. The plan is a serialisable description of the query (AST + parameters); you
|
|
276
|
+
The concept: `db.sql.<ns>.<table>` is a table-shaped builder that produces a *plan*. The plan is a serialisable description of the query (AST + parameters); you run it through the runtime. Pick the runtime method by the result the plan declares: `db.runtime().query(plan)` for rows (it returns the same `AsyncIterableResult` as `.all()`, so `await` it for an array), `db.runtime().execute(plan)` for a write with no `RETURNING` (it resolves `{ affectedRows }` and returns no rows). The builder gives you the lanes the ORM doesn't express — explicit `JOIN`, arbitrary expression projection, target-specific operations through extension helpers — without dropping to raw SQL.
|
|
277
277
|
|
|
278
278
|
```typescript
|
|
279
279
|
// src/queries/posts.ts — adjust the relative import to match file depth.
|
|
280
280
|
import { db } from '../prisma/db';
|
|
281
281
|
|
|
282
282
|
// Select with predicate and limit.
|
|
283
|
-
const plan = db.sql.post
|
|
283
|
+
const plan = db.sql.public.post
|
|
284
284
|
.select('id', 'title', 'userId', 'createdAt')
|
|
285
285
|
.where((f, fns) => fns.eq(f.userId, userId))
|
|
286
286
|
.limit(limit)
|
|
287
287
|
.build();
|
|
288
288
|
|
|
289
|
-
const rows = await db.runtime().
|
|
289
|
+
const rows = await db.runtime().query(plan);
|
|
290
290
|
```
|
|
291
291
|
|
|
292
292
|
The `.where(...)` callback receives `(fields, fns)` — `fields` is the field proxy (column references), `fns` is the operator namespace (`fns.eq`, `fns.ne`, `fns.gt`, …). Extensions inject extension-shaped helpers into the same `fns` namespace (`fns.distanceSphere`, `fns.cosineDistance`, etc.).
|
|
@@ -294,27 +294,27 @@ The `.where(...)` callback receives `(fields, fns)` — `fields` is the field pr
|
|
|
294
294
|
### `INSERT` / `UPDATE` / `DELETE` with `RETURNING`
|
|
295
295
|
|
|
296
296
|
```typescript
|
|
297
|
-
// Insert and return selected columns.
|
|
298
|
-
const plan = db.sql.user
|
|
299
|
-
.insert({ email })
|
|
297
|
+
// Insert and return selected columns. `insert()` takes an array of rows.
|
|
298
|
+
const plan = db.sql.public.user
|
|
299
|
+
.insert([{ email }])
|
|
300
300
|
.returning('id', 'email')
|
|
301
301
|
.build();
|
|
302
|
-
const [row] = await db.runtime().
|
|
302
|
+
const [row] = await db.runtime().query(plan);
|
|
303
303
|
|
|
304
304
|
// Update with predicate and returning.
|
|
305
|
-
const updatePlan = db.sql.user
|
|
305
|
+
const updatePlan = db.sql.public.user
|
|
306
306
|
.update({ email: newEmail })
|
|
307
307
|
.where((f, fns) => fns.eq(f.id, userId))
|
|
308
308
|
.returning('id', 'email')
|
|
309
309
|
.build();
|
|
310
|
-
const rows = await db.runtime().
|
|
310
|
+
const rows = await db.runtime().query(updatePlan);
|
|
311
311
|
|
|
312
|
-
// Delete with predicate.
|
|
313
|
-
const deletePlan = db.sql.user
|
|
312
|
+
// Delete with predicate, no RETURNING — `execute` resolves the affected count.
|
|
313
|
+
const deletePlan = db.sql.public.user
|
|
314
314
|
.delete()
|
|
315
315
|
.where((f, fns) => fns.eq(f.id, userId))
|
|
316
316
|
.build();
|
|
317
|
-
await db.runtime().execute(deletePlan);
|
|
317
|
+
const { affectedRows } = await db.runtime().execute(deletePlan);
|
|
318
318
|
```
|
|
319
319
|
|
|
320
320
|
`.returning(...)` requires the target adapter to advertise the `returning` capability. The Postgres adapter advertises it by default.
|
|
@@ -323,36 +323,36 @@ await db.runtime().execute(deletePlan);
|
|
|
323
323
|
|
|
324
324
|
```typescript
|
|
325
325
|
// Project a computed expression alongside model fields.
|
|
326
|
-
const plan = db.sql.cafe
|
|
326
|
+
const plan = db.sql.public.cafe
|
|
327
327
|
.select('id', 'name')
|
|
328
328
|
.select('meters', (f, fns) => fns.distanceSphere(f.location, point))
|
|
329
329
|
.orderBy((f, fns) => fns.distanceSphere(f.location, point), { direction: 'asc' })
|
|
330
330
|
.orderBy((f) => f.id, { direction: 'asc' })
|
|
331
331
|
.limit(limit)
|
|
332
332
|
.build();
|
|
333
|
-
const rows = await db.runtime().
|
|
333
|
+
const rows = await db.runtime().query(plan);
|
|
334
334
|
|
|
335
335
|
// Self-join with an alias.
|
|
336
|
-
db.sql.post
|
|
337
|
-
.innerJoin(db.sql.post.as('p2'), (f, fns) => fns.ne(f.p1.userId, f.p2.userId))
|
|
336
|
+
db.sql.public.post
|
|
337
|
+
.innerJoin(db.sql.public.post.as('p2'), (f, fns) => fns.ne(f.p1.userId, f.p2.userId))
|
|
338
338
|
// ...
|
|
339
339
|
.build();
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
## Workflow — Transactions
|
|
343
343
|
|
|
344
|
-
The concept: `db.transaction(fn)` opens a transaction and passes a `tx` context to the callback. `tx.orm` and `tx.sql` mirror `db.orm` / `db.sql` but ride the same transaction; `tx.execute(plan)`
|
|
344
|
+
The concept: `db.transaction(fn)` opens a transaction and passes a `tx` context to the callback. `tx.orm` and `tx.sql` mirror `db.orm` / `db.sql` but ride the same transaction; `tx.query(plan)` / `tx.execute(plan)` run a SQL-builder plan within it (rows vs affected count, as on the runtime). The transaction commits on the callback's successful return and rolls back on any thrown error.
|
|
345
345
|
|
|
346
346
|
```typescript
|
|
347
347
|
await db.transaction(async (tx) => {
|
|
348
|
-
const user = await tx.orm.User.create({ id, email });
|
|
349
|
-
await tx.orm.Post.create({ userId: user.id, title: 'hello' });
|
|
348
|
+
const user = await tx.orm.public.User.create({ id, email });
|
|
349
|
+
await tx.orm.public.Post.create({ userId: user.id, title: 'hello' });
|
|
350
350
|
|
|
351
|
-
// SQL-builder plan inside the transaction
|
|
352
|
-
const plan = tx.sql.post.update({ status: 'archived' })
|
|
351
|
+
// SQL-builder plan inside the transaction — no RETURNING, so `execute`.
|
|
352
|
+
const plan = tx.sql.public.post.update({ status: 'archived' })
|
|
353
353
|
.where((f, fns) => fns.lt(f.createdAt, cutoff))
|
|
354
354
|
.build();
|
|
355
|
-
await tx.execute(plan);
|
|
355
|
+
const { affectedRows } = await tx.execute(plan);
|
|
356
356
|
|
|
357
357
|
// If anything throws, all three operations roll back.
|
|
358
358
|
});
|
|
@@ -362,20 +362,20 @@ The callback's return value passes through `db.transaction(...)`. Capture insert
|
|
|
362
362
|
|
|
363
363
|
## Namespace-aware accessors
|
|
364
364
|
|
|
365
|
-
|
|
365
|
+
On Postgres both `db.sql` and `db.orm` are keyed by storage namespace (the Postgres schema) — always, not only when a contract declares more than one. A model outside any `namespace { }` block is in `public`:
|
|
366
366
|
|
|
367
367
|
```typescript
|
|
368
368
|
// db.sql.<namespace>.<table>
|
|
369
|
-
const plan = db.sql.public.
|
|
369
|
+
const plan = db.sql.public.user.select('id', 'email').build();
|
|
370
370
|
const authPlan = db.sql.auth.users.select('id', 'token').build();
|
|
371
|
-
await db.runtime().
|
|
371
|
+
const rows = await db.runtime().query(plan);
|
|
372
372
|
|
|
373
373
|
// db.orm.<namespace>.<Model>
|
|
374
374
|
const user = await db.orm.public.User.create({ id: 1, email: 'a@x.io' });
|
|
375
375
|
const authUser = await db.orm.auth.User.create({ id: 2, token: 'tok' });
|
|
376
376
|
```
|
|
377
377
|
|
|
378
|
-
|
|
378
|
+
There is no flat `db.sql.user` / `db.orm.User` on the Postgres façade (`Db` is "one facet per storage namespace, and nothing else"); reaching for one is a type error. The flat spelling belongs to SQLite, whose façade exposes its single unbound namespace directly — see `references/queries.md` § *Namespace-aware accessors*.
|
|
379
379
|
|
|
380
380
|
Cross-namespace relations (e.g. `public.Profile` → `auth.User`) follow the same `.include()` syntax; the ORM resolves the correct schema-qualified join automatically.
|
|
381
381
|
|
|
@@ -385,13 +385,13 @@ Cross-namespace relations (e.g. `public.Profile` → `auth.User`) follow the sam
|
|
|
385
385
|
2. **Using `.all()` when you wanted one row.** `.all()` issues no implicit limit. Use `.first()` or `.first({ pk })`.
|
|
386
386
|
3. **Coalescing `count()` with `?? 0` "just in case".** `count()` is `number`, not `number | null` — SQL answers an empty set with `0`. The `?? 0` belongs on `sum` / `avg` / `min` / `max`, and its zero should match the aggregate's own type (`0` for an integer sum, `0n` for `sumBigInt`, `'0'` where the result is a decimal string).
|
|
387
387
|
4. **Reaching for `.between(a, b)` on a field proxy.** It doesn't exist. Either chain `.where((m) => m.field.gte(a)).where((m) => m.field.lte(b))` or use `and(m.field.gte(a), m.field.lte(b))` inside one `.where()` clause.
|
|
388
|
-
5. **Importing `and` / `or` / `not` from
|
|
389
|
-
6. **Trying to `db.sql.from(tables.user)`.** That surface does not exist. The builder is table-shaped: `db.sql.<tableName>.select(...)`. There is no `db.schema.tables` either.
|
|
390
|
-
7. **Trying to `db.execute(plan)` directly
|
|
391
|
-
8. **Setting `capabilities: { lateral: true }` in `prisma.config.ts`.** `
|
|
392
|
-
9. **Confabulating a TypedSQL or `.stream()` surface.** Neither exists
|
|
393
|
-
10. **Mixing the ORM mutation return with `runtime.execute(plan)`.** ORM terminals issue the query themselves and return rows.
|
|
394
|
-
11. **Ordering grouped rows by an aggregate metric.** The grouped collection supports `.orderBy(...)` on group keys plus `.limit(...)` / `.offset(...)`, but it cannot order by an aggregate alias such as `SUM(amount)`. Sorting the materialized aggregate result in JS is fine at small cardinalities; for large grouped result sets, drop to `db.sql.<table>`.
|
|
388
|
+
5. **Importing `and` / `or` / `not` from an internal package.** They are exported from `@prisma/orm-postgres/orm-client`; do not reach into `@internal/sql-orm-client`.
|
|
389
|
+
6. **Trying to `db.sql.from(tables.user)`.** That surface does not exist. The builder is table-shaped: `db.sql.<ns>.<tableName>.select(...)`. There is no `db.schema.tables` either.
|
|
390
|
+
7. **Trying to `db.execute(plan)` directly, or reading rows with `execute`.** Plans run through the runtime: `db.runtime().query(plan)` for rows, `db.runtime().execute(plan)` for a non-returning write (`{ affectedRows }`). Inside a transaction, `tx.query(plan)` / `tx.execute(plan)`. `execute` never yields rows — a `select` or `.returning(...)` plan passed to it gives you statistics, not data.
|
|
391
|
+
8. **Setting `capabilities: { lateral: true }` in `prisma.config.ts`.** The ORM config (`ormConfig({...})`) does not take `capabilities`. Capabilities are declared by the active adapter and become part of the emitted contract; the Postgres adapter advertises `lateral`, `jsonAgg`, and `returning` out of the box. Enable extension capabilities through `extensions: [...]` in the config (see `references/contract.md`).
|
|
392
|
+
9. **Confabulating a TypedSQL or `.stream()` surface.** Neither exists. Raw SQL does: the client's raw lane, ``db.raw.sql`…` ``. Reusable statements do: `db.prepare(...)` (see *Prepared statements* in [`queries.md`](./queries.md)). Streaming: `for await` over a read terminal or `runtime.query(plan)` — with the caveats in *Streaming* in [`queries.md`](./queries.md).
|
|
393
|
+
10. **Mixing the ORM mutation return with `runtime.query(plan)` / `runtime.execute(plan)`.** ORM terminals issue the query themselves and return rows. The runtime methods are for SQL-builder plans.
|
|
394
|
+
11. **Ordering grouped rows by an aggregate metric.** The grouped collection supports `.orderBy(...)` on group keys plus `.limit(...)` / `.offset(...)`, but it cannot order by an aggregate alias such as `SUM(amount)`. Sorting the materialized aggregate result in JS is fine at small cardinalities; for large grouped result sets, drop to `db.sql.<ns>.<table>`.
|
|
395
395
|
|
|
396
396
|
## Reference Files
|
|
397
397
|
|
|
@@ -408,8 +408,8 @@ Cross-namespace relations (e.g. `public.Profile` → `auth.User`) follow the sam
|
|
|
408
408
|
- [ ] Compared and serialised aggregate *results* as what they are — a `bigint` from a suffixed variant needs `0n` literals and `String(value)` rather than bare `JSON.stringify` — leaving the ORM's `having(...)` operands as numbers, and matching each SQL-builder comparison literal to the aggregate's own result codec (`fns.gt(fns.count(), 1)`).
|
|
409
409
|
- [ ] Expressed ranges as chained `.where(...)` clauses or a single `and(...)` clause — did NOT reach for a non-existent `.between(...)` operator.
|
|
410
410
|
- [ ] For cursor pagination, used `.orderBy(...).cursor({ field: lastValue }).limit(n).all()` — did NOT hand-write a `.where(p => p.field.lt(cursor))` workaround when the `.cursor()` API serves the same purpose.
|
|
411
|
-
- [ ] For ORM combinators, imported `and` / `or` / `not` from
|
|
412
|
-
- [ ]
|
|
411
|
+
- [ ] For ORM combinators, imported `and` / `or` / `not` from `@prisma/orm-postgres/orm-client`.
|
|
412
|
+
- [ ] Ran SQL-builder plans via `db.runtime().query(plan)` when they return rows and `db.runtime().execute(plan)` only for non-returning writes (`tx.query` / `tx.execute` inside a transaction). Passed `insert()` an array of rows.
|
|
413
413
|
- [ ] Wrapped multi-statement work in `db.transaction(async (tx) => { ... })` where atomicity matters.
|
|
414
|
-
- [ ] For top-N grouped aggregates at meaningful scale, dropped to `db.sql.<table>` rather than JS-side sort + slice over `groupBy(...).aggregate(...)`.
|
|
415
|
-
- [ ] Did NOT confabulate TypedSQL, `.stream()`, `db.batch`, `.between(...)`, a `capabilities` field on
|
|
414
|
+
- [ ] For top-N grouped aggregates at meaningful scale, dropped to `db.sql.<ns>.<table>` rather than JS-side sort + slice over `groupBy(...).aggregate(...)`.
|
|
415
|
+
- [ ] Did NOT confabulate TypedSQL, `.stream()`, `db.batch`, `.between(...)`, a collection-level `.count()`, a `capabilities` field on the config, or a `db.sql.from(tables.user)` API — routed to *What Prisma 8 doesn't do yet* / `references/feedback.md` instead. Raw SQL is spelled `db.raw.sql`, not `db.sql.raw`.
|
|
@@ -28,22 +28,22 @@ Prisma 8 ships **two query lanes per target** on the same `db` value from `src/p
|
|
|
28
28
|
|
|
29
29
|
| Runtime import in `db.ts` | Load |
|
|
30
30
|
| --- | --- |
|
|
31
|
-
| `@internal/postgres/runtime` | [`queries-postgres.md`](./queries-postgres.md) — `db.orm.<Model>` + `db.sql.<table>` |
|
|
31
|
+
| `@internal/postgres/runtime` | [`queries-postgres.md`](./queries-postgres.md) — `db.orm.<ns>.<Model>` + `db.sql.<ns>.<table>` |
|
|
32
32
|
| `@internal/mongo/runtime` | [`queries-mongo.md`](./queries-mongo.md) — `db.orm.<root>` + `db.query.from(...)` |
|
|
33
|
-
| `@internal/extension-supabase/runtime` | [`queries-postgres.md`](./queries-postgres.md) — a Supabase `RoleBoundDb` is a Postgres surface (`db.orm.<Model>` + `db.sql.<table>`); bind a role first via `references/supabase.md` |
|
|
33
|
+
| `@internal/extension-supabase/runtime` | [`queries-postgres.md`](./queries-postgres.md) — a Supabase `RoleBoundDb` is a Postgres surface (`db.orm.<ns>.<Model>` + `db.sql.<ns>.<table>`); bind a role first via `references/supabase.md` |
|
|
34
34
|
|
|
35
35
|
Both targets share the contract and connection on one `db` value. Reach for the ORM first; drop to the lower-level lane when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app.
|
|
36
36
|
|
|
37
|
-
**Do not mix target examples.** Postgres uses PascalCase model roots (`db.orm.User`) and `db.sql.user`; Mongo uses lowercased plural roots (`db.orm.users`) and `db.query.from('users')`. There is no `db.sql` on Mongo and no `db.query` SQL-builder equivalent on Postgres.
|
|
37
|
+
**Do not mix target examples.** Postgres uses PascalCase model roots (`db.orm.public.User`) and `db.sql.public.user`; Mongo uses lowercased plural roots (`db.orm.users`) and `db.query.from('users')`. There is no `db.sql` on Mongo and no `db.query` SQL-builder equivalent on Postgres.
|
|
38
38
|
|
|
39
39
|
## Namespace-aware accessors
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
On Postgres, models and tables are **always** addressed by namespace coordinate — the storage namespace is the Postgres schema, and a model declared outside any `namespace { }` block lands in `public`:
|
|
42
42
|
|
|
43
43
|
- **ORM**: `db.orm.<namespace>.<Model>` — e.g. `db.orm.public.User`, `db.orm.auth.User`
|
|
44
|
-
- **SQL builder**: `db.sql.<namespace>.<table>` — e.g. `db.sql.public.
|
|
44
|
+
- **SQL builder**: `db.sql.<namespace>.<table>` — e.g. `db.sql.public.user`, `db.sql.auth.users`
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
There is no flat `db.orm.User` / `db.sql.user` on the Postgres façade: `db.sql` is one facet per storage namespace and nothing else, and `db.orm` is keyed the same way (`examples/prisma-8-demo` uses `db.orm.public.User` and `db.sql.public.user` throughout). SQLite has no schemas, so its façade exposes the single unbound namespace directly — `db.orm.User` and `db.sql.user` are the SQLite spellings (`examples/prisma-8-demo-sqlite`). Mongo is keyed by collection storage name (`db.orm.users`).
|
|
47
47
|
|
|
48
48
|
See [`queries-postgres.md` § Namespace-aware accessors](./queries-postgres.md#namespace-aware-accessors) for a worked example.
|
|
49
49
|
|
|
@@ -52,7 +52,7 @@ See [`queries-postgres.md` § Namespace-aware accessors](./queries-postgres.md#n
|
|
|
52
52
|
Critical to get right early — on **both Postgres and Mongo**, `.all()` returns an **`AsyncIterableResult<Row>`**, which is *both* a `PromiseLike<Row[]>` and an `AsyncIterable<Row>`. That means three consumption forms all work, and the canonical one is the shortest:
|
|
53
53
|
|
|
54
54
|
```typescript
|
|
55
|
-
const users = await db.orm.User.select('id', 'email').all();
|
|
55
|
+
const users = await db.orm.public.User.select('id', 'email').all();
|
|
56
56
|
// ^? Row[] ← the Thenable resolves to a real array. This is the default idiom.
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -67,12 +67,11 @@ You do **not** need a `collect()` / `toArray()` helper — `await` is enough. In
|
|
|
67
67
|
// `Promise.race` combinators all accept the thenable directly — those are
|
|
68
68
|
// NOT reasons to call `.toArray()`. Whenever you are just going to await it
|
|
69
69
|
// here, use `await ...all()` and skip `.toArray()`.
|
|
70
|
-
const rows: Promise<User[]> = db.orm.User.select('id', 'email').all().toArray();
|
|
70
|
+
const rows: Promise<User[]> = db.orm.public.User.select('id', 'email').all().toArray();
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
for await (const user of db.orm.User.select('id', 'email').all()) {
|
|
72
|
+
// Iterate — decode and handle rows one at a time. Whether the raw rows are
|
|
73
|
+
// also fetched incrementally depends on the façade; see *Streaming* below.
|
|
74
|
+
for await (const user of db.orm.public.User.select('id', 'email').all()) {
|
|
76
75
|
process(user);
|
|
77
76
|
}
|
|
78
77
|
```
|
|
@@ -80,9 +79,9 @@ for await (const user of db.orm.User.select('id', 'email').all()) {
|
|
|
80
79
|
Two single-row shortcuts also exist on the result, in addition to the collection-level `.first()` (which issues `LIMIT 1` on Postgres):
|
|
81
80
|
|
|
82
81
|
```typescript
|
|
83
|
-
const user = await db.orm.User.where({ id }).all().first();
|
|
82
|
+
const user = await db.orm.public.User.where({ id }).all().first();
|
|
84
83
|
// ^? Row | null ← buffers, returns the first row or null. Issues no LIMIT.
|
|
85
|
-
const required = await db.orm.User.where({ id }).all().firstOrThrow();
|
|
84
|
+
const required = await db.orm.public.User.where({ id }).all().firstOrThrow();
|
|
86
85
|
// ^? Row ← buffers; throws `RUNTIME.NO_ROWS` if empty.
|
|
87
86
|
```
|
|
88
87
|
|
|
@@ -92,12 +91,12 @@ For genuine single-row reads, prefer the *collection*-level `.first()` (which ad
|
|
|
92
91
|
|
|
93
92
|
```typescript
|
|
94
93
|
// Bad — second await throws RUNTIME.ITERATOR_CONSUMED.
|
|
95
|
-
const result = db.orm.User.select('id', 'email').all();
|
|
94
|
+
const result = db.orm.public.User.select('id', 'email').all();
|
|
96
95
|
const a = await result;
|
|
97
96
|
const b = await result;
|
|
98
97
|
|
|
99
98
|
// Good — buffer once, reuse the array.
|
|
100
|
-
const users = await db.orm.User.select('id', 'email').all();
|
|
99
|
+
const users = await db.orm.public.User.select('id', 'email').all();
|
|
101
100
|
const a = users;
|
|
102
101
|
const b = users;
|
|
103
102
|
```
|
|
@@ -114,7 +113,7 @@ import { db } from '../prisma/db';
|
|
|
114
113
|
|
|
115
114
|
// Postgres — PascalCase model root from contract
|
|
116
115
|
for (const u of users) {
|
|
117
|
-
await db.orm.User.create(u);
|
|
116
|
+
await db.orm.public.User.create(u);
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
// Mongo — lowercased plural root from contract (e.g. users, not User)
|
|
@@ -126,11 +125,40 @@ console.log('Seeded.');
|
|
|
126
125
|
await db.close();
|
|
127
126
|
```
|
|
128
127
|
|
|
128
|
+
## Streaming
|
|
129
|
+
|
|
130
|
+
Every read terminal (`.all()`, and `runtime.query(plan)` for a SQL-builder plan) returns an `AsyncIterableResult`, so `for await` is always available. What it buys you depends on the façade:
|
|
131
|
+
|
|
132
|
+
- **Long-lived `postgres()` façade** (the usual `db.ts`): the driver runs with cursors disabled. The full result set is fetched from the server before the first row is yielded; only *decoding* happens per row. `for await` therefore does not bound the memory held by the raw result. For very large sets, paginate (`.limit()` / `.offset()`, or `.orderBy(...).cursor(...)`) instead.
|
|
133
|
+
- **Serverless façade** (`@prisma/orm-postgres/serverless`, one `connect()` per invocation): the driver reads through a server-side cursor in batches of 100 rows by default (`cursor: { batchSize }` on the façade options), so `for await` really does stream.
|
|
134
|
+
|
|
135
|
+
There is no `.stream()` method on either façade.
|
|
136
|
+
|
|
137
|
+
## Prepared statements (Postgres, SQLite)
|
|
138
|
+
|
|
139
|
+
`db.prepare(declaration, (sql, params) => plan)` builds a statement once and binds it per call. The declaration names each parameter's codec (`{ email: 'pg/text@1' }`); the callback receives the façade's `sql` builder plus typed `params` and returns a plan. A row-returning plan gives a `PreparedStatement` you run with `ps.query(runtime, params)`; a plan whose result is an affected count gives a `PreparedExecution` you run with `ps.execute(runtime, params)`. Declaring a parameter the plan never references throws `RUNTIME.PREPARE_UNUSED_PARAM`. `runtime.prepare(declaration, (params) => plan)` is the same thing on a `Runtime`.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// examples/prisma-8-demo/src/queries/get-user-by-email-prepared.ts
|
|
143
|
+
const ps = await db.prepare({ email: 'pg/text@1' }, (sql, params) =>
|
|
144
|
+
sql.public.user
|
|
145
|
+
.select('id', 'email', 'displayName', 'createdAt', 'kind')
|
|
146
|
+
.where((f, fns) => fns.eq(f.email, params.email))
|
|
147
|
+
.limit(1)
|
|
148
|
+
.build(),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const runtime = db.runtime();
|
|
152
|
+
for (const email of emails) {
|
|
153
|
+
const rows = await ps.query(runtime, { email });
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
129
157
|
## Naming model and result types
|
|
130
158
|
|
|
131
159
|
The model is the whole row plus its relations, and each related model carries its own relations in turn, so no query returns a value of the model type. A query returns the fields it fetched. The default fetch returns `Scalars<Model>`, the model without relations: `db.orm.public.User.first()` returns `Scalars<Model> | null`, and `db.orm.public.User.all()` returns `Scalars<Model>[]` (or its async iterable). Four types cover every case, and none needs a client in scope:
|
|
132
160
|
|
|
133
|
-
- `Models.<ns>_<Model>` (from `contract.d.ts`) — every scalar field and every relation. On SQLite, which has no schemas, the name is bare: `Models.User` and `typeof models.User`. On Postgres the schema is part of the name: `Models.public_User`, or `typeof models.public.User` by dotted access
|
|
161
|
+
- `Models.<ns>_<Model>` (from `contract.d.ts`) — every scalar field and every relation. On SQLite, which has no schemas, the name is bare: `Models.User` and `typeof models.User`. On Postgres the schema is part of the name: `Models.public_User`, or `typeof models.public.User` by dotted access — a model declared outside any `namespace { }` block is in `public`, so that is also its name. Mongo names its models the same way. A polymorphic base also emits one member per variant and an `Any<Base>` union (`Models.public_AnyTask`).
|
|
134
162
|
- `Scalars<M>` — the model without relations; what a default fetch returns. Distributes over unions, so `Scalars<Models.public_AnyTask>` is the union of variant rows.
|
|
135
163
|
- `Shape<M, Spec>` — a data structure derived from the model, for declaring an endpoint's response type once and having the compiler check the body at the `return`. At every level of `Spec`: `'+'` is a union of scalar and relation names to keep (a relation named there comes with all of its scalars and none of its relations; the scalars are narrowed only when `'+'` names a scalar, so `'+': 'posts'` alone is every scalar plus posts); `'-'` is a union of scalar names to drop; `'+'` naming a scalar beside `'-'` is a compile error, while `{ '-': 'passwordHash'; '+': 'posts' }` is every scalar but the hash plus posts; any other key is a relation whose value is a nested spec that narrows the related model. Relations are absent unless asked for; `X[]`, `X | null`, or `X` comes from the model. Wrong names, a relation in `'-'`, a non-object relation value, and a relation both in `'+'` and as a key are compile errors. No `where`/`orderBy`/`limit`; compose extras with TypeScript (`Shape<M> & { postCount: number }`).
|
|
136
164
|
- `ResultType<typeof query>` — the row of any ORM collection value (plain, `.include()`, `.select()`, `.variant()`), and of SQL lane plans. Bind the query to a name first; `typeof` needs a value.
|
|
@@ -179,18 +207,16 @@ Target-specific pitfalls live in the per-target guides.
|
|
|
179
207
|
|
|
180
208
|
## What Prisma 8 doesn't do yet
|
|
181
209
|
|
|
182
|
-
- **
|
|
183
|
-
- **
|
|
184
|
-
- **`and` / `or` / `not` combinators in the postgres façade.** The combinators currently import from `@internal/sql-orm-client` (an internal package). Workaround today: import them from `@internal/sql-orm-client` directly, the way the example apps do. If you want them on `@internal/postgres/runtime`, file a feature request via `references/feedback.md`.
|
|
185
|
-
- **Ordering grouped aggregates by an aggregate alias (Postgres).** `db.orm.<Model>.groupBy(...)` supports `.orderBy(...)` on group keys plus `.limit(...)` / `.offset(...)`, but the grouped collection cannot order by an aggregate alias such as `SUM(amount)`. A "top-N groups by SUM" query therefore falls back to JS-side sort + slice over the full grouped result, which is fine at small cardinalities and bad at scale. Workarounds: (a) drop to `db.sql.<table>` and write the `GROUP BY` + `ORDER BY` + `LIMIT` against the aggregated table directly; (b) live with the JS-side sort/slice if the grouped cardinality is bounded. File a feature request via `references/feedback.md` if this is hitting you in production.
|
|
210
|
+
- **Many-to-many relations work through the junction.** `.include('tags', (tag) => tag.select(...))` traverses an N:M relation's `through` table, and nested `create` / `connect` / `disconnect` on an N:M relation write the junction rows for you (`examples/prisma-8-demo/src/orm-client/get-post-tags.ts`, `create-post-with-tags.ts`). The one refusal: a junction with required payload columns the relation API cannot populate throws `ORM.RELATION_MUTATION_UNSUPPORTED` — write that junction directly or use the SQL builder.
|
|
211
|
+
- **Ordering grouped aggregates by an aggregate alias (Postgres).** `db.orm.<ns>.<Model>.groupBy(...)` supports `.orderBy(...)` on group keys plus `.limit(...)` / `.offset(...)`, but the grouped collection cannot order by an aggregate alias such as `SUM(amount)`. A "top-N groups by SUM" query therefore falls back to JS-side sort + slice over the full grouped result, which is fine at small cardinalities and bad at scale. Workarounds: (a) drop to `db.sql.<ns>.<table>` and write the `GROUP BY` + `ORDER BY` + `LIMIT` against the aggregated table directly; (b) live with the JS-side sort/slice if the grouped cardinality is bounded. File a feature request via `references/feedback.md` if this is hitting you in production.
|
|
186
212
|
- **A raw-SQL lane.** This one exists. Write whole-query raw SQL through the client's raw lane: ``db.raw.sql`SELECT ...`.returnsRow({ ... }).build()`` for rows, or `.affectedCount()` for a mutation's row count. Each declared column names the codec that decodes it, so the row stays typed. For an expression fragment inside a builder query, use `fns.raw` in a `.select(...)` callback instead.
|
|
187
|
-
- **TypedSQL (`.sql` files compiled into typed callables).** Not implemented.
|
|
213
|
+
- **TypedSQL (`.sql` files compiled into typed callables).** Not implemented. For a repeated query, use `db.prepare(...)` (see *Prepared statements* above) or a function that returns the built plan and `db.runtime().query(plan)` at the call site. If you want a `.sql`-file compile path, file a feature request via `references/feedback.md`.
|
|
188
214
|
- **`EXPLAIN` / query-plan inspection.** Prisma 8 does not expose an `.explain()` method. Workaround: connect a `pg.Pool` you control via the runtime's `pg:` binding (see `references/runtime.md`) and issue `EXPLAIN ANALYZE` through it. If you want a first-class plan-inspection surface, file a feature request via `references/feedback.md`.
|
|
189
|
-
- **
|
|
215
|
+
- **Cursor-backed streaming on the long-lived façade.** `for await` works everywhere, but on `postgres()` the raw result is fetched in full before iteration (see *Streaming* above); only the serverless façade reads through a cursor. Paginate for very large sets on the long-lived façade. If you want cursor streaming there, file a feature request via `references/feedback.md`.
|
|
190
216
|
- **Multi-statement batching (Prisma-7-style `db.$transaction([call1, call2])`).** Prisma 8 runs each call sequentially. Workaround: wrap atomically-related work in `db.transaction(async (tx) => { ... })` on Postgres. If you want batch-as-array semantics, file a feature request via `references/feedback.md`.
|
|
191
217
|
- **Mongo façade transactions.** `@internal/mongo/runtime` does not expose `db.transaction(...)`. Multi-document atomicity is not yet wrapped in the Prisma 8 Mongo façade. Workaround: use the MongoDB driver's session API directly if you control the client binding (`mongoClient:` option). File a feature request via `references/feedback.md` if you need a first-class façade surface.
|
|
192
|
-
- **Mongo ORM aggregates.** No `.aggregate(...)` / `.groupBy(...)` on `db.orm.<root>`. Workaround: express aggregations through `db.query.from(...).group(...).build()` and `runtime.
|
|
193
|
-
- **Mongo filter helpers on the façade.** Rich filters (`.in`, ranges, boolean composition) currently import from `@
|
|
218
|
+
- **Mongo ORM aggregates.** No `.aggregate(...)` / `.groupBy(...)` on `db.orm.<root>`. Workaround: express aggregations through `db.query.from(...).group(...).build()` and `runtime.query(plan)`.
|
|
219
|
+
- **Mongo filter helpers on the façade.** Rich filters (`.in`, ranges, boolean composition) currently import from `@prisma/orm-mongo/query-ast/execution` (`MongoFieldFilter`, etc.) — not re-exported on `@internal/mongo/runtime`. Workaround: use object equality `.where({ field: value })` where possible; import from the internal package only when necessary. Tracked alongside façade-completeness gaps in Linear `TML-2526`.
|
|
194
220
|
- **Automatic N+1 detection.** Prisma 8 does not warn when an `.include(...)` is missing. Workaround: be deliberate about `.include(...)` in code review; the `lints` middleware (see `references/runtime.md`) catches the more common authoring slips (missing `WHERE` on a `DELETE` / `UPDATE`, missing `LIMIT` on a `SELECT`).
|
|
195
221
|
|
|
196
222
|
## Reference Files
|
|
@@ -203,9 +229,9 @@ This skill is split for selective loading. Target-specific reference paths live
|
|
|
203
229
|
## Checklist
|
|
204
230
|
|
|
205
231
|
- [ ] Confirmed the active target from `db.ts` and loaded the matching guide ([`queries-postgres.md`](./queries-postgres.md) or [`queries-mongo.md`](./queries-mongo.md)).
|
|
206
|
-
- [ ]
|
|
232
|
+
- [ ] On Postgres, used `db.orm.<ns>.<Model>` / `db.sql.<ns>.<table>` coordinates (usually `public`) — not a flat `db.orm.User`, which exists only on SQLite.
|
|
207
233
|
- [ ] Chose the right lane (ORM by default; lower-level builder for shapes the ORM doesn't express).
|
|
208
234
|
- [ ] Used `.first()` / `.first({ pk })` (Postgres) or `.where({ ... }).first()` (Mongo) for single-row reads — not `.all()`.
|
|
209
|
-
- [ ] Consumed `.all()` with plain `await` (not a `collect()` / `toArray()` helper). Used `for await` only when
|
|
235
|
+
- [ ] Consumed `.all()` with plain `await` (not a `collect()` / `toArray()` helper). Used `for await` only when per-row handling is actually wanted — and did not promise it bounds memory on the long-lived façade — and never iterated the same result twice.
|
|
210
236
|
- [ ] Did NOT use `db.sql` on a Mongo project or `db.query` where the Postgres SQL builder is meant.
|
|
211
237
|
- [ ] Completed the target-specific checklist in the loaded guide.
|