Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
0d63f59
TML-3474: re-plan the remaining slices; specs for 3a and 4a
wmadden-electric Oct 8, 2026
b5a51bb
TML-3474: the spec describes scaffolding as it is today
wmadden-electric Oct 8, 2026
2808c74
TML-3474: specs name the ADR amendments, variant deletes and the type…
wmadden-electric Oct 9, 2026
3699206
Merge main
wmadden-electric Oct 9, 2026
e44518a
TML-3474: the plan and the 3a and 4a specs lead with tables and short…
wmadden-electric Oct 9, 2026
e86e483
TML-3478: the 4a spec records Will's decisions on field removal, name…
wmadden-electric Oct 9, 2026
9d2922e
TML-3474: dispatch plans for 3a and 4a, first briefs, and the new tic…
wmadden-electric Oct 9, 2026
0b4a222
TML-3478: a renameCollection operation runs end to end on MongoDB
wmadden-electric Oct 9, 2026
9b6df6e
TML-3478: the rename test reads the validator with full collection info
wmadden-electric Oct 9, 2026
fc8d24e
TML-3478: renameField and unsetField rewrite documents past the valid…
wmadden-electric Oct 9, 2026
f1e5f1f
TML-3478: field rewrites resume after a partial run and refuse names …
wmadden-electric Oct 9, 2026
7f707e8
TML-3478: the MongoDB planner carries out renames and asks about remo…
wmadden-electric Oct 9, 2026
50e0e32
TML-3478: the statements runner test reads operation labels with thei…
wmadden-electric Oct 9, 2026
7f47aa5
TML-3478: removed MongoDB fields leave no document the validator reje…
wmadden-electric Oct 9, 2026
a438e88
TML-3478: a base field renamed after moving to a variant stays with t…
wmadden-electric Oct 9, 2026
dee0274
TML-3478: journeys, docs and upgrade fragments for MongoDB renames an…
wmadden-electric Oct 9, 2026
cbb7534
TML-3478: ADR 188 classes planner rewrites by their effect; ADR 264 g…
wmadden-electric Oct 9, 2026
79c5319
TML-3478: ADR 188 says which transforms are data, whoever writes them
wmadden-electric Oct 9, 2026
6a204a1
TML-3478: the extension fragment detects AnyMongoDdlCommand and only …
wmadden-electric Oct 9, 2026
d15f0f4
TML-3478: MongoDB operations that run queries are DML operations
wmadden-electric Oct 9, 2026
f36ab4a
TML-3478: one family rule recognises the planner's field rewrites
wmadden-electric Oct 9, 2026
e8862a0
TML-3478: one rule for field names MongoDB cannot rewrite, and a same…
wmadden-electric Oct 9, 2026
7777ed2
TML-3478: planCalls takes the statement calls and their schema as one…
wmadden-electric Oct 9, 2026
e0676e4
TML-3478: split the MongoDB statement tests by what they test
wmadden-electric Oct 9, 2026
5824bc9
TML-3478: a removed field is unset only in a managed collection
wmadden-electric Oct 9, 2026
99480e2
TML-3478: a variant rename that changes its discriminator value is re…
wmadden-electric Oct 9, 2026
eab9bbf
TML-3478: a rewrite the database user may not run past validation nam…
wmadden-electric Oct 9, 2026
0af4ca2
TML-3478: the app fragment covers the preview's getCollection form, t…
wmadden-electric Oct 9, 2026
2bcb0cd
TML-3478: ADR 188 uses the MongoDmlOperation and MongoDmlCheck names
wmadden-electric Oct 9, 2026
f69bccd
TML-3478: ADR 188 calls the run-shaped operation a DML operation
wmadden-electric Oct 9, 2026
6336910
TML-3478: ADR 188 says DML operation where it tells shapes apart
wmadden-electric Oct 9, 2026
b7d84d0
TML-3478: the privilege failure names MongoDB's error code, not its c…
wmadden-electric Oct 9, 2026
f55b1bb
TML-3478: manual QA script for MongoDB renames and deletes
wmadden-electric Oct 9, 2026
5965751
TML-3478: manual QA report for slice 4a
wmadden-electric Oct 9, 2026
5eab8fb
TML-3478: the no-snapshot recovery steps in a data-loss question are …
wmadden-electric Oct 9, 2026
b3aab16
TML-3478: a field rewrite's label says which documents it touches in …
wmadden-electric Oct 9, 2026
d53972b
TML-3478: a field rename stopped by documents holding the new name sa…
wmadden-electric Oct 9, 2026
39a18df
TML-3478: renaming or removing a field of a time-series collection is…
wmadden-electric Oct 9, 2026
0e28210
TML-3478: the question about a dropped collection whose model keeps i…
wmadden-electric Oct 12, 2026
66a3168
TML-3478: a journey checks that the no-snapshot recovery steps print …
wmadden-electric Oct 12, 2026
9241436
TML-3478: advice to self-emit a migration names the command that work…
wmadden-electric Oct 12, 2026
7fec13d
TML-3478: on Postgres and SQLite, the question about a dropped table …
wmadden-electric Oct 12, 2026
613bf7c
TML-3478: migration plan refuses a MongoDB field change that leaves t…
wmadden-electric Oct 12, 2026
cf3879e
TML-3478: every instruction to run a migration.ts gives the command f…
wmadden-electric Oct 12, 2026
6b1ab6c
Merge remote-tracking branch 'origin/main' into tml-3478-mongo-renames
wmadden-electric Oct 12, 2026
83effa7
TML-3478: the MongoDB family doc and the error reference give the sel…
wmadden-electric Oct 12, 2026
bc2ca12
TML-3478: a three-part statement side that reads both as namespace.Mo…
wmadden-electric Oct 12, 2026
2bfe866
TML-3478: removing a declared time-series or clustered-index option i…
wmadden-electric Oct 12, 2026
1d984fc
TML-3478: the advice to keep a dropped collection also covers a new c…
wmadden-electric Oct 12, 2026
f6720eb
TML-3478: node migration.ts finds the config above its own directory,…
wmadden-electric Oct 13, 2026
92eb093
TML-3478: the advice to keep a collection's documents puts renameColl…
wmadden-electric Oct 13, 2026
b816028
TML-3478: a collection rename stopped because the new collection exis…
wmadden-electric Oct 13, 2026
3f0de5c
TML-3478: deferred.md ties keepDataAdvice to the missing statement th…
wmadden-electric Oct 13, 2026
94bbe66
TML-3478: without --config, migration.ts uses the config whose migrat…
wmadden-electric Oct 13, 2026
63ef84c
TML-3478: skills and docs give the self-emit command as node <package…
wmadden-electric Oct 13, 2026
18ff475
TML-3478: tests that run a migration.ts next to their config give it …
wmadden-electric Oct 13, 2026
f03b0ab
Merge origin/main into tml-3478-mongo-renames
wmadden-electric Oct 13, 2026
f70973e
TML-3478: the migration-file config check and the CLI find the migrat…
wmadden-electric Oct 13, 2026
1ef2c1d
TML-3478: the migration plan placeholder summary builds its node comm…
wmadden-electric Oct 13, 2026
ae4a7d4
TML-3478: the multi-extension example names the migration directory i…
wmadden-electric Oct 13, 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
@@ -1,5 +1,7 @@
# ADR 188 — MongoDB migration operation model: data-driven commands and checks

> **Update — 2026-10:** document rewrites the planner writes for `--rename` and `--delete` statements are classed by their effect, not as `data`. See § DML operations and unified check shape.

## Grounding example

The migration planner has compared two contracts and determined that a unique ascending index on `users.email` needs to be created. It needs to produce an operation that the runner can execute.
Expand Down Expand Up @@ -119,26 +121,26 @@ interface MongoMigrationCheck {

This gives checks the same expressive power as MongoDB query filters, without inventing a separate vocabulary.

### Data transform operations and unified check shape
### DML operations and unified check shape

The envelope described above covers DDL operations — creating indexes, modifying collections. Data transform operations (backfills, renames, seed inserts) use the same three-phase envelope but carry different payloads.
The envelope described above covers DDL operations — creating indexes, modifying collections. DML operations, which read and write documents (backfills, field renames and removals, seed inserts), use the same three-phase envelope but carry different payloads. A data transform is a DML operation built with `dataTransform`.

```ts
interface MongoDataTransformOperation extends MigrationPlanOperation {
readonly operationClass: 'data';
interface MongoDmlOperation extends MigrationPlanOperation {
readonly operationClass: MigrationOperationClass;
readonly name: string;
readonly precheck: readonly MongoDataTransformCheck[];
readonly precheck: readonly MongoDmlCheck[];
readonly run: readonly MongoQueryPlan[];
readonly postcheck: readonly MongoDataTransformCheck[];
readonly postcheck: readonly MongoDmlCheck[];
}
```

The `operationClass` is always `'data'`. The `run` array contains `MongoQueryPlan` objects — aggregation pipelines or raw commands (`rawUpdateMany`, `rawInsertOne`, etc.) — rather than DDL commands. The `precheck` and `postcheck` arrays contain `MongoDataTransformCheck` objects instead of `MongoMigrationCheck`.
A transform built with `dataTransform`, whether a user writes it or the planner scaffolds it with a placeholder, has `operationClass: 'data'`. The `renameField` and `unsetField` rewrites carry the class of their effect, whoever writes them, as every other operation does: `renameField` is `widening` and `unsetField` is `destructive`. So `db update`, which does not allow `data`, can run a rename. The runner and the serializer tell a DML operation from a DDL operation by its shape (`run` against `execute`), not by its class. The `run` array contains `MongoQueryPlan` objects — aggregation pipelines or raw commands (`rawUpdateMany`, `rawInsertOne`, etc.) — rather than DDL commands. The `precheck` and `postcheck` arrays contain `MongoDmlCheck` objects instead of `MongoMigrationCheck`.

`MongoDataTransformCheck` has the same four-field shape as `MongoMigrationCheck`:
`MongoDmlCheck` has the same four-field shape as `MongoMigrationCheck`:

```ts
interface MongoDataTransformCheck {
interface MongoDmlCheck {
readonly description: string;
readonly source: MongoQueryPlan;
readonly filter: MongoFilterExpr;
Expand All @@ -148,7 +150,7 @@ interface MongoDataTransformCheck {

The only difference is the `source` type: `MongoQueryPlan` (an aggregation or raw command) instead of `AnyMongoInspectionCommand`. Both use `MongoFilterExpr` for `filter` and `'exists' | 'notExists'` for `expect`. Both are evaluated by the same `FilterEvaluator` path in the runner — run the source query, apply the filter client-side, check against the expectation.

The two types are kept concrete rather than collapsed into a generic `MigrationCheck<TSource>` because the source types need different executors — DDL checks dispatch through `MongoInspectionCommandVisitor`, data transform checks dispatch through `MongoAdapter.lower()` → `MongoDriver.execute()`. Two concrete types with a shared shape let the runner dispatch internally based on which operation type it's processing.
The two types are kept concrete rather than collapsed into a generic `MigrationCheck<TSource>` because the source types need different executors — DDL checks dispatch through `MongoInspectionCommandVisitor`, DML checks dispatch through `MongoAdapter.lower()` → `MongoDriver.execute()`. Two concrete types with a shared shape let the runner dispatch internally based on which operation type it's processing.

#### One factory derives both precheck and postcheck

Expand All @@ -169,11 +171,11 @@ The factory resolves this into a precheck with `expect: 'exists'` (violations fo

#### DML execution

Data transform `run` commands execute via `MongoAdapter.lower()` → `MongoDriver.execute()` — the same adapter+driver transport used for runtime queries. No bespoke DML executor is needed; the adapter lowers each `MongoQueryPlan` to a wire command, and the driver executes it against the database.
DML `run` commands execute via `MongoAdapter.lower()` → `MongoDriver.execute()` — the same adapter+driver transport used for runtime queries. No bespoke DML executor is needed; the adapter lowers each `MongoQueryPlan` to a wire command, and the driver executes it against the database.

#### Relationship to the generic envelope

Both DDL and data transform operations are specializations of the generic three-phase migration operation envelope described in [ADR 191](ADR%20191%20-%20Generic%20three-phase%20migration%20operation%20envelope.md). The DDL variant fills the phases with inspection commands, DDL commands, and `MongoMigrationCheck`; the data transform variant fills them with query plans and `MongoDataTransformCheck`. The runner processes both through the same three-phase loop.
Both DDL and DML operations are specializations of the generic three-phase migration operation envelope described in [ADR 191](ADR%20191%20-%20Generic%20three-phase%20migration%20operation%20envelope.md). The DDL variant fills the phases with inspection commands, DDL commands, and `MongoMigrationCheck`; the DML variant fills them with query plans and `MongoDmlCheck`. The runner processes both through the same three-phase loop.

## Serialization

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ A schema that declares no `@@map` and whose storage was created under the loweri
- A model with no `@@map` names storage that can be quoted exactly as written, which is what a database created by an earlier Prisma version holds.
- `contract infer` round-trips: a table whose name already equals the model name infers with no `@@map` and verifies clean, and a snake_case table still infers with `@@map`.
- A rename needs a hand-written migration. The user states one line per renamed table and the operations follow from the contracts.
- MongoDB has no rename operation. A collection is renamed outside Prisma, with `renameCollection`.
- MongoDB renames a collection with the `renameCollection` operation, which a `--rename` statement plans (see [Migration System § Statements](../subsystems/7.%20Migration%20System.md#statements)).

## Alternatives considered

Expand Down
21 changes: 8 additions & 13 deletions docs/architecture docs/subsystems/10. MongoDB Family.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This document explains what makes MongoDB different from SQL, how each differenc

**Responsibilities:** MongoDB-specific contract types (`MongoContract`), execution pipeline (`MongoQueryPlan`, `MongoDriver`, `MongoRuntime`), codec registry (BSON codecs), ORM client, and contract deserialization and validation (`MongoContractSerializer.deserializeContract()`).

**Migrations are supported.** MongoDB does have server-side schema state — indexes, JSON Schema validators, collection options (capped, time-series, collation) — and Mongo participates in the migration system on the same footing as SQL targets. `prisma migration new --target mongo` and `prisma migration plan --target mongo` scaffold a runnable `migration.ts` (extending `Migration` from `@internal/family-mongo/migration`). Two paths produce identical `ops.json` artifacts — the authoritative migration contract (see [ADR 192](../adrs/ADR%20192%20-%20ops.json%20is%20the%20migration%20contract.md)): inline emit at the end of `migration plan` and self-emit via `./migration.ts` (see [ADR 193](../adrs/ADR%20193%20-%20Class-flow%20as%20the%20canonical%20migration%20authoring%20strategy.md) for the authoring strategy). Data migrations are authored as `dataTransform({ check, run })` operations whose pipelines invariant-guard the transition (see [ADR 176](../adrs/ADR%20176%20-%20Data%20migrations%20as%20invariant-guarded%20transitions.md)). See the [Migration System](7.%20Migration%20System.md) subsystem doc for the full canonical pipeline.
**Migrations are supported.** MongoDB does have server-side schema state — indexes, JSON Schema validators, collection options (capped, time-series, collation) — and Mongo participates in the migration system on the same footing as SQL targets. `prisma migration new --target mongo` and `prisma migration plan --target mongo` scaffold a runnable `migration.ts` (extending `Migration` from `@internal/family-mongo/migration`). Two paths produce identical `ops.json` artifacts — the authoritative migration contract (see [ADR 192](../adrs/ADR%20192%20-%20ops.json%20is%20the%20migration%20contract.md)): inline emit at the end of `migration plan` and self-emit by running `node <directory>/migration.ts` from any directory (see [ADR 193](../adrs/ADR%20193%20-%20Class-flow%20as%20the%20canonical%20migration%20authoring%20strategy.md) for the authoring strategy). Data migrations are authored as `dataTransform({ check, run })` operations whose pipelines invariant-guard the transition (see [ADR 176](../adrs/ADR%20176%20-%20Data%20migrations%20as%20invariant-guarded%20transitions.md)). See the [Migration System](7.%20Migration%20System.md) subsystem doc for the full canonical pipeline.

## At a glance

Expand Down Expand Up @@ -435,12 +435,14 @@ Both commands write into `migrations/<timestamp>_<slug>/`. The destination contr

The Mongo *target* (not the framework) owns the scaffolding pipeline end-to-end. The framework defines the shape of a migration package and the runner contract; everything family-specific — what factories exist, how the planner IR renders into a runnable file, what the diff produces — lives under `packages/3-mongo-target/` and `packages/2-mongo-family/`.

Concretely, the Mongo planner produces an `OpFactoryCall[]` IR (a discriminated union of frozen AST classes — `CreateIndexCall`, `DropIndexCall`, `CreateCollectionCall`, `DropCollectionCall`, `CollModCall`) and feeds it to two renderers: an operation renderer that calls the matching factory functions to materialize `MongoMigrationPlanOperation[]` for `ops.json`, and a TypeScript renderer that emits a `migration.ts` calling the same factories. Both outputs derive from the same IR, so re-running `./migration.ts` after editing it produces an `ops.json` that matches the planner's output for the unedited regions. See [ADR 195 — Planner IR with two renderers](../adrs/ADR%20195%20-%20Planner%20IR%20with%20two%20renderers.md) and [ADR 194 — Plans carry their own authoring surface](../adrs/ADR%20194%20-%20Plans%20carry%20their%20own%20authoring%20surface.md).
Concretely, the Mongo planner produces an `OpFactoryCall[]` IR (a discriminated union of frozen AST classes — `CreateIndexCall`, `DropIndexCall`, `CreateCollectionCall`, `DropCollectionCall`, `RenameCollectionCall`, `RenameFieldCall`, `UnsetFieldCall`, `CollModCall`) and feeds it to two renderers: an operation renderer that calls the matching factory functions to materialize `MongoMigrationPlanOperation[]` for `ops.json`, and a TypeScript renderer that emits a `migration.ts` calling the same factories. Both outputs derive from the same IR, so running it again after editing it (`node <directory>/migration.ts`) produces an `ops.json` that matches the planner's output for the unedited regions. See [ADR 195 — Planner IR with two renderers](../adrs/ADR%20195%20-%20Planner%20IR%20with%20two%20renderers.md) and [ADR 194 — Plans carry their own authoring surface](../adrs/ADR%20194%20-%20Plans%20carry%20their%20own%20authoring%20surface.md).

The factories the scaffold imports live in two layered modules:

- `@internal/family-mongo/migration` — the `Migration` base class (a `MongoMigration` subclass that `implements MigrationPlan`) plus `Migration.run(...)` for the shebang self-emit path ([ADR 196](../adrs/ADR%20196%20-%20In-process%20emit%20for%20class-flow%20targets.md)).
- `@internal/target-mongo/migration` — the operation factories (`createCollection`, `dropCollection`, `createIndex`, `dropIndex`, `collMod`, `setValidation`, `validatedCollection`, `dataTransform`) and the `placeholder(slot)` utility for unfilled scaffold slots.
- `@internal/target-mongo/migration` — the operation factories (`createCollection`, `dropCollection`, `renameCollection`, `createIndex`, `dropIndex`, `collMod`, `setValidation`, `validatedCollection`, `renameField`, `unsetField`, `dataTransform`) and the `placeholder(slot)` utility for unfilled scaffold slots.

`renameCollection(from, to)` renames a collection, keeping its documents, indexes and validator; it checks that the source exists and the target does not. `renameField(collection, from, to, { filter? })` and `unsetField(collection, field, { filter? })` rewrite the documents that hold the field with one `updateMany` (`$rename` or `$unset`) that skips validation; `filter` limits them to one variant's documents. They refuse an empty field name, `_id`, and a name containing `.` or starting with `$`. The planner emits all three for `--rename` and `--delete` statements; see [Subsystem 7 — Rename statements](7.%20Migration%20System.md#rename-statements).

A scaffolded `migration.ts` therefore looks like:

Expand Down Expand Up @@ -472,27 +474,20 @@ Non-nullable fields are added to `required`. Nullable fields are still listed in

### Polymorphic variants

For polymorphic models (`@@discriminator` on the base, `@@base` on each variant) using single-collection inheritance, the *intent* is that variants share the base model's collection and the validator becomes a discriminated `oneOf` over variant shapes (one branch per discriminator value, base fields hoisted to the top-level `properties`/`required`). This pattern is described in [Schema validation](#schema-validation-jsonschema) and reflects how the runtime interpreter already handles polymorphic indexes ([ADR 173](../adrs/ADR%20173%20-%20Polymorphism%20via%20discriminator%20and%20variants.md)).

**Known gaps in the migration planner today** (tracked as [TML-2447](https://linear.app/prisma-company/issue/TML-2447)):

- The planner emits standalone `createCollection` ops for variant models that should share the base's collection. Applying these would create unwanted empty collections; in example apps the migration artifacts are committed as-is and the variant ops have to be removed by hand before apply.
- Variant validators include only variant-specific fields and miss every base-model field (including `_id` and the discriminator). They are structurally wrong even when the variant collection is intentional.

Until these are addressed, polymorphic schemas may need post-scaffold edits to `migration.ts` (or to `ops.json` after self-emit) before the migration is safe to apply.
For polymorphic models (`@@discriminator` on the base, `@@base` on each variant) using single-collection inheritance, variants share the base model's collection, and the planner creates that one collection. Its validator is a discriminated `oneOf` over variant shapes: base fields at the top-level `properties`/`required`, and one branch per discriminator value that repeats the base fields, adds the variant's own fields, pins the discriminator value and closes with `additionalProperties: false`. This pattern is described in [Schema validation](#schema-validation-jsonschema) and matches how the runtime handles polymorphic indexes ([ADR 173](../adrs/ADR%20173%20-%20Polymorphism%20via%20discriminator%20and%20variants.md)). Every variant document holds its discriminator value, so a variant's field rename or removal touches only that variant's documents.

### Author-editable seams

`migration.ts` is the user-editable surface; `ops.json` is the contract the runner executes. The split is deliberate — see [ADR 192 — ops.json is the migration contract](../adrs/ADR%20192%20-%20ops.json%20is%20the%20migration%20contract.md). The maker workflow:

1. Run `migration plan` (or `migration new`). The planner fills in everything it can derive from the contract diff.
2. Open `migration.ts`. Replace any `placeholder(slot)` calls with real query plans (typically inside a `dataTransform({ check, run })` operation, where the planner can scaffold the structure but not the body). Hand-author additional operations as needed.
3. Re-emit by running the file directly: `./migration.ts` or `node migration.ts`. `Migration.run(...)` detects it is the main module and rewrites `ops.json` and the `migrationId` in `migration.json`. Unfilled `placeholder(...)` calls fail here with the structured error `PN-MIG-2001` rather than producing silent nonsense ([ADR 200](../adrs/ADR%20200%20-%20Placeholder%20utility%20for%20scaffolded%20migration%20slots.md)).
3. Re-emit by running the file with node from any directory: `node <directory>/migration.ts`. It loads the config `--config` names. Without `--config`, it uses the config that owns the migration, whose migrations directory holds the file: the nearest `prisma.config.ts` above the migration's directory if it owns it, else the `prisma.config.ts` in the working directory if that one does; otherwise it stops with `CLI.MIGRATION_CONFIG_NOT_FOUND`. `Migration.run(...)` detects it is the main module and rewrites `ops.json` and the `migrationId` in `migration.json`. Unfilled `placeholder(...)` calls fail here with the structured error `PN-MIG-2001` rather than producing silent nonsense ([ADR 200](../adrs/ADR%20200%20-%20Placeholder%20utility%20for%20scaffolded%20migration%20slots.md)).
4. Commit both files. CI / `migrate` reads `ops.json` only — TypeScript is never executed at apply time. Apply-time hash verification rehashes `(manifest, ops)` and aborts on mismatch, so post-emit hand edits to `ops.json` (or any FS corruption) are caught before the runner touches the database. See [Subsystem 7 — Hash verification](7.%20Migration%20System.md#hash-verification).

What the runner guarantees about the seam:

- **Round-trippable.** Re-running `./migration.ts` on an unedited scaffold produces byte-identical `ops.json` (the operation renderer and TypeScript renderer share the same IR).
- **Round-trippable.** Running `node <directory>/migration.ts` again on an unedited scaffold produces byte-identical `ops.json` (the operation renderer and TypeScript renderer share the same IR).
- **No drift between source and contract.** `migrationId` is content-addressed over `(manifest, ops)`, so any edit to either side that changes the canonical operations updates `migrationId` and is caught at apply time.
- **Hand-authored operations are first-class.** Operations added by the developer (e.g. extra `dataTransform` calls, custom `collMod` adjustments) flow through the same factories the planner uses; there is no "planner-only" vs "user-only" distinction in `ops.json`.

Expand Down
Loading
Loading