Skip to content

Commit 50e24b1

Browse files
ankur-archclaude
andcommitted
Validate guide against prisma-next 0.16.0; fix adoption flow, ObjectId filters, and raw-command mapping
Every command and snippet now runs against a live v6 database on mongodb-memory-server (mongod 8.0.4 replica set) migrated to @prisma-next/mongo@0.16.0. Changes that came out of that run: - Step 4: db init refuses a populated v6 database (validator adds are classed destructive), and migration plans start from an empty baseline without a ref, so the adoption path is db update --advance-ref db, with plan/migrate/verify for every change after that. - 3c: the typed pipeline builder passes ObjectId values through as strings, so match on an ObjectId field silently returns nothing; grouped example no longer filters by authorId and a warning shows the working rawCommand + ObjectId pattern. - 3e/quick reference: $runCommandRaw maps to sharing one MongoClient with the mongo() binding (mongoClient + dbName), shown end to end; collection-scoped escapes point at db.query.rawCommand. - Checklist: restore same-database and MongoDB 8.0+ items; rehearsal commands now match the adoption flow. - Step 2: note that every discriminator value in existing data needs a declared variant with at least one field. - Align snippets with the actual init scaffold (contract.d import, non-null env assertions), storage-value enum row in common mistakes, relative docs links, em-dash cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3335424 commit 50e24b1

1 file changed

Lines changed: 78 additions & 37 deletions

File tree

  • apps/docs/content/docs/guides/next/upgrade-prisma-orm

apps/docs/content/docs/guides/next/upgrade-prisma-orm/mongodb.mdx

Lines changed: 78 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ metaTitle: How to migrate a MongoDB project from Prisma v6 to Prisma Next
66
metaDescription: Step-by-step guide to migrating a Prisma ORM v6 MongoDB project to Prisma Next, covering setup, contracts, client calls, migrations, and production cutover.
77
---
88

9-
This guide is for developers moving a Prisma ORM v6 MongoDB project to **Prisma Next**. Prisma 7 has **no MongoDB connector**, so Prisma Next is the successor path. This is a **code and workflow migration against the same database** your data never moves.
9+
This guide is for developers moving a Prisma ORM v6 MongoDB project to **Prisma Next**. Prisma 7 has **no MongoDB connector**, so Prisma Next is the successor path. This is a **code and workflow migration against the same database**: your data never moves.
1010

1111
:::tip[Working with an AI coding agent?]
1212

@@ -24,7 +24,7 @@ Install the companion [prisma-mongodb-upgrade](https://github.com/prisma/skills/
2424

2525
:::info
2626

27-
Prisma Next MongoDB is Early Access (GA planned after Postgres), so details can shift between releases — check anything below against the `@prisma-next/*` version you install.
27+
Prisma Next MongoDB is Early Access (GA planned after Postgres) and evolves quickly, so check anything below against the `@prisma-next/*` version you install. Every command and code example in this guide was validated against `@prisma-next/mongo@0.16.0`.
2828

2929
:::
3030

@@ -46,12 +46,13 @@ npx prisma-next init --yes --target mongodb --authoring psl
4646

4747
```typescript tab="After"
4848
// prisma-next.config.ts
49+
import 'dotenv/config';
4950
import { defineConfig } from '@prisma-next/mongo/config';
5051

5152
export default defineConfig({
5253
contract: './src/prisma/contract.prisma',
5354
db: {
54-
connection: process.env['DATABASE_URL'],
55+
connection: process.env['DATABASE_URL']!,
5556
},
5657
});
5758
```
@@ -77,12 +78,12 @@ The way to instantiate the Prisma client now is from the emitted contract:
7778
```typescript tab="After"
7879
// src/prisma/db.ts
7980
import mongo from '@prisma-next/mongo/runtime';
80-
import type { Contract } from './contract';
81+
import type { Contract } from './contract.d';
8182
import contractJson from './contract.json' with { type: 'json' };
8283

8384
const db = mongo<Contract>({
8485
contractJson,
85-
url: process.env['DATABASE_URL'],
86+
url: process.env['DATABASE_URL']!,
8687
dbName: 'my-app-db',
8788
});
8889

@@ -98,7 +99,7 @@ const prisma = new PrismaClient();
9899
export { prisma };
99100
```
100101

101-
## 2. Port the Schema to a Contract
102+
## 2. Port the schema to a contract
102103

103104
:::note
104105

@@ -122,7 +123,7 @@ You can use the [prisma-mongodb-upgrade](https://github.com/prisma/skills/tree/m
122123
| Id field | `id String @id @default(auto()) @map("_id") @db.ObjectId` | `id ObjectId @id @map("_id")` |
123124
| Embedded document | `type Address { ... }` | `type Address { ... }` (unchanged) |
124125
| Index | `@@index(...)`, synced by `db push` | `@@index(...)` / `@@unique(...)`, applied by migrations |
125-
| Polymorphism | not supported | `@@discriminator(field)` on the base + `@@base(Base, "value")` on each variant. <br />See the [Base Model and Variants](https://www.prisma.io/docs/orm/next/contract-authoring/psl-syntax#base-models-and-variants) |
126+
| Polymorphism | not supported | `@@discriminator(field)` on the base + `@@base(Base, "value")` on each variant. <br />See [Base models and variants](/orm/next/contract-authoring/psl-syntax#base-models-and-variants) |
126127

127128
A fuller contract putting together enum, embedded type, relation, and polymorphism:
128129

@@ -168,6 +169,8 @@ model Article {
168169
}
169170
```
170171

172+
When you use `@@discriminator`, declare a variant for every value the field takes in your existing data: the generated validator only accepts declared values, so writes to documents with an undeclared value fail once the validators are live. A variant model must declare at least one field (an optional one is enough).
173+
171174
## 3. Port client calls
172175

173176
Names look similar but parity does not hold. This section breaks down each category of operation.
@@ -221,6 +224,7 @@ await prisma.user.delete({ where: { id } });
221224
| `db.orm.User` | `db.orm.users` | address by storage name, not model name |
222225
| `.where({ email: { equals: 'x' } })` | `.where({ email: 'x' })` | `.where(...)` takes a plain equality object |
223226
| `.update({ ... })` with no `.where(...)` | `.where(...).update({ ... })` | every mutation except `create` needs a `.where(...)` first |
227+
| `role: 'Author'` | `role: 'author'` | enums read and write their storage value, not the key name |
224228

225229
### 3b. Relations and polymorphism
226230

@@ -241,51 +245,68 @@ import { prisma } from './db';
241245
const withAuthor = await prisma.post.findMany({ include: { author: true } });
242246

243247
// Query one variant of a polymorphic collection
244-
const articles = await prisma.post.findMany({ where: { type: 'Article' } });
248+
const articles = await prisma.post.findMany({ where: { kind: 'article' } });
245249
```
246250

247251
### 3c. Aggregations
248252

249-
v6's `groupBy` / `aggregate` become a chain of steps. `acc` provides the group totals (`acc.count()`, `acc.max(...)`, etc.) — the equivalent of v6's `_count` / `_sum` / `_max`.
253+
v6's `groupBy` / `aggregate` become a typed pipeline: build a plan with `db.query`, then run it with `db.execute`. `acc` provides the group totals (`acc.count()`, `acc.max(...)`, and friends), the equivalent of v6's `_count` / `_sum` / `_max`.
250254

251255
```typescript tab="After"
252256
import { acc } from '@prisma-next/mongo-query-builder';
253257

254-
const runtime = await db.runtime();
255258
const plan = db.query
256259
.from('posts')
257-
.match((f) => f.authorId.eq(authorId))
258260
.group((f) => ({ _id: f.kind, postCount: acc.count(), latest: acc.max(f.createdAt) }))
259261
.sort({ postCount: -1 })
260262
.build();
261-
const byKind = await runtime.execute(plan);
263+
const byKind = await db.execute(plan).toArray();
262264
```
263265

264266
```typescript tab="Before"
265267
await prisma.post.groupBy({
266268
by: ['kind'],
267-
where: { authorId },
268269
_count: { _all: true },
269270
_max: { createdAt: true },
270271
orderBy: { _count: { kind: 'desc' } },
271272
});
272273
```
273274

275+
:::warning[ObjectId filters need a raw command]
276+
277+
Don't add `.match((f) => f.authorId.eq(authorId))` to filter by an `ObjectId` field (`_id`, or a foreign key like `authorId`): the typed builder sends the id through as a plain string, so the stage silently matches nothing. Filter ObjectId fields with the ORM client instead (`db.orm.posts.where({ authorId })` encodes them for you), or run the aggregation as a raw command carrying a real `ObjectId`:
278+
279+
```typescript
280+
import { RawAggregateCommand } from '@prisma-next/mongo-query-ast/execution';
281+
import { ObjectId } from 'mongodb';
282+
283+
const plan = db.query.rawCommand(
284+
new RawAggregateCommand('posts', [
285+
{ $match: { authorId: new ObjectId(String(authorId)) } },
286+
{ $group: { _id: '$kind', postCount: { $count: {} }, latest: { $max: '$createdAt' } } },
287+
{ $sort: { postCount: -1 } },
288+
]),
289+
);
290+
const byKind = await db.execute(plan).toArray();
291+
```
292+
293+
:::
294+
274295
### 3d. Transactions
275296

276297
Prisma Next has no `transaction` method for MongoDB yet. For multi-document transactions, use the `mongodb` driver's [Transaction API](https://www.mongodb.com/docs/drivers/node/current/crud/transactions/transaction-conv/).
277298

278299
```typescript tab="After"
279300
import { MongoClient } from 'mongodb';
280301

281-
const client = new MongoClient(DATABASE_URL);
302+
const client = new MongoClient(process.env['DATABASE_URL']!);
282303
const session = client.startSession();
283304
const mdb = client.db('my-app-db');
284305

285306
try {
286307
await session.withTransaction(async () => {
287-
// Pass { session } to EVERY operationan op without it silently runs
288-
// outside the transaction.
308+
// Pass { session } to EVERY operation: an op without it silently
309+
// runs outside the transaction.
289310
await mdb
290311
.collection('users')
291312
.insertOne({ email: 'a@e.com', role: 'author' }, { session });
@@ -313,13 +334,12 @@ The direct replacement for v6's `findRaw` and `aggregateRaw` is `db.raw`.
313334
```typescript tab="After"
314335
import { db } from './db';
315336

316-
// Raw aggregation on a collection — replaces v6's aggregateRaw / findRaw.
317-
const runtime = await db.runtime();
337+
// Raw aggregation on a collection, replaces v6's aggregateRaw / findRaw.
318338
const plan = db.raw
319339
.collection('posts')
320340
.aggregate([ ... ])
321341
.build();
322-
const rows = await runtime.execute(plan).toArray();
342+
const rows = await db.execute(plan).toArray();
323343
```
324344

325345
```typescript tab="Before"
@@ -333,12 +353,20 @@ const rows = await prisma.post.aggregateRaw({
333353

334354
#### Commands
335355

336-
`db.raw` is collection-scoped, so it has no equivalent for v6's database-level `$runCommandRaw`. For arbitrary commands, construct your own `MongoClient`, pass it to the binding, and use that client directly — the same `db` object keeps giving you the typed ORM.
356+
`db.raw` is collection-scoped, so v6's database-level `$runCommandRaw` has no ORM equivalent. Construct your own `MongoClient` and pass it to the binding from step 1 in place of `url`: the same client then powers the typed ORM and runs arbitrary commands, sharing one connection pool.
337357

338358
```typescript tab="After"
359+
// src/prisma/db.ts
360+
import mongo from '@prisma-next/mongo/runtime';
339361
import { MongoClient } from 'mongodb';
362+
import type { Contract } from './contract.d';
363+
import contractJson from './contract.json' with { type: 'json' };
364+
365+
export const mongoClient = new MongoClient(process.env['DATABASE_URL']!);
366+
export const db = mongo<Contract>({ contractJson, mongoClient, dbName: 'my-app-db' });
340367

341-
const mongoClient = new MongoClient(process.env['DATABASE_URL']);
368+
// Elsewhere: typed ORM and raw commands from the same client.
369+
await db.orm.users.where({ email: 'alice@example.com' }).first();
342370
await mongoClient.db('my-app-db').command({ ping: 1 });
343371
```
344372

@@ -348,6 +376,8 @@ import { prisma } from './db';
348376
await prisma.$runCommandRaw({ ping: 1 });
349377
```
350378

379+
For raw reads and writes scoped to a single collection (anything the typed builder can't express), you don't need the driver: `db.query.rawCommand(...)` packages a raw command into a plan, as shown in [3c](#3c-aggregations). See [Raw queries](/orm/next/reference/raw-queries) for the full surface.
380+
351381
### Quick reference table
352382

353383
| v6 | Prisma Next |
@@ -356,40 +386,51 @@ await prisma.$runCommandRaw({ ping: 1 });
356386
| `prisma.user.findFirst(...)` | `db.orm.users.where({ ... }).first()` |
357387
| `create` / `update` / `delete` / `upsert` | same names on `db.orm.<collection>` |
358388
| `updateMany` / `deleteMany` | `updateAll` / `deleteAll` |
359-
| `aggregate` / `groupBy` | `db.query.from(...).group(...).build()``runtime.execute(plan)` |
360-
| `findRaw` / `aggregateRaw` | `db.raw.collection(...).aggregate(...).build()``runtime.execute(plan)` |
361-
| `$runCommandRaw` | `mongo({ mongoClient, dbName, contractJson })` then `mongoClient.db(dbName).command(...)` |
362-
| `$transaction(...)` | No façade wrapper yet — use driver sessions on a replica set |
389+
| `aggregate` / `groupBy` | `db.query.from(...).group(...).build()``db.execute(plan)` |
390+
| `findRaw` / `aggregateRaw` | `db.raw.collection(...).aggregate(...).build()``db.execute(plan)`, or `db.query.rawCommand(...)` |
391+
| `$runCommandRaw` | share a `MongoClient` with the binding, then `mongoClient.db(...).command(...)` (see 3e) |
392+
| `$transaction(...)` | no wrapper yet; driver sessions on a replica set (see 3d) |
363393
| `$connect` / `$disconnect` | lazy connect on first use; `db.close()` to disconnect |
364394

365395
## 4. Adopt the migration lifecycle
366396

367-
v6 MongoDB had no Prisma Migrate — only `db push` (ephemeral, no history). Prisma Next gives MongoDB first-class, contract-driven migrations: reviewable diffs you commit, reproducible across environments, and an auditable schema history.
397+
v6 MongoDB had no Prisma Migrate, only `db push` (ephemeral, no history). Prisma Next gives MongoDB first-class, contract-driven migrations: reviewable diffs you commit, reproducible across environments, and an auditable schema history.
398+
399+
### Bring the v6 database under management
368400

369-
Use `db update` for local dev (the `db push` analogue) and the migration workflow for shared/prod. The CLI reads the target from `prisma-next.config.ts`:
401+
Your database already has collections and data, so bootstrap it with `db update`: it diffs the live database against your contract, applies the delta (the strict validators, plus any indexes the contract declares that v6 never created), and signs the database so Prisma Next recognizes it from then on. The `--advance-ref db` flag records the resulting contract state as the [`db` ref](/orm/next/migrations/the-migration-graph#name-important-states-with-refs), the starting point later migration plans diff from.
370402

371403
```bash
372-
# Local dev: diff the contract against the live DB and apply directly, no history kept.
373404
npx prisma-next contract emit
374-
npx prisma-next db update --db "$DATABASE_URL"
405+
npx prisma-next db update --db "$DATABASE_URL" --dry-run # preview the delta first
406+
npx prisma-next db update --db "$DATABASE_URL" --advance-ref db
407+
```
408+
409+
Don't reach for `db init` here. It bootstraps only additively, and on a populated database it refuses the validator changes this migration needs, because adding a validator to a collection with existing documents is classed destructive. `db update` applies them after a confirmation prompt.
410+
411+
### Every schema change after that
375412

376-
# Shared branches / production: write a reviewable migration package, then apply it.
377-
npx prisma-next migration plan --name add_posts_indexes
378-
npx prisma-next migrate --db "$DATABASE_URL"
413+
Use `db update` for local experiments (the `db push` analogue, no history kept) and the migration workflow for shared branches and production:
414+
415+
```bash
416+
npx prisma-next migration plan --name add_posts_indexes # plans from the db ref
417+
npx prisma-next migrate --db "$DATABASE_URL" --advance-ref db
379418
npx prisma-next db verify --db "$DATABASE_URL" # check the DB matches the contract
380419
```
381420

382421
**Good to know:**
383422

384-
- You never hand-write migration steps declare indexes and validators in the contract (step 2) and `migration plan` derives the changes.
385-
- Run `db init` once on a database Prisma Next hasn't managed before, and `db sign` after any manual fix. Interrupted `migrate` runs are resumable — just rerun.
386-
- Prisma Next adds strict `$jsonSchema` validators by default; make sure existing documents pass them before running in production.
423+
- You never hand-write migration steps: declare indexes and validators in the contract (step 2) and `migration plan` derives the changes.
424+
- Interrupted `migrate` runs are resumable; just rerun. After fixing anything by hand, run `db sign` so the signature matches the database again.
425+
- Prisma Next adds strict `$jsonSchema` validators by default. Make sure existing documents pass them before running in production: once the validators are live, writes to documents that don't match the contract fail with `Document failed validation`.
387426

388427
## 5. Pre-flight checklist (before production cutover)
389428

390429
Work through this list before you switch production traffic over:
391430

392-
- [ ] **Dry run first.** Rehearse the whole flow on a throwaway copy of your database (`contract emit`, `migration plan`, `migrate`, `db verify`) and make sure `db verify` passes before you touch production.
431+
- [ ] **Same database.** Your Prisma Next config points at the **same** database and connection string as v6.
432+
- [ ] **Server version.** Your MongoDB server is on 8.0 or newer.
433+
- [ ] **Dry run first.** Rehearse the whole flow on a throwaway copy of your database (`contract emit`, `db update --dry-run`, `db update`, `db verify`) and make sure `db verify` passes before you touch production.
393434
- [ ] **Index parity.** The indexes on each collection (`db.collection.getIndexes()`) match your contract.
394435
- [ ] **Validators.** Your existing documents pass the strict validators Prisma Next adds to each collection (see *Good to know* in step 4).
395436
- [ ] **Addressing.** Every call site uses storage names like `db.orm.users`, not model names.
@@ -408,4 +449,4 @@ Once you've cut over, this is the day-to-day workflow:
408449
- **Troubleshooting.** `db verify` checks the database against your contract, and `db sign` records any fixes you make by hand.
409450
- **Performance.** Your aggregations now compile to native MongoDB pipelines, so it's worth benchmarking them against your old v6 raw calls.
410451

411-
See [Prisma Next docs](https://github.com/prisma/prisma-next) for pipeline builder patterns, relation loading strategies, and advanced contract features. This guide gets you across the bridge; Prisma Next's docs are the source of truth from here on.
452+
See the [Prisma Next docs](/orm/next) for [pipeline builder patterns](/orm/next/reference/pipeline-builder), [relation loading strategies](/orm/next/reference/orm-client), and [advanced contract features](/orm/next/contract-authoring/psl-syntax). This guide gets you across the bridge; Prisma Next's docs are the source of truth from here on.

0 commit comments

Comments
 (0)