Skip to content

v8.0.0-rc.17-dev.32

@StevenMcClankerton StevenMcClankerton tagged this 13 Oct 11:55
## Linked issue

n/a — no Linear ticket; third and last PR for cross-namespace
inheritance (after #30577 and #30646).

## At a glance

```prisma
namespace a {
  model Task {
    id        Int    @id
    title     String
    type      String
    projectId Int?   @map("project_id")

    @@discriminator(type)
    @@map("tasks")
  }
}

namespace b {
  model Chore {
    effort  Int
    ownerId Int? @map("owner_id")

    @@base(a.Task, "chore")
    @@map("tasks")
  }

  model Epic {
    size Int

    @@base(a.Task, "b-epic")
    @@map("epics")
  }
}
```

```ts
const tasks = orm({ runtime, context }).a.Task;
const rows = await tasks.orderBy((task) => task.id.asc()).all();
```

The statement joins each variant table in its own namespace. `b.tasks`
has the same name as the base table `a.tasks`, so it gets an alias:

```sql
LEFT JOIN "b"."tasks" AS "tasks_2" ON "tasks"."id" = "tasks_2"."id"
"tasks_2"."effort" AS "tasks_2__effort"
```

Trimmed from
[`contract.prisma`](test/integration/test/sql-orm-client/fixtures/polymorphism-namespaces/contract.prisma)
and the first test in
[`polymorphism-namespaces.test.ts`](test/integration/test/sql-orm-client/polymorphism-namespaces.test.ts);
the SQL lines are the fragments that test asserts. Before this PR a
base's `variants` named each variant by model name only, so a variant in
another namespace could not be referenced, and the SQL ORM looked every
variant up in the base's namespace.

## Summary

A polymorphic base model can have multi-table variants in other
namespaces, including variants that share a model name or a table name
with something in another namespace. To make that possible, a base's
`variants` in the contract changes form: it is keyed by discriminator
value and each entry references the variant model by `{ namespace, model
}`.

## Decision

This PR ships:

1. **A new contract form for `variants`.** `"variants": { "bug": {
"namespace": "public", "model": "Bug" } }` replaces `"variants": {
"Bug": { "value": "bug" } }`, in `contract.json` and `contract.d.ts`,
for both families. The entry type is `CrossReference`;
`ContractVariantEntry` is removed. Nothing reads the previous form, and
a contract in that form fails validation. Every tracked contract,
fixture and current example migration snapshot is re-emitted.
2. **Every producer and consumer of that form.** Framework types and
domain validation, both structural schemas, SQL PSL, Mongo PSL, the
Mongo TS builder (its authoring input is unchanged), the emitter and the
Postgres PSL printer.
3. **Both ORMs identify a variant by its discriminator value.**
`variant(value)` is a direct `variants[value]` lookup. Collection state
and the variant type helpers hold the value where they held the model
name. `VariantNames`, `VariantNameForValue` and `variantsByValue` are
removed.
4. **The SQL ORM resolves each variant in the namespace its reference
names.** Storage lookups, joins and variant inserts use the variant's
own namespace and table.
5. **A new diagnostic in SQL PSL.**
`PSL_BASE_IN_OTHER_NAMESPACE_WITHOUT_MAP` rejects a variant in another
namespace that has no `@@map`.
6. **Docs and upgrade instructions.** [ADR
173](docs/architecture%20docs/adrs/ADR%20173%20-%20Polymorphism%20via%20discriminator%20and%20variants.md)
(examples and one decision paragraph), ADR 177, ADR 265, the
`CONTRACT.MODEL_VARIANT_MISSING` entry in the error reference, one
addition to the `prisma-8` contract skill reference, a pending upgrade
fragment `variants-keyed-by-discriminator-value` for apps and
extensions, and an update to the still-pending `collection-state-tables`
fragment.

## Notes for the reviewer

- **Where the lines are.** Of 4529 added and 1000 removed lines, emitted
artifacts are about 2530 added and 150 removed; 2160 of those are the
new fixture's `contract.json` and `contract.d.ts`, and the rest are
re-emitted contracts whose only change is `variants`. Hand-written
source is about 700 added and 570 removed across 32 files. Tests are
about 1060 added and 220 removed. Docs and upgrade instructions are
about 240 added and 60 removed.
- **About 400 of the changed source lines are a rename** in
`sql-orm-client/src`: `variantName` to `variantValue` in state and type
state, and `IncludeExpr.localVariantName` to `localVariantValue`. The
behaviour change in that package is concentrated in
[`collection-contract.ts`](packages/3-extensions/sql-orm-client/src/collection-contract.ts),
[`collection-tables.ts`](packages/3-extensions/sql-orm-client/src/collection-tables.ts)
and [`types.ts`](packages/3-extensions/sql-orm-client/src/types.ts).
- **Breaking for anyone with polymorphic models.** A `contract.json` in
the previous form is rejected. An existing migration snapshot in the
previous form makes `prisma migration plan --from <hash>` fail with
`CONTRACT.VALIDATION_FAILED`. No CLI command regenerates a snapshot; the
app upgrade fragment gives the rewrite rule for `contract.json` and
`contract.d.ts`. The storage hash does not change, so no migration is
needed.
- **Four superseded example snapshots stay in the previous form:** two
in `examples/prisma-8-demo` and two in `examples/retail-store`. The
validator already refused them before this PR and nothing loads them.
The planner-golden manifest records the new refusal for the two demo
snapshots.
- **Domain validation no longer checks for duplicate discriminator
values.** A map keyed by value cannot repeat one. SQL PSL, Mongo PSL and
the Mongo TS builder still report two variants declaring the same value.
Domain validation does reject a base that lists the same variant model
under two values.
- **The Postgres PSL printer refuses the new fixture** because the model
name `Doc` appears in namespaces `b` and `c`. That refusal exists on
`main`; the round-trip test lists the fixture with it.
- **Custom collection classes still require a single-namespace
contract.**
- **The new tests work around four gaps** that exist on `main` for
hierarchies in one namespace and are not changed here. They are listed
under Follow-ups.

## How it fits together

1. **Change the representation.** `ContractModelBase.variants` is
`Record<string, CrossReference>`. Domain validation follows each entry's
reference to the variant model and checks that its `base` is the
declaring model's coordinate, and checks the reverse direction by
coordinate too
([`domain-types.ts`](packages/1-framework/0-foundation/contract/src/domain-types.ts),
[`validate-domain.ts`](packages/1-framework/0-foundation/contract/src/validate-domain.ts)).
Both structural schemas require each entry to be a `CrossReference`; the
SQL schema also gives `discriminator` the shape `{ field: string }`, and
`data-contract-sql-v1.json` is regenerated ([SQL
`validators.ts`](packages/2-sql/1-core/contract/src/validators.ts),
[Mongo
`contract-schema.ts`](packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts)).
2. **Write it from every authoring source.** SQL PSL writes the
variant's own namespace into the reference and names models with their
namespace in diagnostics when a hierarchy spans namespaces ([SQL
`interpreter.ts`](packages/2-sql/2-authoring/contract-psl/src/interpreter.ts)).
Mongo PSL and the Mongo TS builder write the same form with the single
Mongo namespace; the builder still takes `variants: { Bug: { value:
'bug' } }` ([Mongo
`interpreter.ts`](packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts),
[`contract-builder.ts`](packages/2-mongo-family/2-authoring/contract-ts/src/contract-builder.ts)).
3. **Emit and print it.** `contract.d.ts` emits each entry as a
`CrossReference` under its value key, takes the discriminator literal
union from the keys, and finds `Any<Base>` members through the
references
([`model-types-emission.ts`](packages/1-framework/3-tooling/emitter/src/model-types-emission.ts),
[`domain-type-generation.ts`](packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts)).
The Postgres PSL printer finds a variant's value by coordinate and
prints `@@base(ns.Base, "value")` when the base is in another namespace
([`model-attributes.ts`](packages/3-targets/3-targets/postgres/src/core/psl-print/model-attributes.ts)).
4. **Identify a variant by value in both ORMs.** `variant(value)` reads
`variants[value]`. SQL collection state, `CollectionTables.variants`,
the variant an include names for its local columns, and the fourth type
parameter of `MongoCollection` all hold the value. `VariantModelRow`,
`VariantCreateInput` and the model accessor take the value and read the
variant's model and namespace from the reference ([SQL
`types.ts`](packages/3-extensions/sql-orm-client/src/types.ts), [Mongo
`collection.ts`](packages/2-mongo-family/5-query-builders/orm/src/collection.ts),
[Mongo
`types.ts`](packages/2-mongo-family/5-query-builders/orm/src/types.ts)).
5. **Resolve SQL variants where their references point.**
`resolvePolymorphismInfo` records each variant's namespace and table. A
variant is single-table only when both equal the base's; otherwise it is
multi-table. Each variant table is aliased at its own storage coordinate
through the table scope added in #30646, which is what gives `b.tasks`
the alias `tasks_2` beside `a.tasks`
([`collection-contract.ts`](packages/3-extensions/sql-orm-client/src/collection-contract.ts),
[`collection-tables.ts`](packages/3-extensions/sql-orm-client/src/collection-tables.ts)).
6. **Prove it against a database.** The new PSL fixture
`polymorphism-namespaces` has a same-namespace single-table variant, a
same-namespace multi-table variant, a variant in another namespace whose
table has the base table's name, two variants named `Epic` mapped to
`epics` in two namespaces, and a relation declared on a variant. It has
12 database tests and 4 type tests.

## Behavior changes & evidence

- **`variants` in `contract.json` and `contract.d.ts` is keyed by
discriminator value and each entry is `{ namespace, model }`; the
previous form is rejected.** Implementation:
[`domain-types.ts`](packages/1-framework/0-foundation/contract/src/domain-types.ts),
[SQL `validators.ts`](packages/2-sql/1-core/contract/src/validators.ts),
[Mongo
`contract-schema.ts`](packages/2-mongo-family/1-foundation/mongo-contract/src/contract-schema.ts).
Evidence:
[`validators.test.ts`](packages/2-sql/1-core/contract/test/validators.test.ts),
[`variant-schema.test.ts`](packages/2-mongo-family/1-foundation/mongo-contract/test/variant-schema.test.ts).
- **A variant may be in a different namespace from its base, and domain
validation matches base and variant by coordinate.** Implementation:
[`validate-domain.ts`](packages/1-framework/0-foundation/contract/src/validate-domain.ts).
Evidence:
[`validate-domain.test.ts`](packages/1-framework/0-foundation/contract/test/validate-domain.test.ts).
- **SQL PSL accepts `@@base(a.Task, "chore")` from another namespace and
rejects it without `@@map` with
`PSL_BASE_IN_OTHER_NAMESPACE_WITHOUT_MAP`.** Two variants named alike in
different namespaces no longer overwrite each other in `variants`.
Implementation: [SQL
`interpreter.ts`](packages/2-sql/2-authoring/contract-psl/src/interpreter.ts).
Evidence:
[`interpreter.polymorphism.test.ts`](packages/2-sql/2-authoring/contract-psl/test/interpreter.polymorphism.test.ts).
- **The SQL ORM reads, filters, includes, aggregates and writes variants
in other namespaces.** `create` and `createAll` insert the variant row
into the table in the variant's namespace. Implementation:
[`collection-contract.ts`](packages/3-extensions/sql-orm-client/src/collection-contract.ts),
[`collection-tables.ts`](packages/3-extensions/sql-orm-client/src/collection-tables.ts),
[`query-plan-select.ts`](packages/3-extensions/sql-orm-client/src/query-plan-select.ts).
Evidence:
[`polymorphism-namespaces.test.ts`](test/integration/test/sql-orm-client/polymorphism-namespaces.test.ts).
- **SQL `variant()` accepts only the values declared by the base in the
collection's own namespace, and two variants with the same model name
have separate row and create-input types.** Implementation:
[`collection.ts`](packages/3-extensions/sql-orm-client/src/collection.ts),
[`types.ts`](packages/3-extensions/sql-orm-client/src/types.ts).
Evidence:
[`polymorphism-namespaces.test-d.ts`](test/integration/test/sql-orm-client/polymorphism-namespaces.test-d.ts),
[`polymorphism.test-d.ts`](packages/3-extensions/sql-orm-client/test/polymorphism.test-d.ts).
- **ORM type helpers and state fields take a discriminator value.**
`VariantModelRow<C, 'Task', 'bug'>` replaces `VariantModelRow<C, 'Task',
'Bug'>`; `VariantNames` and `VariantNameForValue` are no longer
exported; `CollectionState.variantName` is `variantValue`. Mongo
behaviour is otherwise unchanged. Implementation: [SQL
`types.ts`](packages/3-extensions/sql-orm-client/src/types.ts), [Mongo
`types.ts`](packages/2-mongo-family/5-query-builders/orm/src/types.ts),
[Mongo
`exports/index.ts`](packages/2-mongo-family/5-query-builders/orm/src/exports/index.ts).
Evidence:
[`orm-types.test-d.ts`](packages/2-mongo-family/5-query-builders/orm/test/orm-types.test-d.ts),
[`collection-variant.test.ts`](packages/3-extensions/sql-orm-client/test/collection-variant.test.ts).
- **The Postgres PSL printer prints a base in another namespace as
`@@base(ns.Base, "value")`.** Implementation:
[`model-attributes.ts`](packages/3-targets/3-targets/postgres/src/core/psl-print/model-attributes.ts),
[`contract-model-index.ts`](packages/3-targets/3-targets/postgres/src/core/psl-print/contract-model-index.ts).
Evidence:
[`refusals-models.test.ts`](packages/3-targets/3-targets/postgres/test/psl-print/refusals-models.test.ts).
- **Upgrade instructions.**
[`app/instructions.md`](upgrade-instructions/pending/variants-keyed-by-discriminator-value/app/instructions.md),
[`extension/instructions.md`](upgrade-instructions/pending/variants-keyed-by-discriminator-value/extension/instructions.md).

## Testing performed

On the branch with the latest `main` merged:

- root `pnpm typecheck`: all packages but `prisma7-adoption`, which
fails locally on an engine download (known);
- `pnpm test:packages`: 25338 passed; 3 tarball tests fail locally with
`ERR_PNPM_TRUST_DOWNGRADE` (known);
- integration typecheck and `test/sql-orm-client` (122 files, 878
tests);
- `pnpm lint:deps`, `pnpm fixtures:check`, `pnpm
check:upgrade-coverage`.

Before the last three small commits, also the targeted integration runs
`test/planner-golden`, `test/mongo`, `test/authoring`,
`test/upgrade-instructions`, `test/psl-print`, CLI emit and prepared
polymorphism (with `test/sql-orm-client`: 312 files, 3620 tests).

The full `pnpm test:integration` is blocked by a local hook. CI is the
full run.

## Skill update


[`skills/prisma-8/references/contract.md`](skills/prisma-8/references/contract.md),
polymorphism section: a base in another namespace is written
`@@base(ns.Model, "value")`, and such a variant must set `@@map`. The
query references already describe `variant('<discriminator value>')`
since #30577.

## Follow-ups

These exist on `main` for hierarchies in one namespace and are not
changed here:

- Single-row `update()` and `delete()` on a multi-table variant return
only the base table's fields.
- `aggregate()` cannot reduce over a variant's field.
- `select()` types do not accept a variant's field names, although they
are read at run time.
- `select()` is not scoped to the collection's namespace when two models
in a contract share a name.

Also open: custom collection classes on a multi-namespace contract.

## Alternatives considered

- **Keep `variants` keyed by model name and add a namespace to each
entry.** A model name is not unique across namespaces, so two variants
named `Epic` in `b` and `c` would need one key. A discriminator value is
unique in a hierarchy, and it is what `variant()` already takes since
#30577, so the lookup becomes a key access.
- **Read the previous form, or rewrite it on load.** Not done: the
repo's rule is no backward-compatibility code, and the rewrite is
mechanical enough to give as an upgrade instruction.
- **Allow a single-table variant in another namespace.** A table belongs
to one namespace, so a variant in `b` cannot store its fields in
`a.tasks` while being a model of `b`. PSL reports
`PSL_BASE_IN_OTHER_NAMESPACE_WITHOUT_MAP` and asks for `@@map`.
- **Change the Mongo TS builder's `variants` input to the value-keyed
form.** Not done: Mongo has one namespace, so the model-keyed input is
unambiguous, and the builder writes the contract form from it without a
change for its users.

## Checklist

- [x] All commits are signed off (`git commit -s`) per the
[DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO
status check will block merge if any commit is missing a
`Signed-off-by:` trailer.
- [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is
scoped to one logical concern.
- [x] Tests are updated (or `n/a` if the change is doc-only / refactor
with no behavioural delta).
- [ ] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear
ticket prefix + concise title naming the concrete deliverable). See
`.claude/skills/create-pr/SKILL.md` for the full convention. — Not
ticked: there is no Linear ticket, so the title has no `TML-` prefix.
- [x] The **Skill update** section above is filled in (or stated `n/a —
internal only`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Features**
* Polymorphic variants are identified by discriminator values and can
reference models in different namespaces. SQL and MongoDB ORM
operations—including filtering, selection, relations, and
mutations—support this mapping.
* **Documentation**
* Updated architecture references and Prisma guidance describe
cross-namespace variants and their storage requirements.
* **Upgrade Notes**
* Existing contracts using the previous variant format must be
regenerated, with matching migration snapshots updated.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Steven McClankerton <tatarintsev@prisma.io>
Co-authored-by: Steven McClankerton <tatarintsev@prisma.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Assets 2
Loading