@prisma/orm-mongo 8.0.0-rc.5 → 8.0.0-rc.5-dev.1
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 +11 -10
- package/skills/prisma-8/SKILL.md +84 -0
- package/skills/prisma-8/references/build.md +142 -0
- package/skills/prisma-8/references/contract.md +417 -0
- package/skills/prisma-8/references/debug.md +141 -0
- package/skills/prisma-8/references/feedback.md +251 -0
- package/skills/prisma-8/references/migration-review.md +224 -0
- package/skills/prisma-8/references/migrations.md +519 -0
- package/skills/prisma-8/references/queries-mongo.md +236 -0
- package/skills/prisma-8/references/queries-postgres.md +415 -0
- package/skills/prisma-8/references/queries.md +168 -0
- package/skills/prisma-8/references/quickstart.md +326 -0
- package/skills/prisma-8/references/runtime.md +344 -0
- package/skills/prisma-8/references/supabase.md +244 -0
- package/skills/prisma-8/references/upgrade-app.md +101 -0
- package/skills/prisma-8/references/upgrade-extension.md +105 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.10-to-0.11/instructions.md +56 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/instructions.md +381 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-closed-mongo-contracts.ts +202 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-domain-namespaced-contracts.ts +201 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/re-emit-postgres-public-default.ts +198 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.11-to-0.12/strip-migration-labels-hints.ts +340 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.12-to-0.13/instructions.md +339 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.12-to-0.13/re-emit-mti-variant-link-columns.ts +229 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/instructions.md +543 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts +290 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.13-to-0.14/uuid-preset-rename.ts +43 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.14-to-0.15/instructions.md +359 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.15-to-0.16/instructions.md +173 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.16-to-0.17/instructions.md +805 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.16-to-0.17/strip-sha256-hash-prefixes.ts +382 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.17-to-8.0.0-rc.1/instructions.md +72 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.7-to-0.8/instructions.md +8 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.8-to-0.9/instructions.md +36 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.8-to-0.9/strip-inline-contracts.ts +226 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.9-to-0.10/instructions.md +86 -0
- package/skills/prisma-8/upgrading/app/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts +360 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.1-to-8.0.0-rc.2/instructions.md +588 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.2-to-8.0.0-rc.3/instructions.md +5 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.3-to-8.0.0-rc.4/instructions.md +158 -0
- package/skills/prisma-8/upgrading/app/upgrades/8.0.0-rc.4-to-8.0.0-rc.5/instructions.md +42 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.10-to-0.11/instructions.md +276 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/instructions.md +738 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/migrate-contract-testing-imports.ts +97 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/regenerate-extension-public-baseline.ts +223 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.11-to-0.12/strip-migration-labels-hints.ts +340 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.12-to-0.13/instructions.md +266 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/instructions.md +522 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/migration-op-factories-to-methods.ts +290 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.13-to-0.14/uuid-preset-rename.ts +43 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.14-to-0.15/instructions.md +803 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.15-to-0.16/instructions.md +219 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.16-to-0.17/instructions.md +731 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.16-to-0.17/strip-sha256-hash-prefixes.ts +382 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.17-to-8.0.0-rc.1/instructions.md +194 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.7-to-0.8/instructions.md +8 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.8-to-0.9/instructions.md +57 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.8-to-0.9/strip-inline-contracts.ts +226 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.9-to-0.10/instructions.md +150 -0
- package/skills/prisma-8/upgrading/extension/upgrades/0.9-to-0.10/stamp-storage-types-kind.ts +360 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.1-to-8.0.0-rc.2/instructions.md +746 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.2-to-8.0.0-rc.3/instructions.md +5 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.3-to-8.0.0-rc.4/instructions.md +137 -0
- package/skills/prisma-8/upgrading/extension/upgrades/8.0.0-rc.4-to-8.0.0-rc.5/instructions.md +129 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Prisma Next — Queries (Mongo)
|
|
2
|
+
|
|
3
|
+
> Load this guide when `db.ts` imports from `@internal/mongo/runtime`.
|
|
4
|
+
|
|
5
|
+
Shared concepts (result consumption, script teardown, cross-target pitfalls, capability gaps) live in [`queries.md`](./queries.md).
|
|
6
|
+
|
|
7
|
+
## Key Concepts
|
|
8
|
+
|
|
9
|
+
**Mongo** (`mongo<Contract>(...)` from `@internal/mongo/runtime`):
|
|
10
|
+
|
|
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()`. Execute via `(await db.runtime()).execute(plan)`.
|
|
13
|
+
|
|
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
|
+
|
|
16
|
+
**Lane decision table:**
|
|
17
|
+
|
|
18
|
+
| Need | Choose | Why |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Standard CRUD with reference relations | **ORM (`db.orm.<root>`)** | Collection-shaped; object `.where({ ... })`; `.create` / `.update` / `.delete` / `.upsert`. |
|
|
21
|
+
| Eager-load a reference relation | **ORM `.include('<relation>')`** | Lowers to `$lookup`; composes with `.where` / `.select` / `.orderBy` / `.take`. |
|
|
22
|
+
| Polymorphic root (discriminated variants) | **ORM `.variant('<VariantName>')`** | Narrows to one variant and injects the discriminator filter. |
|
|
23
|
+
| Field-level Mongo updates (`$push`, `$inc`, dot-path `$set`) | **ORM `.update((f) => [f.field.inc(1)])`** | Field-accessor callback; plain-object `.update({ ... })` for whole-field replacement. |
|
|
24
|
+
| Aggregation pipeline (group, facet, `$lookup` with reshaping) | **Query builder (`db.query.from(...)`)** | Full pipeline surface; typed row shape through `.build()`. |
|
|
25
|
+
| Typed cross-collection join in a pipeline | **Query builder `.lookup((from) => from('users').on(...).as('author'))`** | `$lookup` with compile-time foreign-root checking. |
|
|
26
|
+
| Bulk writes with pipeline semantics | **Query builder write terminals** (`.insertOne`, `.updateMany`, `.findOneAndUpdate`, `.upsertOne`, …) | Filtered writes after `.match(...)`; plans execute through the runtime. |
|
|
27
|
+
|
|
28
|
+
## Workflow — ORM reads
|
|
29
|
+
|
|
30
|
+
The concept matches Postgres — `db.orm.<root>` returns a collection you compose method-by-method — but roots are **lowercased plurals** from the emitted contract (`users`, `posts`, not `User` / `Post`), and filters are usually **object equality**:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// src/queries/users.ts — adjust the relative import to match file depth.
|
|
34
|
+
import { db } from '../prisma/db';
|
|
35
|
+
|
|
36
|
+
// All users.
|
|
37
|
+
const users = await db.orm.users.all();
|
|
38
|
+
|
|
39
|
+
// Single row by equality filter.
|
|
40
|
+
const alice = await db.orm.users.where({ email: 'alice@example.com' }).first();
|
|
41
|
+
|
|
42
|
+
// Projection, sort, pagination — same chaining as Postgres.
|
|
43
|
+
const recent = await db.orm.posts
|
|
44
|
+
.select('title', 'authorId', 'createdAt')
|
|
45
|
+
.orderBy({ createdAt: -1 })
|
|
46
|
+
.take(10)
|
|
47
|
+
.all();
|
|
48
|
+
```
|
|
49
|
+
|
|
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
|
+
|
|
52
|
+
For operators the object form doesn't cover (`.in([...])`, range comparisons, nested logic), pass a `MongoFilterExpr` — today that means importing filter helpers from `@internal/mongo-query-ast/execution` (a façade-completeness gap; see *What Prisma Next doesn't do yet* in [`queries.md`](./queries.md)). Prefer the object form whenever equality suffices.
|
|
53
|
+
|
|
54
|
+
**Polymorphic roots.** When the contract declares variants on a model, narrow before querying:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
const articles = await db.orm.posts.variant('Article').all();
|
|
58
|
+
const tutorials = await db.orm.posts.variant('Tutorial').where({ authorId }).all();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Sorting and pagination.** `.orderBy({ field: 1 | -1 })` (Mongo sort directions). `.take(n)` maps to `$limit`; `.skip(n)` maps to `$skip`.
|
|
62
|
+
|
|
63
|
+
**`.first()` vs `.all()`.** `.first()` issues a limit-1 read; `.all()` returns every matching document. There is no `.first({ pk })` shorthand on Mongo — filter on `_id` explicitly: `.where({ _id: id }).first()`.
|
|
64
|
+
|
|
65
|
+
Mongo `.all()` returns the same `AsyncIterableResult` shape as Postgres — `await db.orm.users.all()` yields an array; see *Consuming the result* in [`queries.md`](./queries.md).
|
|
66
|
+
|
|
67
|
+
## Workflow — Eager-loading relations (`.include`)
|
|
68
|
+
|
|
69
|
+
Mongo reference relations eager-load through the same `.include('<relation>')` surface; the ORM lowers to `$lookup`:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
const posts = await db.orm.posts
|
|
73
|
+
.include('author')
|
|
74
|
+
.orderBy({ createdAt: -1 })
|
|
75
|
+
.all();
|
|
76
|
+
// → Array<{ title, authorId, createdAt, author: { name, email, ... } }>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Relation names match the contract's `@relation` field names. Nested includes follow the same chaining rules as the parent collection.
|
|
80
|
+
|
|
81
|
+
## Workflow — ORM writes
|
|
82
|
+
|
|
83
|
+
Mongo mutations require a preceding `.where(...)` filter (except `.create` / `.createAll`). Updates accept either a partial document or a field-accessor callback for Mongo operators:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Create — returns the row with server-assigned `_id`.
|
|
87
|
+
const user = await db.orm.users.create({
|
|
88
|
+
name: 'Alice',
|
|
89
|
+
email: 'alice@example.com',
|
|
90
|
+
bio: null,
|
|
91
|
+
address: null,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Update one — plain object replaces top-level fields.
|
|
95
|
+
await db.orm.users.where({ _id: user._id }).update({ bio: 'Writer' });
|
|
96
|
+
|
|
97
|
+
// Update one — field operations ($push, $inc, dot-path $set).
|
|
98
|
+
await db.orm.users
|
|
99
|
+
.where({ _id: user._id })
|
|
100
|
+
.update((u) => [u.tags.push('admin'), u.loginCount.inc(1)]);
|
|
101
|
+
|
|
102
|
+
// Update many / delete many — iterate or count.
|
|
103
|
+
const updated = await db.orm.users
|
|
104
|
+
.where({ bio: null })
|
|
105
|
+
.updateAll({ bio: 'filled' });
|
|
106
|
+
for await (const row of updated) { /* each modified doc */ }
|
|
107
|
+
|
|
108
|
+
await db.orm.users.where({ _id: user._id }).delete();
|
|
109
|
+
|
|
110
|
+
// Upsert — filter via .where(), split create vs update branches.
|
|
111
|
+
await db.orm.users.where({ email: 'alice@example.com' }).upsert({
|
|
112
|
+
create: { name: 'Alice', email: 'alice@example.com', bio: null, address: null },
|
|
113
|
+
update: { bio: 'Editor' },
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Count-only terminals.** `.createAndCount(...)`, `.updateAndCount(...)`, `.deleteAndCount()` return numbers without re-reading full documents — useful for bulk operations where you only need the modified count.
|
|
118
|
+
|
|
119
|
+
**Upsert + dot-path.** The upsert `update` callback cannot use dot-path field operations — use top-level field replacement in the upsert branch or a separate `.update((u) => [...])` call.
|
|
120
|
+
|
|
121
|
+
## Workflow — Aggregates
|
|
122
|
+
|
|
123
|
+
The Mongo ORM does not expose `.aggregate(...)` / `.groupBy(...)`. Express aggregations through **`db.query`** — the pipeline builder — with `.group(...)` and accumulator helpers:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { acc } from '@internal/mongo-query-builder';
|
|
127
|
+
|
|
128
|
+
const runtime = await db.runtime();
|
|
129
|
+
const plan = db.query
|
|
130
|
+
.from('posts')
|
|
131
|
+
.match((f) => f.authorId.eq(authorId))
|
|
132
|
+
.group((f) => ({
|
|
133
|
+
_id: f.kind,
|
|
134
|
+
postCount: acc.count(),
|
|
135
|
+
latest: acc.max(f.createdAt),
|
|
136
|
+
}))
|
|
137
|
+
.sort({ postCount: -1 })
|
|
138
|
+
.build();
|
|
139
|
+
|
|
140
|
+
const byKind = await runtime.execute(plan);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Import `acc` and expression helpers (`fn`) from `@internal/mongo-query-builder` when building computed pipeline stages.
|
|
144
|
+
|
|
145
|
+
## Workflow — Query builder (`db.query`)
|
|
146
|
+
|
|
147
|
+
The concept: `db.query.from('<root>')` starts a typed aggregation-pipeline chain. Terminal methods produce a `MongoQueryPlan`; execute it through the runtime:
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// src/queries/analytics.ts
|
|
151
|
+
import { acc, fn } from '@internal/mongo-query-builder';
|
|
152
|
+
import { db } from '../prisma/db';
|
|
153
|
+
|
|
154
|
+
const runtime = await db.runtime();
|
|
155
|
+
|
|
156
|
+
// Read pipeline — match, project, sort, limit.
|
|
157
|
+
const plan = db.query
|
|
158
|
+
.from('posts')
|
|
159
|
+
.match((f) => f.authorId.eq(authorId))
|
|
160
|
+
.sort({ createdAt: -1 })
|
|
161
|
+
.limit(10)
|
|
162
|
+
.project('title', 'authorId', 'createdAt')
|
|
163
|
+
.build();
|
|
164
|
+
const recent = await runtime.execute(plan);
|
|
165
|
+
|
|
166
|
+
// Cross-collection join ($lookup).
|
|
167
|
+
const withAuthor = db.query
|
|
168
|
+
.from('posts')
|
|
169
|
+
.lookup((from) =>
|
|
170
|
+
from('users')
|
|
171
|
+
.on((local, foreign) => ({
|
|
172
|
+
local: local.authorId,
|
|
173
|
+
foreign: foreign._id,
|
|
174
|
+
}))
|
|
175
|
+
.as('author'),
|
|
176
|
+
)
|
|
177
|
+
.build();
|
|
178
|
+
const rows = await runtime.execute(withAuthor);
|
|
179
|
+
```
|
|
180
|
+
|
|
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
|
+
|
|
183
|
+
**Write terminals on the builder.** After `.from('users')` or `.from('users').match(...)`, use insert/update/delete terminals:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
await runtime.execute(
|
|
187
|
+
db.query.from('users').insertOne({ name: 'Alice', email: 'a@e.com', bio: null }),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await runtime.execute(
|
|
191
|
+
db.query
|
|
192
|
+
.from('users')
|
|
193
|
+
.match((f) => f.name.eq('Alice'))
|
|
194
|
+
.updateMany((f) => [f.bio.set('filled')]),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
await runtime.execute(
|
|
198
|
+
db.query
|
|
199
|
+
.from('users')
|
|
200
|
+
.match((f) => f.email.eq('a@e.com'))
|
|
201
|
+
.findOneAndUpdate((f) => [f.bio.set('updated')], { returnDocument: 'after' }),
|
|
202
|
+
);
|
|
203
|
+
```
|
|
204
|
+
|
|
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
|
+
|
|
207
|
+
**Plans vs ORM.** The ORM's `.create` / `.update` / `.all` issue queries directly. Don't pass ORM collections to `runtime.execute` — that entry point is for `db.query` plans (and migration/runtime internals).
|
|
208
|
+
|
|
209
|
+
## Common Pitfalls (Mongo)
|
|
210
|
+
|
|
211
|
+
1. **Reaching for the lower-level lane when the ORM would have done.** Default to the ORM; drop to `db.query` only for shapes the ORM can't express.
|
|
212
|
+
2. **Using `.all()` when you wanted one row.** Use `.where({ ... }).first()` — not `.all()`.
|
|
213
|
+
3. **Calling `.update()` / `.delete()` without `.where()`.** Mutations other than `.create` / `.createAll` require a filter — the compiler enforces this at the type level where possible.
|
|
214
|
+
4. **Using PascalCase model names on ORM.** Roots are lowercased plurals from the contract (`db.orm.users`, not `db.orm.User`).
|
|
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
|
+
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 Next façade. Route to *What Prisma Next doesn't do yet* / `references/feedback.md` if the user needs this.
|
|
217
|
+
7. **Trying to use `db.sql`.** There is no `db.sql` on Mongo.
|
|
218
|
+
8. **Trying to `db.execute(plan)` directly.** Execute query-builder plans via `(await db.runtime()).execute(plan)`.
|
|
219
|
+
9. **Expecting ORM `.aggregate(...)` / `.groupBy(...)`.** Use `db.query.from(...).group(...).build()` instead.
|
|
220
|
+
|
|
221
|
+
## Reference Files
|
|
222
|
+
|
|
223
|
+
- Example queries under [`examples/mongo-demo/src/server.ts`](examples/mongo-demo/src/server.ts) — ORM reads, `.include`, `.variant`, and pipeline DSL via `db.query`.
|
|
224
|
+
- Integration tests under `examples/mongo-demo/test/` (`blog.test.ts`, `crud-lifecycle.test.ts`, `query-builder-writes.test.ts`).
|
|
225
|
+
- Query builder README under `packages/2-mongo-family/5-query-builders/query-builder/README.md`.
|
|
226
|
+
- ORM collection surface under `packages/2-mongo-family/5-query-builders/orm/src/collection.ts`.
|
|
227
|
+
|
|
228
|
+
## Checklist
|
|
229
|
+
|
|
230
|
+
- [ ] Used lowercased plural ORM roots (`db.orm.users`, not `db.orm.User`).
|
|
231
|
+
- [ ] Chose the right lane (ORM by default; `db.query` for shapes the ORM doesn't express).
|
|
232
|
+
- [ ] Used `.where({ ... }).first()` for single-row reads — not `.all()`.
|
|
233
|
+
- [ ] Executed query-builder plans via `(await db.runtime()).execute(plan)`.
|
|
234
|
+
- [ ] For aggregations, used `db.query.from(...).group(...)` rather than a non-existent ORM `.aggregate(...)`.
|
|
235
|
+
- [ ] Did NOT confabulate `db.transaction`, `db.sql`, or ORM `.aggregate(...)` — routed to *What Prisma Next doesn't do yet* / `references/feedback.md` instead.
|
|
236
|
+
- [ ] Did NOT use the lower-level builder for something the ORM cleanly expresses.
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# Prisma Next — Queries (Postgres)
|
|
2
|
+
|
|
3
|
+
> Load this guide when `db.ts` imports from `@internal/postgres/runtime`.
|
|
4
|
+
|
|
5
|
+
Shared concepts (result consumption, script teardown, cross-target pitfalls, capability gaps) live in [`queries.md`](./queries.md).
|
|
6
|
+
|
|
7
|
+
## Key Concepts
|
|
8
|
+
|
|
9
|
+
**Postgres** (`postgres<Contract>(...)` from `@internal/postgres/runtime`):
|
|
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* executed via `db.runtime().execute(plan)`. Use when the ORM is too high-level — explicit `JOIN`, computed projections, set operations, window functions.
|
|
13
|
+
|
|
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
|
+
|
|
16
|
+
**Lane decision table:**
|
|
17
|
+
|
|
18
|
+
| Need | Choose | Why |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Standard CRUD with relations | **ORM (`db.orm.<Model>`)** | Highest ergonomics; fully typed; model-shaped. |
|
|
21
|
+
| Eager-load related records | **ORM `.include(...)`** | Composes with `.where` / `.select` / `.orderBy` / `.take` per branch. |
|
|
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. |
|
|
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`, `cipherstash`). |
|
|
27
|
+
|
|
28
|
+
## Workflow — ORM reads
|
|
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()` / `.aggregate(...)`) issues the query. Predicates are lambdas over a field proxy: `u.field.<op>(value)`.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// src/queries/users.ts — one directory deep under src/, so the import is '../prisma/db'
|
|
34
|
+
import { db } from '../prisma/db';
|
|
35
|
+
|
|
36
|
+
// Find one record by primary key shorthand.
|
|
37
|
+
const user = await db.orm.User.first({ id: userId });
|
|
38
|
+
// Returns the full row or `null`.
|
|
39
|
+
|
|
40
|
+
// Find one matching a predicate.
|
|
41
|
+
const alice = await db.orm.User
|
|
42
|
+
.where((u) => u.email.eq('alice@example.com'))
|
|
43
|
+
.first();
|
|
44
|
+
|
|
45
|
+
// Find many with projection, sort, and limit.
|
|
46
|
+
const recentUsers = await db.orm.User
|
|
47
|
+
.select('id', 'email', 'createdAt')
|
|
48
|
+
.orderBy((u) => u.createdAt.desc())
|
|
49
|
+
.take(10)
|
|
50
|
+
.all();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Predicates** (`.where(...)`) come in two forms:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// Lambda form — full expression power.
|
|
57
|
+
db.orm.User.where((u) => u.email.eq('alice@example.com'));
|
|
58
|
+
|
|
59
|
+
// Shorthand object form — equality on the named fields.
|
|
60
|
+
db.orm.User.where({ kind: 'admin' });
|
|
61
|
+
```
|
|
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(...)`, `cipherstash`'s `.cipherstashEq(...)` / `.cipherstashGt(...)` / …).
|
|
64
|
+
|
|
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
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
// Chained .where() — each clause AND-composes with the previous one.
|
|
69
|
+
await db.orm.Sale
|
|
70
|
+
.where((s) => s.day.gte(start))
|
|
71
|
+
.where((s) => s.day.lte(end))
|
|
72
|
+
.all();
|
|
73
|
+
|
|
74
|
+
// Equivalent with an explicit `and(...)` inside one clause.
|
|
75
|
+
import { and } from '@internal/sql-orm-client'; // façade re-export pending — see *What PN doesn't do yet* in queries.md
|
|
76
|
+
await db.orm.Sale
|
|
77
|
+
.where((s) => and(s.day.gte(start), s.day.lte(end)))
|
|
78
|
+
.all();
|
|
79
|
+
```
|
|
80
|
+
|
|
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
|
+
|
|
83
|
+
**Combinators** (`and`, `or`, `not`) compose predicates, and **relation predicates** (`.some(...)`, `.none(...)`, `.every(...)`) recurse into a relation. These currently come from the internal `@internal/sql-orm-client` package — see *What Prisma Next doesn't do yet* in [`queries.md`](./queries.md):
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { and, or, not } from '@internal/sql-orm-client';
|
|
87
|
+
|
|
88
|
+
await db.orm.User
|
|
89
|
+
.where((u) =>
|
|
90
|
+
and(
|
|
91
|
+
or(u.kind.eq('admin'), u.email.ilike('%@example.com')),
|
|
92
|
+
not(u.posts.none((p) => p.title.ilike('%draft%'))),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
.all();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Sorting and pagination.** `.orderBy(...)` accepts a single lambda or an array of lambdas (each calling `.asc()` / `.desc()` on a field). `.take(n)` limits; `.skip(n)` offsets.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
await db.orm.Post
|
|
102
|
+
.where((p) => p.authorId.eq(userId))
|
|
103
|
+
.orderBy([(p) => p.createdAt.desc(), (p) => p.id.desc()])
|
|
104
|
+
.take(20)
|
|
105
|
+
.all();
|
|
106
|
+
```
|
|
107
|
+
|
|
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
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const page1 = await db.orm.Post
|
|
112
|
+
.orderBy((p) => p.createdAt.desc())
|
|
113
|
+
.take(20)
|
|
114
|
+
.all();
|
|
115
|
+
|
|
116
|
+
const last = page1[page1.length - 1]!;
|
|
117
|
+
const page2 = await db.orm.Post
|
|
118
|
+
.orderBy((p) => p.createdAt.desc())
|
|
119
|
+
.cursor({ createdAt: last.createdAt })
|
|
120
|
+
.take(20)
|
|
121
|
+
.all();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Cursor keys must match fields in the active `orderBy`. For a composite `orderBy`, pass a value for each ordering column — a partial cursor seeks only on the columns you supply, which gives an incomplete keyset. An empty cursor object is a no-op: you get the unfiltered first page back.
|
|
125
|
+
|
|
126
|
+
**`.first()` vs `.first({ pk })` vs `.all()`.** Use `.first()` for a single row (issues a `LIMIT 1`); use `.first({ pk })` for primary-key lookups; reserve `.all()` for the genuine many case (no implicit `LIMIT`).
|
|
127
|
+
|
|
128
|
+
## Workflow — Eager-loading relations (`.include`)
|
|
129
|
+
|
|
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` / `.take` on it just like the parent.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
await db.orm.User
|
|
134
|
+
.select('id', 'email')
|
|
135
|
+
.include('posts', (post) =>
|
|
136
|
+
post
|
|
137
|
+
.select('id', 'title', 'createdAt')
|
|
138
|
+
.orderBy((p) => p.createdAt.desc())
|
|
139
|
+
.take(5),
|
|
140
|
+
)
|
|
141
|
+
.take(10)
|
|
142
|
+
.all();
|
|
143
|
+
// → Array<{ id, email, posts: Array<{ id, title, createdAt }> }>
|
|
144
|
+
```
|
|
145
|
+
|
|
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
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
await db.orm.User.include('posts', (posts) => posts.count()).all();
|
|
150
|
+
// → Array<{ ...user, posts: number }> — a parent with no posts reads 0
|
|
151
|
+
|
|
152
|
+
await db.orm.User.include('posts', (posts) => posts.sum('views')).all();
|
|
153
|
+
// → Array<{ ...user, posts: number | null }>
|
|
154
|
+
|
|
155
|
+
await db.orm.User.include('posts', (posts) => posts.avg('views')).all();
|
|
156
|
+
// → Array<{ ...user, posts: number | null }>
|
|
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();
|
|
160
|
+
// → Array<{ ...user, posts: number | null }>
|
|
161
|
+
|
|
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();
|
|
164
|
+
// → Array<{ ...user, posts: bigint | null }>
|
|
165
|
+
|
|
166
|
+
// Several sub-views of one relation at once:
|
|
167
|
+
await db.orm.User.include('posts', (posts) =>
|
|
168
|
+
posts.combine({ recent: posts.take(3), total: posts.count() }),
|
|
169
|
+
).all();
|
|
170
|
+
// → Array<{ ...user, posts: { recent: Post[]; total: number } }>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A reducer's result type is the one the target declares for that aggregate — the same types *Workflow — Aggregates* tabulates below, including the `null` a field-taking reducer answers for a parent with no related rows. The reducer set is read from the contract, so an operation a target or extension contributes shows up as a reducer under its own name, with no client change.
|
|
174
|
+
|
|
175
|
+
Nested `1:N → 1:N` includes (e.g. `User → posts → comments`) require the contract to advertise the `lateral` + `jsonAgg` capabilities for the active target. The Postgres adapter advertises both by default, so most apps get this for free; if the type system rejects a nested include with a *missing capability* error, route to `references/contract.md` to add the required capability declarations and use `references/queries.md` for query-shape guidance.
|
|
176
|
+
|
|
177
|
+
## Workflow — ORM writes
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Create — returns the inserted row.
|
|
181
|
+
const user = await db.orm.User.create({ id, email, displayName, kind, createdAt });
|
|
182
|
+
|
|
183
|
+
// Create with selected return — narrows the return shape.
|
|
184
|
+
const summary = await db.orm.User
|
|
185
|
+
.select('id', 'email', 'kind')
|
|
186
|
+
.create({ id, email, displayName, kind, createdAt });
|
|
187
|
+
|
|
188
|
+
// Update by predicate.
|
|
189
|
+
await db.orm.User.where({ id }).update({ email: newEmail });
|
|
190
|
+
|
|
191
|
+
// Update with selected return.
|
|
192
|
+
await db.orm.User
|
|
193
|
+
.where({ id })
|
|
194
|
+
.select('id', 'email', 'kind')
|
|
195
|
+
.update({ email: newEmail });
|
|
196
|
+
|
|
197
|
+
// Delete by predicate.
|
|
198
|
+
await db.orm.User.where({ id }).delete();
|
|
199
|
+
|
|
200
|
+
// Upsert — typed by the create branch's shape.
|
|
201
|
+
await db.orm.User
|
|
202
|
+
.select('id', 'email', 'kind', 'createdAt')
|
|
203
|
+
.upsert({
|
|
204
|
+
create: { id, email, displayName, kind, createdAt: new Date() },
|
|
205
|
+
update: { email, displayName, kind },
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The ORM returns inserted / updated rows by default. The `.returning(...)` selector lives on the SQL builder (next section), where you build a plan and execute it explicitly.
|
|
210
|
+
|
|
211
|
+
## Workflow — Aggregates
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const totals = await db.orm.User.aggregate((aggregate) => ({
|
|
215
|
+
totalUsers: aggregate.count(),
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
const adminTotals = await db.orm.User
|
|
219
|
+
.where({ kind: 'admin' })
|
|
220
|
+
.aggregate((aggregate) => ({
|
|
221
|
+
adminUsers: aggregate.count(),
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// Group-by + aggregate.
|
|
225
|
+
const byKind = await db.orm.User
|
|
226
|
+
.groupBy('kind')
|
|
227
|
+
.having((having) => having.count().gte(minUsers))
|
|
228
|
+
.aggregate((aggregate) => ({
|
|
229
|
+
totalUsers: aggregate.count(),
|
|
230
|
+
}));
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`aggregate` exposes `.count()`, `.sum(field)`, `.avg(field)`, `.min(field)`, `.max(field)`, and the lossless `.countBigInt()`, `.sumBigInt(field)`, `.avgDecimal(field)`. Project the aggregates into named result keys; the result type narrows accordingly.
|
|
234
|
+
|
|
235
|
+
**The bare operations answer as JS numbers. The suffixed ones answer losslessly.** An aggregate's type is the one its target declares and its nullability matches SQL semantics, both read from the contract. On PostgreSQL:
|
|
236
|
+
|
|
237
|
+
| Aggregate | Type | Empty result |
|
|
238
|
+
|---|---|---|
|
|
239
|
+
| `count()` | `number` | `0` |
|
|
240
|
+
| `countBigInt()` | `bigint` | `0n` |
|
|
241
|
+
| `sum(field)` over `int2` / `int4` / `int8` / `BigIntNumber` | `number \| null` | `null` (SQL `SUM` over zero rows is `NULL`) |
|
|
242
|
+
| `sumBigInt(field)` over any integer | `bigint \| null` | `null` |
|
|
243
|
+
| `sum(field)` over `float4` / `float8` | `number \| null` | `null` |
|
|
244
|
+
| `sum(field)` over `numeric` | decimal `string \| null` | `null` |
|
|
245
|
+
| `sum(field)` over `UnboundedInt` | `bigint \| null` — that column's own family | `null` |
|
|
246
|
+
| `avg(field)` over any integer | `number \| null` | `null` |
|
|
247
|
+
| `avgDecimal(field)` over any integer or `numeric` | decimal `string \| null` | `null` |
|
|
248
|
+
| `avg(field)` over a float | `number \| null` | `null` |
|
|
249
|
+
| `min(field)` / `max(field)` | the column's own type `\| null` — `text` where the column is `varchar` | `null` |
|
|
250
|
+
|
|
251
|
+
**`count()`, and `sum` over an integer column, throw outside ±(2^53 − 1) — they never round.** Those two are the results a guarded integer codec produces. The rows above the guard does not reach: `sum` over `numeric`, `UnboundedInt`, or a float column, and every `avg`, which is a fraction already. The error is `RUNTIME.DECODE_FAILED` (`… value must be an integer within the safe integer range, got …`), and it fires on the include path too. That is the trade the defaults make: a `number` you can compare, serialise, and do arithmetic with, and a loud failure instead of a quietly wrong total. Switch that one call to `sumBigInt` / `countBigInt` where the magnitude is real.
|
|
252
|
+
|
|
253
|
+
**Float and Decimal columns keep their own family.** `sum` over `numeric` is still a decimal string, and `sum` over `float8` still a `number` — the defaults policy is about integers, and a column whose author chose `numeric` already chose its representation.
|
|
254
|
+
|
|
255
|
+
**SQLite states the same policy.** `count`, `sum` over integers, and `avg` all read as `number`; `countBigInt` and `sumBigInt` are there too. There is no `avgDecimal` — SQLite has no exact decimal type, so the method does not exist on a SQLite contract.
|
|
256
|
+
|
|
257
|
+
**The ORM's `having(...)` stays a `number`.** Its comparand is typed `number` outright, whatever result type the aggregate carries, so `having.count().gte(minUsers)` takes a plain number — the value is inlined as a SQL literal and compared inside the database. Only the aggregate's *result*, the value that reaches your code, carries its target's type. The lossless variants are projection-only and have no HAVING method at all.
|
|
258
|
+
|
|
259
|
+
**The SQL builder types its comparands differently.** `fns.gt(a, b)` types both operands from one codec, so a literal compared against an aggregate follows that aggregate's result codec — `fns.gt(fns.count(), 1)`, not `1n`. That applies in `having(...)`, in `where(...)`, and inside any larger expression.
|
|
260
|
+
|
|
261
|
+
Nullability isn't a typing bug — it's faithful to what the database returns. Coalesce client-side when you want zero-fill:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const revenue = await db.orm.Sale
|
|
265
|
+
.where((s) => s.day.gte(start))
|
|
266
|
+
.aggregate((a) => ({ total: a.sum('amount') }));
|
|
267
|
+
// revenue.total: number | null
|
|
268
|
+
|
|
269
|
+
const safe = revenue.total ?? 0; // ← apply at the consumption site, not in the aggregate spec.
|
|
270
|
+
```
|
|
271
|
+
|
|
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
|
+
|
|
274
|
+
## Workflow — SQL builder (`db.sql.<table>`)
|
|
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 execute it through the runtime with `db.runtime().execute(plan)`. 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
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// src/queries/posts.ts — adjust the relative import to match file depth.
|
|
280
|
+
import { db } from '../prisma/db';
|
|
281
|
+
|
|
282
|
+
// Select with predicate and limit.
|
|
283
|
+
const plan = db.sql.post
|
|
284
|
+
.select('id', 'title', 'userId', 'createdAt')
|
|
285
|
+
.where((f, fns) => fns.eq(f.userId, userId))
|
|
286
|
+
.limit(limit)
|
|
287
|
+
.build();
|
|
288
|
+
|
|
289
|
+
const rows = await db.runtime().execute(plan);
|
|
290
|
+
```
|
|
291
|
+
|
|
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.).
|
|
293
|
+
|
|
294
|
+
### `INSERT` / `UPDATE` / `DELETE` with `RETURNING`
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// Insert and return selected columns.
|
|
298
|
+
const plan = db.sql.user
|
|
299
|
+
.insert({ email })
|
|
300
|
+
.returning('id', 'email')
|
|
301
|
+
.build();
|
|
302
|
+
const [row] = await db.runtime().execute(plan);
|
|
303
|
+
|
|
304
|
+
// Update with predicate and returning.
|
|
305
|
+
const updatePlan = db.sql.user
|
|
306
|
+
.update({ email: newEmail })
|
|
307
|
+
.where((f, fns) => fns.eq(f.id, userId))
|
|
308
|
+
.returning('id', 'email')
|
|
309
|
+
.build();
|
|
310
|
+
const rows = await db.runtime().execute(updatePlan);
|
|
311
|
+
|
|
312
|
+
// Delete with predicate.
|
|
313
|
+
const deletePlan = db.sql.user
|
|
314
|
+
.delete()
|
|
315
|
+
.where((f, fns) => fns.eq(f.id, userId))
|
|
316
|
+
.build();
|
|
317
|
+
await db.runtime().execute(deletePlan);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`.returning(...)` requires the target adapter to advertise the `returning` capability. The Postgres adapter advertises it by default.
|
|
321
|
+
|
|
322
|
+
### Computed projections and joins
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Project a computed expression alongside model fields.
|
|
326
|
+
const plan = db.sql.cafe
|
|
327
|
+
.select('id', 'name')
|
|
328
|
+
.select('meters', (f, fns) => fns.distanceSphere(f.location, point))
|
|
329
|
+
.orderBy((f, fns) => fns.distanceSphere(f.location, point), { direction: 'asc' })
|
|
330
|
+
.orderBy((f) => f.id, { direction: 'asc' })
|
|
331
|
+
.limit(limit)
|
|
332
|
+
.build();
|
|
333
|
+
const rows = await db.runtime().execute(plan);
|
|
334
|
+
|
|
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))
|
|
338
|
+
// ...
|
|
339
|
+
.build();
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Workflow — Transactions
|
|
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)` executes a SQL-builder plan within it. The transaction commits on the callback's successful return and rolls back on any thrown error.
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
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' });
|
|
350
|
+
|
|
351
|
+
// SQL-builder plan inside the transaction.
|
|
352
|
+
const plan = tx.sql.post.update({ status: 'archived' })
|
|
353
|
+
.where((f, fns) => fns.lt(f.createdAt, cutoff))
|
|
354
|
+
.build();
|
|
355
|
+
await tx.execute(plan);
|
|
356
|
+
|
|
357
|
+
// If anything throws, all three operations roll back.
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The callback's return value passes through `db.transaction(...)`. Capture inserted ids out of the callback and use them downstream after commit.
|
|
362
|
+
|
|
363
|
+
## Namespace-aware accessors
|
|
364
|
+
|
|
365
|
+
When the contract declares multiple namespaces, both `db.sql` and `db.orm` expose a namespace coordinate alongside the flat bare-name surface:
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
// db.sql.<namespace>.<table>
|
|
369
|
+
const plan = db.sql.public.users.select('id', 'email').build();
|
|
370
|
+
const authPlan = db.sql.auth.users.select('id', 'token').build();
|
|
371
|
+
await db.runtime().execute(plan);
|
|
372
|
+
|
|
373
|
+
// db.orm.<namespace>.<Model>
|
|
374
|
+
const user = await db.orm.public.User.create({ id: 1, email: 'a@x.io' });
|
|
375
|
+
const authUser = await db.orm.auth.User.create({ id: 2, token: 'tok' });
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The flat `db.sql.users` / `db.orm.User` form still works when bare names are unique across all namespaces. When the same bare name appears in more than one namespace, use the coordinate form — both the type system and the runtime require it to resolve to the right table.
|
|
379
|
+
|
|
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
|
+
|
|
382
|
+
## Common Pitfalls (Postgres)
|
|
383
|
+
|
|
384
|
+
1. **Reaching for the lower-level lane when the ORM would have done.** The ORM covers most CRUD shapes; drop to `db.sql` only for shapes the ORM can't express. Default to the ORM.
|
|
385
|
+
2. **Using `.all()` when you wanted one row.** `.all()` issues no implicit limit. Use `.first()` or `.first({ pk })`.
|
|
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
|
+
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 a Postgres façade subpath.** The combinators currently live in `@internal/sql-orm-client` — an internal package. See *What Prisma Next doesn't do yet* in [`queries.md`](./queries.md).
|
|
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.** Plans execute through the runtime: `db.runtime().execute(plan)`. Inside a transaction, use `tx.execute(plan)`.
|
|
391
|
+
8. **Setting `capabilities: { lateral: true }` in `prisma.config.ts`.** `defineConfig` 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 today. Raw SQL does: the client's raw lane, ``db.raw.sql`…` ``. See *What Prisma Next doesn't do yet* in [`queries.md`](./queries.md) for all three.
|
|
393
|
+
10. **Mixing the ORM mutation return with `runtime.execute(plan)`.** ORM terminals issue the query themselves and return rows. `runtime.execute` is for SQL-builder plans.
|
|
394
|
+
11. **Top-N grouped queries written as `groupBy(...).aggregate(...).sort().slice()` in JS.** That's a fallback because the grouped collection doesn't expose `.orderBy(...)` / `.take(...)`. Fine at small cardinalities; for large grouped result sets, drop to `db.sql.<table>`.
|
|
395
|
+
|
|
396
|
+
## Reference Files
|
|
397
|
+
|
|
398
|
+
- Example queries under [`examples/prisma-8-demo/src/orm-client/`](examples/prisma-8-demo/src/orm-client/) and [`examples/prisma-8-demo/src/queries/`](examples/prisma-8-demo/src/queries/) — canonical ORM and SQL-builder shapes.
|
|
399
|
+
- ORM client source under `packages/3-extensions/sql-orm-client/src/`.
|
|
400
|
+
- SQL builder source under `packages/2-sql/4-lanes/sql-builder/src/`.
|
|
401
|
+
|
|
402
|
+
## Checklist
|
|
403
|
+
|
|
404
|
+
- [ ] Chose the right lane (ORM by default; `db.sql` for shapes the ORM doesn't express).
|
|
405
|
+
- [ ] Used `.first()` / `.first({ pk })` for single-row reads — not `.all()`.
|
|
406
|
+
- [ ] Coalesced `sum` / `avg` / `min` / `max` results at the consumption site when zero-fill is desired, with a zero of the aggregate's own type — did NOT coalesce `count()`, which is `number` and never null.
|
|
407
|
+
- [ ] Reached for `countBigInt` / `sumBigInt` / `avgDecimal` where the value can outgrow a JS number or the exact decimal matters — `count()` and `sum` over an integer column throw `RUNTIME.DECODE_FAILED` outside ±(2^53 − 1) rather than rounding, and `avg` rounds as any double does.
|
|
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
|
+
- [ ] Expressed ranges as chained `.where(...)` clauses or a single `and(...)` clause — did NOT reach for a non-existent `.between(...)` operator.
|
|
410
|
+
- [ ] For cursor pagination, used `.orderBy(...).cursor({ field: lastValue }).take(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 the (currently internal) `@internal/sql-orm-client` and noted the façade gap to the user.
|
|
412
|
+
- [ ] Executed SQL-builder plans via `db.runtime().execute(plan)` (or `tx.execute(plan)` inside a transaction).
|
|
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 `defineConfig`, or a `db.sql.from(tables.user)` API — routed to *What Prisma Next doesn't do yet* / `references/feedback.md` instead. Raw SQL is spelled `db.raw.sql`, not `db.sql.raw`.
|