Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
26e21f8
TML-3690: RuntimeMode is declared once, in framework-components
wmadden-electric Oct 23, 2026
2a8447a
TML-3689: A stack with a driver always has one
wmadden-electric Oct 23, 2026
dd0b29f
TML-3689: Record upgrade instructions for the required stack driver
wmadden-electric Oct 23, 2026
9449d14
TML-3686: The published mongoQuery validates its contract once
wmadden-electric Oct 23, 2026
2480add
TML-3686: Register the MongoDB facade query-builder entry in the arch…
wmadden-electric Oct 23, 2026
d96b2ea
TML-3689: SqlExecutionStackWithDriver is SqlExecutionStack with a req…
wmadden-electric Oct 23, 2026
8fda81f
TML-3689: Docs show the runtime path the clients use, not instantiate…
wmadden-electric Oct 23, 2026
5e05ed3
TML-3686: The only mongoQuery a user can import is the validating one
wmadden-electric Oct 23, 2026
a021cac
Merge branch 'main' of https://github.com/prisma/prisma into tml-3689…
wmadden-electric Oct 23, 2026
626282c
TML-3686: Examples import from published packages, and the upgrade de…
wmadden-electric Oct 23, 2026
25f554a
TML-3689: The upgrade detection finds driver destructured from a stack
wmadden-electric Oct 23, 2026
9ee860d
TML-3689: Stack driver detection matches driver only as the destructu…
wmadden-electric Oct 23, 2026
e422b7b
TML-3686: mongoQuery takes an execution context, as the SQL builder does
wmadden-electric Oct 27, 2026
cc9da80
TML-3686: MongoExecutionContext lives in query-ast without a stack, a…
wmadden-electric Oct 27, 2026
5e67745
Merge branch 'main' of https://github.com/prisma/prisma into tml-3689…
wmadden-electric Oct 27, 2026
1225f36
TML-3689: createSqlExecutionStack builds the stack through createExec…
wmadden-electric Oct 27, 2026
bfecc75
Merge origin/main into tml-3689-runtime-type-cleanups
wmadden-electric Oct 28, 2026
09f121e
TML-3686: the Mongo runtime takes an adapter, and mongoOrm and mongoR…
wmadden-electric Oct 28, 2026
6acd291
TML-3686: mongoOrm takes a context and a runtime, as the SQL orm() does
wmadden-electric Oct 28, 2026
680e68b
Merge branch 'main' of https://github.com/prisma/prisma into tml-3689…
wmadden-electric Oct 28, 2026
61e3475
TML-3686: the Mongo context builds enum accessors once with buildName…
wmadden-electric Oct 28, 2026
375428b
TML-3686: db.enums is built by the static context, and the ORM builds…
wmadden-electric Oct 28, 2026
0b3daac
TML-3686: one codec lookup builds Mongo enum accessors
wmadden-electric Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const user = await db.User.byEmail('alice@example.com').first();
**Mongo ORM (target — not yet implemented):**

```typescript
const db = mongoOrm({ contract, runtime });
const db = mongoOrm({ context, runtime });

// Same custom collection pattern
class UserCollection extends Collection<Contract, 'User'> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ The migration file imports the contract *locally*, not from the project root:
import type { Contract } from './contract'
import contractJson from './contract.json' with { type: 'json' }

const raw = mongoRaw({ contract: contractJson as Contract })
const { raw } = mongoStatic({ contract: contractJson as Contract })
```

`contract.json` and `contract.d.ts` are copies — snapshots of the project's emitted contract when the migration was scaffolded. The migration never reaches outside its own directory for type information.
Expand Down Expand Up @@ -88,8 +88,8 @@ import type { Contract as Intermediate } from './intermediate'
import contractJson from './contract.json' with { type: 'json' }
import intermediateJson from './intermediate.json' with { type: 'json' }

const dest = mongoRaw({ contract: contractJson as Contract })
const mid = mongoRaw({ contract: intermediateJson as Intermediate })
const dest = mongoStatic({ contract: contractJson as Contract }).raw
const mid = mongoStatic({ contract: intermediateJson as Intermediate }).raw
```

Multiple intermediate contracts are supported — the user names them and emits them independently. Each is a self-contained contract snapshot that lives in the migration directory.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The chain has three qualitatively different vocabularies, so it gets three class
- **`FilteredCollection<TContract, ModelName>`** — reached by `.match(...)` on a `CollectionHandle` (or chained match). Filtered writes (`updateMany`, `updateOne`, `deleteMany`, `deleteOne`), `upsertOne(updater)`, `findOneAndUpdate`, `findOneAndDelete`. Further `.match(...)` calls stay on `FilteredCollection` (AND-folded); any pipeline-stage method moves to `PipelineChain`.
- **`PipelineChain<TContract, Shape, U, F, L>`** — reached by any pipeline-stage method on either earlier state. Pipeline stages (`.sort`, `.limit`, `.group`, `.lookup`, …), `$merge`/`$out` write terminals, and read terminals (`.build()`, `.aggregate()`). Pipeline-style writes and find-and-modify are *conditional* — available only when the capability markers `U` and `F` are still "enabled"; `L` is a prefix tracker that conditions what a further `.match(...)` does to `U`. See § 2.

The user-facing entry point `mongoQuery({ contract }).from(name)` returns a `CollectionHandle`. Everything else is reached from there.
The user-facing entry point `db.query.from(name)` (or `mongoStatic({ contract }).query.from(name)`) returns a `CollectionHandle`. Everything else is reached from there.

> Throughout this ADR a **terminal** is a method that ends the chain by returning a `MongoQueryPlan` (e.g. `.build()`, `.updateMany()`, `.findOneAndUpdate(...)`). A **stage** is a method that extends the chain and returns another builder class.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ For each default of `(namespace, entry)` with a phase for `op` (`onCreate` for `
- **A field is applied at most once per call**, even when two defaults name it.
- **`namespace` is required.** Refs are namespace-scoped, so only defaults declared for `(namespace, entry)` apply; that is what tells two same-named tables in different schemas apart. Requiring it keeps the coordinate part of every match: a missing namespace is a caller bug, not a quiet fallback to matching by entry name alone.

`MutationDefaults` is the interface an ORM depends on: `applyMutationDefaults(options): ReadonlyArray<AppliedMutationDefault>`. The SQL lane `ExecutionContext` and `MongoExecutionContext` extend it, and the Mongo ORM takes it as `mongoOrm({ contract, executor, mutationDefaults })`.
`MutationDefaults` is the interface an ORM depends on: `applyMutationDefaults(options): ReadonlyArray<AppliedMutationDefault>`. The SQL lane `ExecutionContext` and `MongoExecutionContext` extend it, and the Mongo ORM reads it from the context it takes, `mongoOrm({ context, runtime })`, as the SQL ORM reads it from `orm({ context, runtime })`.

## The execution section

Expand Down
11 changes: 7 additions & 4 deletions docs/architecture docs/subsystems/10. MongoDB Family.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ The contract records each generated field in an optional `execution` section, wi

Because a ref names a collection and a field, a default applies to every document in the collection. On a polymorphic model a generated field therefore belongs on the base model, and every variant create fills it; a generator attribute or generating type alias on a variant's own field is rejected (`PSL_GENERATOR_ON_VARIANT_FIELD` in PSL, `CONTRACT.DEFAULT_INVALID` with `reason: 'executionDefaults-on-variant'` in TS). Two models stored in one collection that declare the same field with identical defaults share one ref; different defaults are rejected (`PSL_GENERATOR_CONFLICT`, or `reason: 'executionDefaults-conflict'`). Such a collection's validator accepts a document of either model (see [Schema validation](#schema-validation-jsonschema)).

At runtime, `createMongoExecutionContext` collects `mutationDefaultGenerators` from the target, adapter and extensions (the adapter registers `timestampNow`) and throws `RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING` if the contract needs a generator no component provides; the registry, the check, and the apply loop are the framework's shared mutation-default runtime ([Runtime & Middleware Framework](4.%20Runtime%20%26%20Middleware%20Framework.md#mutation-defaults), [ADR 256](../adrs/ADR%20256%20-%20Mutation-default%20generators%20are%20a%20framework%20runtime%20concern.md)). The context implements the framework `MutationDefaults` interface, which the ORM depends on without depending on the Mongo runtime, and `mongoOrm({ contract, executor, mutationDefaults })` calls it on every write (`mongo()` passes the context; a hand-built `mongoOrm()` over a contract with execution defaults and no `mutationDefaults` throws `ORM.MUTATION_DEFAULTS_MISSING`), with the same rules as SQL:
At runtime, `createMongoExecutionContext` collects `mutationDefaultGenerators` from the target, adapter and extensions (the adapter registers `timestampNow`) and throws `RUNTIME.MUTATION_DEFAULT_GENERATOR_MISSING` if the contract needs a generator no component provides; the registry, the check, and the apply loop are the framework's shared mutation-default runtime ([Runtime & Middleware Framework](4.%20Runtime%20%26%20Middleware%20Framework.md#mutation-defaults), [ADR 256](../adrs/ADR%20256%20-%20Mutation-default%20generators%20are%20a%20framework%20runtime%20concern.md)). The context implements the framework `MutationDefaults` interface, which the ORM depends on without depending on the Mongo runtime, and `mongoOrm({ context, runtime })` calls it on every write, with the same rules as SQL:

- `create`, `createAll`, `createAndCount` and the insert half of `upsert` fill `onCreate` fields; `update`, `updateAll`, `updateAndCount` and the update half of `upsert` add `onUpdate` fields to `$set`.
- A field the write sets explicitly keeps its value. The ORM drops `undefined` values before it applies defaults, so a field set to `undefined` gets its default. An update whose payload touches no field gets no generated values.
Expand Down Expand Up @@ -318,7 +318,7 @@ db.posts.updateMany(
The ORM presents the same chaining API to consumers regardless of database family. A developer querying MongoDB uses the same `where()`, `select()`, `include()`, `orderBy()`, `limit()`, and `all()` methods as one querying Postgres:

```typescript
const db = mongoOrm({ contract, executor: runtime, enums: buildMongoEnums(contract, context.codecs) });
const db = mongoOrm({ context, runtime });

class UserCollection extends Collection<Contract, 'User'> {
admins() { return this.where({ kind: 'admin' }); }
Expand Down Expand Up @@ -353,10 +353,13 @@ Row type inference, custom collection subclasses, and the `include` interface al

Below the ORM and above the raw command AST sits `@internal/mongo-query-builder`: a contract-bound, typed CRUD surface that produces the same `MongoQueryPlan` values the ORM emits. The ORM is model-centric and multi-collection (roots, includes, repositories); the query builder is collection-centric and single-command (reads, writes, find-and-modify, `$merge` / `$out`). Migrations, raw tooling, and other non-ORM callers author plans through the query builder rather than hand-building AST nodes.

`mongoQuery({ context })` takes an execution context, as the SQL builder `sql({ context })` does; it reads the contract from the context. A context comes only from `mongoStatic({ contract })` or `mongo({ contract })`, which validate the contract once, so applications use `mongoStatic({ contract }).query` or `db.query`. `@prisma/orm-mongo/query-builder` republishes the builder unchanged.

```typescript
import { acc, mongoQuery } from '@internal/mongo-query-builder';
import { acc } from '@prisma/orm-mongo/query-builder';
import mongoStatic from '@prisma/orm-mongo/static';

const q = mongoQuery({ contract }); // from contractFromJson<Contract>(json) or defineContract
const q = mongoStatic({ contract }).query; // or db.query from mongo({ contract, url })

// Read — aggregation pipeline
const analytics = q
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,12 +632,12 @@ TPlan

The runtime does not own pooling or sockets; it delegates to a target driver. This thin-core approach keeps the runtime simple and stable while adapters and packs carry dialect and capability logic.

**Lifecycle:** instantiate stack with driver → connect at boundary → create runtime ([ADR 159](../adrs/ADR%20159%20-%20Driver%20Terminology%20and%20Lifecycle.md)).
**Lifecycle:** create the adapter and driver from the stack's descriptors → connect at boundary → create runtime ([ADR 159](../adrs/ADR%20159%20-%20Driver%20Terminology%20and%20Lifecycle.md)).

1. Create
- `createSqlExecutionStack({ target, adapter, driver, extensions })` - Creates SQL descriptor stack.
- `createSqlExecutionStack({ target, adapter, driver, extensions })` - Creates SQL descriptor stack: a `SqlExecutionStackWithDriver` when given a driver, for clients that connect, and a `SqlExecutionStack` without one, for the query surface and `toSql`.
- `createExecutionContext({ contract, stack })` - Creates context from contract and descriptors-only stack. Context creation uses descriptor `SqlStaticContributions` (codecs, operation signatures, parameterized codecs) — no instantiation required.
- `instantiateExecutionStack(stack)` - Instantiates all components including driver (unbound).
- `stack.adapter.create(stack)` and `stack.driver.create(options)` - The client creates the adapter and the unbound driver from the stack's descriptors. `instantiateExecutionStack(stack)` from the framework does the same for every component, and tests use it.
- Caller resolves binding from options and calls `driver.connect(binding)` at the boundary where env is available.
- Target factory (e.g. `postgres()`) constructs `new PostgresRuntimeImpl({ context, adapter, driver, verifyMarker, middleware })` — a concrete subclass of `SqlRuntimeBase` / `RuntimeCore`. There is no family-level `createRuntime` factory.
- Validates `contract.json` and caches `storageHash` / `profileHash` (contract is already validated before being passed to context).
Expand Down
14 changes: 7 additions & 7 deletions docs/reference/Mongo Pipeline Builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ This page is a working reference: a concrete example to ground the mental model,
Imagine an `orders` collection where each document has `customerId`, `amount`, and `status`. We want the top ten customers by total spend on active orders:

```ts
import { contractFromJson } from '@internal/mongo/static';
import { mongoQuery } from '@internal/mongo-query-builder';
import mongoStatic, { contractFromJson } from '@prisma/orm-mongo/static';
import { acc } from '@prisma/orm-mongo/query-builder';
import type { Contract } from './contract';
import contractJson from './contract.json' with { type: 'json' };

const query = mongoQuery({ contract: contractFromJson<Contract>(contractJson) });
const { query } = mongoStatic({ contract: contractFromJson<Contract>(contractJson) });

const plan = query
.from('orders')
Expand Down Expand Up @@ -73,7 +73,7 @@ Examples in the rest of this page assume:

```ts
const orders = query.from('orders');
import { acc, fn } from '@internal/mongo-query-builder';
import { acc, fn } from '@prisma/orm-mongo/query-builder';
```

`fn` is a namespace of typed expression helpers (`fn.toUpper(...)`, `fn.concat(...)`, `fn.literal(...)`); `acc` is the parallel namespace of accumulators (`acc.sum(...)`, `acc.count(...)`, …). Both produce `MongoAggExpr` AST nodes under the hood, but you rarely need to think about that.
Expand Down Expand Up @@ -144,7 +144,7 @@ orders.sortByCount((f) => f.status).build();
For bucketing the typed surface gives you the chain entry point but the bucket boundaries / grouping expression flow as raw AST nodes (most commonly `MongoAggFieldRef.of('field')` — see [the primitives reference](./mongodb-primitives-reference.md)):

```ts
import { MongoAggFieldRef } from '@internal/mongo-query-ast/execution';
import { MongoAggFieldRef } from '@prisma/orm-mongo/query-ast/execution';

orders
.bucket({ groupBy: MongoAggFieldRef.of('amount'), boundaries: [0, 100, 1000] })
Expand All @@ -170,7 +170,7 @@ orders
`graphLookup` is the recursive form. `unionWith` appends another collection's rows to the current pipeline. Both expose option objects rather than fluent chains:

```ts
import { MongoAggFieldRef } from '@internal/mongo-query-ast/execution';
import { MongoAggFieldRef } from '@prisma/orm-mongo/query-ast/execution';

orders
.graphLookup({
Expand Down Expand Up @@ -299,7 +299,7 @@ What is *not* statically guaranteed:
Two levels of escape hatch are available when the typed surface does not reach a use case:

```ts
import { MongoLimitStage } from '@internal/mongo-query-ast/execution';
import { MongoLimitStage } from '@prisma/orm-mongo/query-ast/execution';

// Append a raw stage; preserves the chain but collapses the shape unless
// you supply a NewShape to pipe<NewShape>().
Expand Down
8 changes: 2 additions & 6 deletions docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ An aggregate was invoked for an operation/input pair the composed target declare

### ORM.ARGUMENT_INVALID

A method argument on the ORM client, or on the `sql()` / Mongo query-builder DSLs, is malformed or missing a required part: a `null` where-arg, `upsert()` without conflict columns or without a create value for a conflict column, a custom collection given as an instance / for a nonexistent model in a client's `collections` option or `orm({ collections })`, invalid builder argument shapes, `$and`/`$or` with no expressions, a limit, offset or skip that is negative or not an integer, malformed lookup/group/update specs, a row-locking method (`forUpdate()`, `forNoKeyUpdate()`, `forShare()`, `forKeyShare()`) given both `nowait` and `skipLocked`, or a `variant()` call whose value is not a declared discriminator value of the receiver model, or on a model with no discriminator (SQL and Mongo ORMs; the payload adds `value` and `declaredValues`). The Mongo ORM also raises it when `mongoOrm()` is given `enums` without an accessor for an enum the contract declares (`argument: 'enums'`, `namespace`, `enum`), and when a write through `createMongoCollection()` finds no accessor for the field's enum (`argument: 'enums'`, `enum`); pass `buildMongoEnums(contract, context.codecs)`. For SQL, the limit/offset check runs in relational-core when the `SelectAst` is constructed, so every SQL lane and target raises it before any SQL is rendered. That check also refuses integers above `Number.MAX_SAFE_INTEGER`, and does not check a limit or offset bound as a parameter. Payload: `method`, `argument` (`limit` or `offset` for the SQL limit/offset check), `model`, `column`, `key`. The `sql()` builder also raises it when a table's `indexes` is read by a name that more than one of its indexes shares (payload: `namespaceId`, `tableName`, `index`). It also raises it when a query is built with a column of an alias that is not one of the query's sources, such as one from an index read from `table.as('other').indexes` (payload: `alias`, `column`, `sources`).
A method argument on the ORM client, or on the `sql()` / Mongo query-builder DSLs, is malformed or missing a required part: a `null` where-arg, `upsert()` without conflict columns or without a create value for a conflict column, a custom collection given as an instance / for a nonexistent model in a client's `collections` option or `orm({ collections })`, invalid builder argument shapes, `$and`/`$or` with no expressions, a limit, offset or skip that is negative or not an integer, malformed lookup/group/update specs, a row-locking method (`forUpdate()`, `forNoKeyUpdate()`, `forShare()`, `forKeyShare()`) given both `nowait` and `skipLocked`, or a `variant()` call whose value is not a declared discriminator value of the receiver model, or on a model with no discriminator (SQL and Mongo ORMs; the payload adds `value` and `declaredValues`). For SQL, the limit/offset check runs in relational-core when the `SelectAst` is constructed, so every SQL lane and target raises it before any SQL is rendered. That check also refuses integers above `Number.MAX_SAFE_INTEGER`, and does not check a limit or offset bound as a parameter. Payload: `method`, `argument` (`limit` or `offset` for the SQL limit/offset check), `model`, `column`, `key`. The `sql()` builder also raises it when a table's `indexes` is read by a name that more than one of its indexes shares (payload: `namespaceId`, `tableName`, `index`). It also raises it when a query is built with a column of an alias that is not one of the query's sources, such as one from an index read from `table.as('other').indexes` (payload: `alias`, `column`, `sources`).

### ORM.CAPABILITY_MISSING

Expand Down Expand Up @@ -1088,10 +1088,6 @@ The Mongo ORM client was asked to operate on a model name that is not in the con

`create()` or `createAndCount()` was called with zero rows; at least one row of data is required. Payload: `method`, `namespaceId`, `tableName`.

### ORM.MUTATION_DEFAULTS_MISSING

`mongoOrm()` was built over a contract with execution defaults (fields such as `temporal.CreatedAt` that the ORM fills on write) without `mutationDefaults`, so those fields would never be written. Pass the execution context, `mongoOrm({ contract, executor, mutationDefaults: context })`, or create the client with `mongo()`. Payload: `fields` (`<collection>.<field>` for each default).

### ORM.MUTATION_ROW_MISSING

A mutation that expected the database to return a row got none: `create()`/`upsert()` read-back, MTI base or variant INSERT, or a nested create. The Prisma-classic analogue of P2025. Payload: `operation`, `model`, `tableName`, `phase`.
Expand Down Expand Up @@ -1212,7 +1208,7 @@ A codec descriptor handed to a target codec-descriptor registry (Postgres, SQLit

### RUNTIME.CODEC_DESCRIPTOR_MISSING

A column, a domain enum, or an AST-carried CodecRef references a `codecId` for which no runtime component registered a codec descriptor: usually the extension pack that owns the codec is missing from the runtime stack. Surfaces at SQL context construction during the contract codec walk, when a SQL or Mongo client builds `db.enums` (for Mongo, in `buildMongoEnums`, which also builds the accessors `mongoOrm()` takes), or lazily when the AST codec resolver materializes a codec at query time. Payload: `codecId`; on the column path also `table`, `column`.
A column, a domain enum, or an AST-carried CodecRef references a `codecId` for which no runtime component registered a codec descriptor: usually the extension pack that owns the codec is missing from the runtime stack. Surfaces at SQL context construction during the contract codec walk, when a SQL or Mongo client builds `db.enums` (for Mongo, also when `mongoOrm()` builds the accessors it checks written enum values against), or lazily when the AST codec resolver materializes a codec at query time. Payload: `codecId`; on the column path also `table`, `column`.

### RUNTIME.CODEC_MISSING

Expand Down
Loading
Loading