@prisma/orm-sqlite 8.0.0-rc.9-dev.8 → 8.0.0-rc.9-dev.10
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/build.md +1 -1
- package/skills/prisma-8/references/contract.md +60 -31
- package/skills/prisma-8/references/debug.md +44 -41
- package/skills/prisma-8/references/feedback.md +8 -8
- 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 +86 -70
- 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 +34 -43
- package/skills/prisma-8/references/runtime.md +76 -54
- package/skills/prisma-8/references/supabase.md +15 -28
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.9-to-8.0.0-rc.10/instructions.md +42 -1
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.9-to-8.0.0-rc.10/instructions.md +29 -1
|
@@ -9,7 +9,7 @@ Shared concepts (result consumption, script teardown, cross-target pitfalls, cap
|
|
|
9
9
|
**Mongo** (`mongo<Contract>(...)` from `@internal/mongo/runtime`):
|
|
10
10
|
|
|
11
11
|
- **`db.orm.<root>`** — ORM, lowercased plural contract root (`db.orm.users`, `db.orm.posts`). Same fluent chaining; `.where({ field: value })` object equality is the idiomatic filter form.
|
|
12
|
-
- **`db.query`** — typed aggregation-pipeline builder. Start with `db.query.from('<root>')`, chain `.match(...)` / `.project(...)` / `.group(...)` / `.lookup(...)`, terminal with `.build()`.
|
|
12
|
+
- **`db.query`** — typed aggregation-pipeline builder. Start with `db.query.from('<root>')`, chain `.match(...)` / `.project(...)` / `.group(...)` / `.lookup(...)`, terminal with `.build()`. Run via `(await db.runtime()).query(plan)` for anything that returns documents; `execute(plan)` is only for a write you want an affected count from, and it throws `RUNTIME.MONGO_STATISTICS_UNSUPPORTED` on a find or aggregate.
|
|
13
13
|
|
|
14
14
|
Reach for the ORM first; drop to `db.query` when the ORM can't express the shape. Lane choice is local — one query function picks one lane, not the whole app.
|
|
15
15
|
|
|
@@ -49,7 +49,7 @@ const recent = await db.orm.posts
|
|
|
49
49
|
|
|
50
50
|
**`.where(...)`** accepts a plain object whose keys are model field names and values are compared with equality (codec-aware — `ObjectId` fields accept string ids from the contract). Chain multiple `.where({ ... })` calls to AND-compose filters.
|
|
51
51
|
|
|
52
|
-
For operators the object form doesn't cover (`.in([...])`, range comparisons, nested logic), pass a `MongoFilterExpr` — today that means importing filter helpers from `@
|
|
52
|
+
For operators the object form doesn't cover (`.in([...])`, range comparisons, nested logic), pass a `MongoFilterExpr` — today that means importing filter helpers from `@prisma/orm-mongo/query-ast/execution` (a façade-completeness gap; see *What Prisma 8 doesn't do yet* in [`queries.md`](./queries.md)). Prefer the object form whenever equality suffices.
|
|
53
53
|
|
|
54
54
|
**Polymorphic roots.** When the contract declares variants on a model, narrow before querying:
|
|
55
55
|
|
|
@@ -123,7 +123,7 @@ await db.orm.users.where({ email: 'alice@example.com' }).upsert({
|
|
|
123
123
|
The Mongo ORM does not expose `.aggregate(...)` / `.groupBy(...)`. Express aggregations through **`db.query`** — the pipeline builder — with `.group(...)` and accumulator helpers:
|
|
124
124
|
|
|
125
125
|
```typescript
|
|
126
|
-
import { acc } from '@
|
|
126
|
+
import { acc } from '@prisma/orm-mongo/query-builder';
|
|
127
127
|
|
|
128
128
|
const runtime = await db.runtime();
|
|
129
129
|
const plan = db.query
|
|
@@ -137,18 +137,18 @@ const plan = db.query
|
|
|
137
137
|
.sort({ postCount: -1 })
|
|
138
138
|
.build();
|
|
139
139
|
|
|
140
|
-
const byKind = await runtime.
|
|
140
|
+
const byKind = await runtime.query(plan);
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
Import `acc` and expression helpers (`fn`) from `@
|
|
143
|
+
Import `acc` and expression helpers (`fn`) from `@prisma/orm-mongo/query-builder` when building computed pipeline stages (as `examples/mongo-demo/src/server.ts` does).
|
|
144
144
|
|
|
145
145
|
## Workflow — Query builder (`db.query`)
|
|
146
146
|
|
|
147
|
-
The concept: `db.query.from('<root>')` starts a typed aggregation-pipeline chain. Terminal methods produce a `MongoQueryPlan`;
|
|
147
|
+
The concept: `db.query.from('<root>')` starts a typed aggregation-pipeline chain. Terminal methods produce a `MongoQueryPlan`; run it through the runtime with `query(plan)` (an `AsyncIterableResult` of documents — `await` it for an array):
|
|
148
148
|
|
|
149
149
|
```typescript
|
|
150
150
|
// src/queries/analytics.ts
|
|
151
|
-
import { acc, fn } from '@
|
|
151
|
+
import { acc, fn } from '@prisma/orm-mongo/query-builder';
|
|
152
152
|
import { db } from '../prisma/db';
|
|
153
153
|
|
|
154
154
|
const runtime = await db.runtime();
|
|
@@ -161,7 +161,7 @@ const plan = db.query
|
|
|
161
161
|
.limit(10)
|
|
162
162
|
.project('title', 'authorId', 'createdAt')
|
|
163
163
|
.build();
|
|
164
|
-
const recent = await runtime.
|
|
164
|
+
const recent = await runtime.query(plan);
|
|
165
165
|
|
|
166
166
|
// Cross-collection join ($lookup).
|
|
167
167
|
const withAuthor = db.query
|
|
@@ -175,26 +175,26 @@ const withAuthor = db.query
|
|
|
175
175
|
.as('author'),
|
|
176
176
|
)
|
|
177
177
|
.build();
|
|
178
|
-
const rows = await runtime.
|
|
178
|
+
const rows = await runtime.query(withAuthor);
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
**Filters — `.match(...)`.** Callback form: `.match((f) => f.status.eq('active'))`. Filters AND-compose across chained `.match(...)` calls. Field accessors support property access (`f.email`), callable dot paths (`f('address.city').eq('NYC')`), and `f.rawPath('path')` for migration/backfill paths outside the current contract.
|
|
182
182
|
|
|
183
|
-
**Write terminals on the builder.** After `.from('users')` or `.from('users').match(...)`, use insert/update/delete terminals:
|
|
183
|
+
**Write terminals on the builder.** After `.from('users')` or `.from('users').match(...)`, use insert/update/delete terminals. Write plans run through `query(plan)` too (one result row carrying the driver's response — the inserted ids, or the document `findOneAndUpdate` returns); reach for `execute(plan)` only on an `update*` / `delete*` plan when all you want is the affected count (any other command kind throws `RUNTIME.MONGO_STATISTICS_UNSUPPORTED`):
|
|
184
184
|
|
|
185
185
|
```typescript
|
|
186
|
-
await runtime.
|
|
186
|
+
const inserted = await runtime.query(
|
|
187
187
|
db.query.from('users').insertOne({ name: 'Alice', email: 'a@e.com', bio: null }),
|
|
188
188
|
);
|
|
189
189
|
|
|
190
|
-
await runtime.execute(
|
|
190
|
+
const { affectedRows } = await runtime.execute(
|
|
191
191
|
db.query
|
|
192
192
|
.from('users')
|
|
193
193
|
.match((f) => f.name.eq('Alice'))
|
|
194
194
|
.updateMany((f) => [f.bio.set('filled')]),
|
|
195
195
|
);
|
|
196
196
|
|
|
197
|
-
await runtime.
|
|
197
|
+
const [updated] = await runtime.query(
|
|
198
198
|
db.query
|
|
199
199
|
.from('users')
|
|
200
200
|
.match((f) => f.email.eq('a@e.com'))
|
|
@@ -204,7 +204,7 @@ await runtime.execute(
|
|
|
204
204
|
|
|
205
205
|
Update callbacks return arrays of field operations (`.set`, `.inc`, `.push`, `.pull`, …). Pipeline-style updates use `f.stage.set(...)` inside an aggregation chain, then `.updateMany()` with no callback.
|
|
206
206
|
|
|
207
|
-
**Plans vs ORM.** The ORM's `.create` / `.update` / `.all` issue queries directly. Don't pass ORM collections to `runtime.execute` —
|
|
207
|
+
**Plans vs ORM.** The ORM's `.create` / `.update` / `.all` issue queries directly. Don't pass ORM collections to `runtime.query` / `runtime.execute` — those entry points are for `db.query` plans (and migration/runtime internals).
|
|
208
208
|
|
|
209
209
|
## Common Pitfalls (Mongo)
|
|
210
210
|
|
|
@@ -215,7 +215,7 @@ Update callbacks return arrays of field operations (`.set`, `.inc`, `.push`, `.p
|
|
|
215
215
|
5. **Expecting Postgres-style lambda `.where((u) => u.email.eq(...))` on ORM.** Prefer object equality `.where({ email: '...' })`; richer operators need `MongoFilterExpr` helpers (façade gap today).
|
|
216
216
|
6. **Expecting `db.transaction(...)`.** The Mongo façade does not expose it today. Multi-document atomicity requires MongoDB transactions on a replica set via the driver — not yet wrapped in the Prisma 8 façade. Route to *What Prisma 8 doesn't do yet* / `references/feedback.md` if the user needs this.
|
|
217
217
|
7. **Trying to use `db.sql`.** There is no `db.sql` on Mongo.
|
|
218
|
-
8. **Trying to `db.execute(plan)` directly
|
|
218
|
+
8. **Trying to `db.execute(plan)` directly, or reading documents with `execute`.** Run query-builder plans via `(await db.runtime()).query(plan)`. `execute(plan)` resolves statistics only and throws `RUNTIME.MONGO_STATISTICS_UNSUPPORTED` for a find or aggregate.
|
|
219
219
|
9. **Expecting ORM `.aggregate(...)` / `.groupBy(...)`.** Use `db.query.from(...).group(...).build()` instead.
|
|
220
220
|
|
|
221
221
|
## Reference Files
|
|
@@ -230,7 +230,7 @@ Update callbacks return arrays of field operations (`.set`, `.inc`, `.push`, `.p
|
|
|
230
230
|
- [ ] Used lowercased plural ORM roots (`db.orm.users`, not `db.orm.User`).
|
|
231
231
|
- [ ] Chose the right lane (ORM by default; `db.query` for shapes the ORM doesn't express).
|
|
232
232
|
- [ ] Used `.where({ ... }).first()` for single-row reads — not `.all()`.
|
|
233
|
-
- [ ]
|
|
233
|
+
- [ ] Ran query-builder plans via `(await db.runtime()).query(plan)`; used `execute(plan)` only for an affected count on a write.
|
|
234
234
|
- [ ] For aggregations, used `db.query.from(...).group(...)` rather than a non-existent ORM `.aggregate(...)`.
|
|
235
235
|
- [ ] Did NOT confabulate `db.transaction`, `db.sql`, or ORM `.aggregate(...)` — routed to *What Prisma 8 doesn't do yet* / `references/feedback.md` instead.
|
|
236
236
|
- [ ] Did NOT use the lower-level builder for something the ORM cleanly expresses.
|
|
@@ -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`.
|