## Linked issue
n/a. Third slice of the PSL mixins project (`projects/psl-mixins/`); the
syntax landed in #30675. This replaces #30678, which resolved inclusion
names in the symbol table.
## At a glance
```prisma
model mixin Timestamps {
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
@@index([createdAt])
}
enum mixin BaseRoles {
ADMIN = "admin"
USER = "user"
}
model User {
id Int @id
+Timestamps
email String
}
enum Role {
@@type("pg/text@1")
+BaseRoles
GUEST = "guest"
}
```
`User` has the fields `id`, `createdAt`, `updatedAt`, `email`, in that
order, and the index on `createdAt`. `Role` has `ADMIN`, `USER`,
`GUEST`. The emitted contract is the one the same schema gives with the
members written inline. Before this PR both forms parsed and were
rejected with "Mixins are not supported yet".
## Summary
Mixins now work. The binder resolves each inclusion's name, and the
mixin's members are then placed in the enclosing block's symbol, so the
interpreters read an ordinary block. This holds for models, composite
types, enums and extension blocks such as row-level security policies.
## Decision
1. **The symbol table records what is written.** It gains mixin symbols;
a model, composite type or block symbol holds only the members written
in it. The symbol table resolves no name.
2. **The binder resolves an inclusion's name like any other name,** in
its first pass, through the scope chain and qualified-name code that
type references use: the including block's namespace, then the top
level, and a nearer declaration of any kind hides an outer one. Whatever
it finds that is not a mixin is reported as `Cannot find mixin "X"`.
3. **Placement is one function, called between the binder's two
passes.** For each block with a resolved inclusion it rebuilds the
member records in source order, adding the mixin's fields or entries and
its attributes at the inclusion's position. It fills the records of the
existing symbols, so a symbol handed out earlier shows the placed
members. The members are the mixin's own symbols and nodes, shared by
every including block.
4. **A mixin body is bound once, where it is written.** In its second
pass the binder resolves a mixin's field types in the mixin's namespace
and the field names in its attributes against the mixin's own fields. An
including block binds only the members written in it; its own attributes
see the placed fields.
5. **Errors about an inclusion are reported at the inclusion.** From the
binder: a name that is not a mixin, a mixin for a different block
keyword, an inclusion inside a mixin. From placement: the same mixin
included twice, and a member the block already has.
6. **The binder passes the mixin itself as the owner** of a member
written in a mixin. `AttributeSpecContext.model`,
`UnsupportedAttribute.owner` and `UnresolvedTypeReference.owner` accept
a `MixinSymbol`, and the SQL and Mongo unsupported-attribute messages
say `Mixin "<name>"` for one.
7. **A mixin is not a type.** A field typed with a mixin's name is an
unresolved reference.
8. **The interpreters for Prisma 7 and Prisma 6 schemas reject mixins.**
The parser reads mixins under every grammar (#30675), so those
interpreters report a mixin declaration (`Unsupported top-level block
"model mixin"`) and an inclusion (`Unsupported mixin inclusion
"+Timestamps"`).
## Reviewer notes
- **Own versus placed members.** The second pass runs after placement,
so the binder treats a member as a block's own when its syntax node's
parent is the block's node (`isWrittenIn` in
[`binder.ts`](packages/1-framework/2-authoring/psl-parser/src/binder.ts)).
No symbol shape carries this, so interpreters, attribute specs and block
specs have no way to ask whether a member came from a mixin.
- **Changes in the default-grammar interpreters are four files**, each a
compile fix or the wording in decision 4:
[`psl-column-resolution.ts`](packages/2-sql/2-authoring/contract-psl/src/psl-column-resolution.ts)
(one `case`),
[`psl-field-resolution.ts`](packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts)
(the word, and nothing for a `type` mixin as for a composite type),
[`sql-attribute-specs.ts`](packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts)
(a parameter type),
[`mongo-attribute-specs.ts`](packages/2-mongo-family/2-authoring/contract-psl/src/mongo-attribute-specs.ts)
(the word).
- **Creating a binder completes the symbol table.** A block's member
records are final only after `createBinder` returns. Every production
path builds the table and the binder together; the Prisma 7 and Prisma 6
interpreters create no binder, and they reject mixins. The parser
README's binder section gains one sentence saying this.
- **The duplicate-name test exists twice.** The symbol table checks the
fields written in a block; placement checks a mixin's members against
what the block already has, and reports a block's own field that follows
an inclusion which provided its name, reusing the symbol table's message
and location code.
- **Placement assigns new member records to the existing symbol.**
`fields` and `attributes` on model and composite type symbols, and
`entries` and `attributes` on block symbols, are assignable for this;
the symbol object keeps its identity, so a symbol the binder handed out
earlier shows the placed members.
- **`members()` on the block AST classes returns inclusions too,** in
source order with fields or entries and block attributes. Its only other
production callers are the language server's semantic-token walkers,
which skip an inclusion in this PR.
- **Which duplicate is reported.** First wins, as before. If a model's
own field comes after an inclusion that provided the name, the existing
"Duplicate declaration" is reported at the field. If the inclusion comes
second, the error is at the inclusion and names the mixin and the
member.
- **Entry keys are checked in the symbol table too.** Block-spec
interpretation rejects a repeated key in both struct and map mode, so a
repeated key is an error for every generic block; dropping the mixin's
duplicate here keeps that later check from reporting it again. A block's
own repeated keys still reach the later check unchanged.
- **Attributes are not checked for duplicates here.** Whether two
`@@index` or two `@@map` are allowed is each interpreter's rule, and
those errors are reported at the attribute, inside the mixin.
- **An interpreter error on a mixin's member is reported once per
including block**, at the member in the mixin. A test pins this.
De-duplication is out of scope.
- **Not in this PR:** go-to-definition, hover, rename and completion for
mixins, and semantic tokens inside a mixin body. The binder records what
an inclusion refers to; the language server answers for it in the next
slice.
## Behavior changes & evidence
- A schema using a mixin gives the contract of the inline schema, for a
model, a composite type, an enum and an extension block. Members are
placed at the inclusion's position; an enum's member list in the
contract follows it, while a model's fields and a table's columns are
sorted in the emitted contract either way.
[`symbol-table.ts`](packages/1-framework/2-authoring/psl-parser/src/symbol-table.ts);
[`interpreter.mixins.test.ts`](packages/2-sql/2-authoring/contract-psl/test/interpreter.mixins.test.ts)
(SQL),
[`interpreter.mixins.test.ts`](packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.mixins.test.ts)
(Mongo).
- A `policy_select` mixin holding `roles` and `using`, included by two
policies with different targets, gives the contract of the policies
written in full.
[`psl-policy-mixin.test.ts`](packages/3-targets/3-targets/postgres/test/psl-policy-mixin.test.ts).
- A mixin in namespace `auth` with `owner User`, included in a model in
`billing` that has its own `User`, relates to `auth.User`.
[`binder.ts`](packages/1-framework/2-authoring/psl-parser/src/binder.ts);
[`binder.mixin.test.ts`](packages/1-framework/2-authoring/psl-parser/test/binder.mixin.test.ts)
and the SQL contract test above.
- Each inclusion error has a test asserting code, message, file and
range.
[`mixin-placement.test.ts`](packages/1-framework/2-authoring/psl-parser/test/mixin-placement.test.ts).
- One mixin with an error in its body, included by three models, reports
each binder diagnostic once; a mixin nothing includes is still bound.
[`binder.mixin.test.ts`](packages/1-framework/2-authoring/psl-parser/test/binder.mixin.test.ts).
- Type completion does not offer mixin names.
[`completion-scope.ts`](packages/1-framework/3-tooling/language-server/src/completion-scope.ts).
## Testing performed
- `pnpm test:packages`: 1761 of 1765 files pass.
- `pnpm typecheck`: 170 of 171 tasks pass.
- `pnpm lint:deps`, `pnpm fixtures:check` (no fixture changed), `pnpm
lint:skills`, and `lint` for `psl-parser`, `language-server`, the SQL
and Mongo `contract-psl`, the Prisma 7 and Prisma 6 contract sources,
and the Postgres target: pass.
- Local failures: `prisma7-adoption#typecheck` and three tarball-install
tests, which fail on my machine and pass in CI, plus the CLI telemetry
end-to-end test, which timed out at 5 s under the full run and passes
when run alone.
## Skill update
`skills/prisma-8/references/contract.md` gains "Workflow — Mixins
(shared members)": the example above and the rules (one keyword per
mixin, a member provided twice is an error, a mixin's attributes can
name only its own fields, a mixin cannot include a mixin, names follow
namespaces, a mixin is not a type).
## Alternatives considered
- **Resolving names in a mixin where it is included.** A mixin's
`@@unique([tenantId, id])` could then use the including model's `id`.
The binder stores one resolution per syntax node, so one node would need
several meanings, a mixin could not be checked on its own, and hover
inside a mixin would have no single answer.
- **Copying members per including block**, so interpreter errors could
point at the inclusion. Members would stop being shared symbols, which
rename and go-to-definition across including blocks rely on, and errors
that are the mixin's own fault would move away from it.
- **Building a model-shaped object around a mixin** to pass to spec
factories and the family callbacks without widening their types. The
object would claim to be a model that is in no symbol record.
- **Resolving inclusion names in the symbol table** (#30678). The symbol
table needed its own scope chain and had to register every symbol before
filling any, and names were resolved by two components.
- **Placing members after all binding.** A block's own attributes can
name a mixed-in field, so the binder would have to work out a block's
placed fields before placement had run.
- **Letting interpreters expand inclusions.** Two family interpreters,
two legacy-schema interpreters and the language server would each need
the logic.
## Checklist
- [x] All commits are signed off (`git commit -s`) per the
[DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco).
- [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. No
Linear ticket is used for this project.
- [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
* **New Features**
* Added support for defining and including PSL mixins across models,
composite types, enums, key-value blocks, and policies. Included members
appear at the point of inclusion, and mixin names resolve across
namespaces.
* SQL and MongoDB contracts interpret supported mixin members, including
fields and attributes.
* **Bug Fixes**
* Improved diagnostics for invalid, unresolved, duplicate, and
unsupported mixins and mixin members.
* Prisma 6 and Prisma 7 contracts report mixins as unsupported.
* **Documentation**
* Added guidance and examples for declaring and using mixins.
<!-- 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>