prisma-8

Use when working in a project that depends on @prisma/orm-postgres, @prisma/orm-sqlite, or @prisma/orm-mongo (Prisma 8, formerly Prisma Next): editing contract.prisma or a contract.ts builder, running `prisma contract emit`, planning or applying migrations, editing migration.ts, writing db.orm / db.sql / db.query queries, wiring db.ts or middleware, integrating a build tool, using the Supabase extension or RLS, or reading a dotted error code such as MIGRATION.HASH_MISMATCH. Use when the user asks "what is Prisma 8", "where do I start", or compares it to another ORM. Use when the user asks to upgrade or bump Prisma 8 in an app or an extension package. Use when you see @internal/* or @prisma/orm-* imports, prisma.config.ts with definePrismaConfig, or contract.json / contract.d.ts. Do not use for Prisma ORM 7 or earlier (schema.prisma + @prisma/client).

Install
npx skills add 'https://github.com/prisma/orm/tree/main/skills/prisma-8'
Download bundle ↓
main · fac8604Scanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

references/queries-mongo.md

references/queries-mongo.mdBrowse 76 files
View on GitHub
← Back to SKILL.md

Prisma 8 — Queries (Mongo)

Load this guide when db.ts imports from @internal/mongo/runtime.

Shared concepts (result consumption, script teardown, cross-target pitfalls, capability gaps) live in queries.md.

Key Concepts

Mongo (mongo<Contract>(...) from @internal/mongo/runtime):

  • 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.
  • 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.

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.

Lane decision table:

NeedChooseWhy
Standard CRUD with reference relationsORM (db.orm.<root>)Collection-shaped; object .where({ ... }); .create / .update / .delete / .upsert.
Eager-load a reference relationORM .include('<relation>')Lowers to $lookup; composes with .where / .select / .orderBy / .limit.
Polymorphic root (discriminated variants)ORM .variant('<VariantName>')Narrows to one variant and injects the discriminator filter.
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.
Aggregation pipeline (group, facet, $lookup with reshaping)Query builder (db.query.from(...))Full pipeline surface; typed row shape through .build().
Typed cross-collection join in a pipelineQuery builder .lookup((from) => from('users').on(...).as('author'))$lookup with compile-time foreign-root checking.
Bulk writes with pipeline semanticsQuery builder write terminals (.insertOne, .updateMany, .findOneAndUpdate, .upsertOne, …)Filtered writes after .match(...); plans execute through the runtime.

Workflow — ORM reads

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:

// src/queries/users.ts — adjust the relative import to match file depth.
import { db } from '../prisma/db';

// All users.
const users = await db.orm.users.all();

// Single row by equality filter.
const alice = await db.orm.users.where({ email: 'alice@example.com' }).first();

// Projection, sort, pagination — same chaining as Postgres.
const recent = await db.orm.posts
  .select('title', 'authorId', 'createdAt')
  .orderBy({ createdAt: -1 })
  .limit(10)
  .all();

.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.

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). Prefer the object form whenever equality suffices.

Polymorphic roots. When the contract declares variants on a model, narrow before querying:

const articles = await db.orm.posts.variant('Article').all();
const tutorials = await db.orm.posts.variant('Tutorial').where({ authorId }).all();

Sorting and pagination. .orderBy({ field: 1 | -1 }) (Mongo sort directions). .limit(n) maps to $limit; .offset(n) maps to $skip.

.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().

Mongo .all() returns the same AsyncIterableResult shape as Postgres — await db.orm.users.all() yields an array; see Consuming the result in queries.md.

Workflow — Eager-loading relations (.include)

Mongo reference relations eager-load through the same .include('<relation>') surface; the ORM lowers to $lookup:

const posts = await db.orm.posts
  .include('author')
  .orderBy({ createdAt: -1 })
  .all();
// → Array<{ title, authorId, createdAt, author: { name, email, ... } }>

Relation names match the contract's @relation field names. Nested includes follow the same chaining rules as the parent collection.

Workflow — ORM writes

Mongo mutations require a preceding .where(...) filter (except .create / .createAll). Updates accept either a partial document or a field-accessor callback for Mongo operators:

// Create — returns the row with server-assigned `_id`.
const user = await db.orm.users.create({
  name: 'Alice',
  email: 'alice@example.com',
  bio: null,
  address: null,
});

// Update one — plain object replaces top-level fields.
await db.orm.users.where({ _id: user._id }).update({ bio: 'Writer' });

// Update one — field operations ($push, $inc, dot-path $set).
await db.orm.users
  .where({ _id: user._id })
  .update((u) => [u.tags.push('admin'), u.loginCount.inc(1)]);

// Update many / delete many — iterate or count.
const updated = await db.orm.users
  .where({ bio: null })
  .updateAll({ bio: 'filled' });
for await (const row of updated) { /* each modified doc */ }

await db.orm.users.where({ _id: user._id }).delete();

// Upsert — filter via .where(), split create vs update branches.
await db.orm.users.where({ email: 'alice@example.com' }).upsert({
  create: { name: 'Alice', email: 'alice@example.com', bio: null, address: null },
  update: { bio: 'Editor' },
});

Count-only terminals. .createAndCount(...), .updateAndCount(...), .deleteAndCount() return numbers without re-reading full documents — useful for bulk operations where you only need the modified count.

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.

Workflow — Aggregates

The Mongo ORM does not expose .aggregate(...) / .groupBy(...). Express aggregations through db.query — the pipeline builder — with .group(...) and accumulator helpers:

import { acc } from '@prisma/orm-mongo/query-builder';

const runtime = await db.runtime();
const plan = db.query
  .from('posts')
  .match((f) => f.authorId.eq(authorId))
  .group((f) => ({
    _id: f.kind,
    postCount: acc.count(),
    latest: acc.max(f.createdAt),
  }))
  .sort({ postCount: -1 })
  .build();

const byKind = await runtime.query(plan);

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).

Workflow — Query builder (db.query)

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):

// src/queries/analytics.ts
import { acc, fn } from '@prisma/orm-mongo/query-builder';
import { db } from '../prisma/db';

const runtime = await db.runtime();

// Read pipeline — match, project, sort, limit.
const plan = db.query
  .from('posts')
  .match((f) => f.authorId.eq(authorId))
  .sort({ createdAt: -1 })
  .limit(10)
  .project('title', 'authorId', 'createdAt')
  .build();
const recent = await runtime.query(plan);

// Cross-collection join ($lookup).
const withAuthor = db.query
  .from('posts')
  .lookup((from) =>
    from('users')
      .on((local, foreign) => ({
        local: local.authorId,
        foreign: foreign._id,
      }))
      .as('author'),
  )
  .build();
const rows = await runtime.query(withAuthor);

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.

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):

const inserted = await runtime.query(
  db.query.from('users').insertOne({ name: 'Alice', email: 'a@e.com', bio: null }),
);

const { affectedRows } = await runtime.execute(
  db.query
    .from('users')
    .match((f) => f.name.eq('Alice'))
    .updateMany((f) => [f.bio.set('filled')]),
);

const [updated] = await runtime.query(
  db.query
    .from('users')
    .match((f) => f.email.eq('a@e.com'))
    .findOneAndUpdate((f) => [f.bio.set('updated')], { returnDocument: 'after' }),
);

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.

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).

Common Pitfalls (Mongo)

  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.
  2. Using .all() when you wanted one row. Use .where({ ... }).first() — not .all().
  3. Calling .update() / .delete() without .where(). Mutations other than .create / .createAll require a filter — the compiler enforces this at the type level where possible.
  4. Using PascalCase model names on ORM. Roots are lowercased plurals from the contract (db.orm.users, not db.orm.User).
  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).
  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.
  7. Trying to use db.sql. There is no db.sql on Mongo.
  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.
  9. Expecting ORM .aggregate(...) / .groupBy(...). Use db.query.from(...).group(...).build() instead.

Reference Files

  • Example queries under examples/mongo-demo/src/server.ts — ORM reads, .include, .variant, and pipeline DSL via db.query.
  • Integration tests under examples/mongo-demo/test/ (blog.test.ts, crud-lifecycle.test.ts, query-builder-writes.test.ts).
  • Query builder README under packages/2-mongo-family/5-query-builders/query-builder/README.md.
  • ORM collection surface under packages/2-mongo-family/5-query-builders/orm/src/collection.ts.

Checklist

  • Used lowercased plural ORM roots (db.orm.users, not db.orm.User).
  • Chose the right lane (ORM by default; db.query for shapes the ORM doesn't express).
  • Used .where({ ... }).first() for single-row reads — not .all().
  • Ran query-builder plans via (await db.runtime()).query(plan); used execute(plan) only for an affected count on a write.
  • For aggregations, used db.query.from(...).group(...) rather than a non-existent ORM .aggregate(...).
  • Did NOT confabulate db.transaction, db.sql, or ORM .aggregate(...) — routed to What Prisma 8 doesn't do yet / references/feedback.md instead.
  • Did NOT use the lower-level builder for something the ORM cleanly expresses.
Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 64.
| Review migrations on deploy | [`references/migration-review.md`](references/migration-review.md) | "what migrations are going to run", "what runs on deploy / merge", merge conflict, diamond convergence, concurrent migrations, migration status, ref management for CI, staging / production environment refs, `MIGRATION.MARKER_NOT_IN_HISTORY`, `MIGRATION.MISSING_INVARIANTS`, `MIGRATION.AMBIGUOUS_TARGET`, `db migrate --show` || Write queries | [`references/queries.md`](references/queries.md) | query, where, select, project, orderBy, limit, offset, take, skip, include, lookup, first, all, count, aggregate, groupBy, create, update, delete, upsert, returning, transaction, `db.orm`, `db.sql`, `runtime.query(plan)` vs `runtime.execute(plan)`, `db.prepare` / prepared statements, streaming / `for await`, many-to-many `include` and nested `connect`, `and` / `or` / `not` from `@prisma/orm-postgres/orm-client`, `db.query.from(...)` (Mongo pipeline), namespace-aware accessors, `.all()` Thenable, single-use iterators (`RUNTIME.ITERATOR_CONSUMED`), target-declared aggregate types (`count`, integer `sum`, and integer `avg` are `number`; `count` and integer `sum` throw outside ±(2^53 − 1) rather than round, while `avg` is a fraction already and carries no guard; `countBigInt` / `sumBigInt` / `avgDecimal` are the lossless forms, `avgDecimal` on PostgreSQL only), drizzle-style, kysely-style. Naming types (§ *Naming model and result types*): "type of my model", "return type of a query", `ResultType`, `Scalars`, `Shape`, `models` / `Models` namespace in `contract.d.ts`, `Models.public_User`, Prisma 7 `Prisma.User` / `GetPayload` / `UserGetPayload` equivalents. Postgres/SQLite specifics: [`references/queries-postgres.md`](references/queries-postgres.md); Mongo specifics: [`references/queries-mongo.md`](references/queries-mongo.md) || Wire the runtime | [`references/runtime.md`](references/runtime.md) | `db.ts`, `postgres<Contract>(...)` / `sqlite<Contract>(...)` / `mongo<Contract>(...)` façades, middleware composition (lints, budgets, cache via `@prisma/orm-extension-middleware-cache`, custom `afterQuery` middleware, `@prisma/orm-postgres/family-runtime`), `DATABASE_URL`, `.env`, connection pool / `poolOptions`, dev vs prod config, transactions, read replicas, multi-database, script won't exit / hangs, `db.close` / `pool.end`, `await using` / `[Symbol.asyncDispose]` |