prisma/prisma · error
PSL_AMBIGUOUS_BACKRELATION
PSL_AMBIGUOUS_BACKRELATION
Error message
Backrelation list field "${candidate.modelName}.${candidate.field.name}" matches multiple junction FK pairs for a many-to-many relation. Add @relation(name: "...") (or @relation("...")) to the list field and the junction FK-side relation pointing back at "${candidate.modelName}" to disambiguate. What it means
Emitted when a list-typed backrelation field (e.g. `posts Post[]` with no `fields/references`) is being resolved as an implicit many-to-many relation, and the resolver finds more than one junction-shaped model that could link the candidate's model to its target. Because each candidate junction is a valid M:N bridge, the library refuses to guess and demands an explicit relation name to pin the parent-side FK. It is a hard authoring failure: the contract cannot be emitted until the ambiguity is resolved.
Source
Thrown at packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts:493
if (matches.length === 0) {
// A singular candidate is the back side of a 1:1 — many-to-many junction
// matching only makes sense for a list-typed backrelation.
if (candidate.isList) {
const { pairs: junctionPairs, nearMisses } = findJunctionFkPairs({
candidate,
fkRelationsByDeclaringModel: input.fkRelationsByDeclaringModel,
modelIdColumns: input.modelIdColumns,
});
const junctionPair = junctionPairs[0];
if (junctionPairs.length === 1 && junctionPair) {
relationsForModel(input.modelRelations, candidate.modelName).push(
manyToManyRelationNode(candidate, junctionPair),
);
continue;
}
if (junctionPairs.length > 1) {
input.diagnostics.push({
code: 'PSL_AMBIGUOUS_BACKRELATION',
message: `Backrelation list field "${candidate.modelName}.${candidate.field.name}" matches multiple junction FK pairs for a many-to-many relation. Add @relation(name: "...") (or @relation("...")) to the list field and the junction FK-side relation pointing back at "${candidate.modelName}" to disambiguate.`,
sourceId: input.sourceId,
span: candidate.field.span,
});
continue;
}
const nearMiss = nearMisses[0];
if (nearMiss) {
input.diagnostics.push(junctionNearMissDiagnostic(candidate, nearMiss, input.sourceId));
continue;
}
}
input.diagnostics.push({
code: 'PSL_ORPHANED_BACKRELATION',
message: `Backrelation field "${candidate.modelName}.${candidate.field.name}" has no matching FK-side relation on model "${candidate.targetModelName}". Add @relation(fields: [...], references: [...]) on the FK-side relation${candidate.isList ? ' or use an explicit join model for many-to-many' : ''}.`,
sourceId: input.sourceId,
span: candidate.field.span,
});View on GitHub (pinned to 1243cdffbe)
Solutions
- Add the same `@relation("MyRelation")` (or `@relation(name: "MyRelation")`) to the list backrelation field on the parent model AND to the junction model's FK-side relation that points back at the parent (`candidate.modelName`).
- If you did not intend two junctions, drop or rename the duplicate join model so only one qualifies.
- If both junctions are legitimate, give each list field a distinct relation name so each resolves to exactly one junction pair.
- As a last resort, model the many-to-many explicitly with `rel.manyToMany(...)` instead of relying on implicit junction detection.
Example fix
// before
model User {
id Int @id
posts Post[]
}
model Post {
id Int @id
// two junction-shaped models both qualify
}
model UserPostLikes { user User[] @relation(fields: [userId], references: [id]) ... }
model UserPostViews { user User[] @relation(fields: [userId], references: [id]) ... }
// after — name the parent side of the chosen junction
model User {
id Int @id
likedPosts Post[] @relation("UserPostLikes")
} Defensive patterns
Strategy: validation
Validate before calling
// After contract resolution, assert no ambiguous many-to-many backrelation diagnostics.
// `diagnostics` is the array the authoring API populates during PSL resolution.
import type { ContractSourceDiagnostic } from '@prisma-next/contract/types';
const AMBIGUOUS_M2M = 'PSL_AMBIGUOUS_BACKRELATION';
function findAmbiguousM2MBackrelations(diagnostics: readonly ContractSourceDiagnostic[]): ContractSourceDiagnostic[] {
return diagnostics.filter((d) => d.code === AMBIGUOUS_M2M && d.message.includes('matches multiple junction FK pairs'));
}
// Call after the authoring pass that runs relation resolution:
const ambiguous = findAmbiguousM2MBackrelations(diagnostics);
if (ambiguous.length > 0) {
throw new Error('Ambiguous many-to-many backrelation(s); add @relation(name) to disambiguate:\n' + ambiguous.map((d) => d.message).join('\n'));
} Type guard
function isAmbiguousBackrelationDiagnostic(d: unknown): d is ContractSourceDiagnostic & { code: 'PSL_AMBIGUOUS_BACKRELATION' } {
return typeof d === 'object' && d !== null && (d as ContractSourceDiagnostic).code === 'PSL_AMBIGUOUS_BACKRELATION';
} Try / catch
// Diagnostics are collected, not thrown. Do NOT wrap resolution in try/catch expecting this error;
// instead read the diagnostics array returned/Populated by the authoring API.
// If a downstream step (e.g. contract emit) throws on error diagnostics, catch and surface:
try {
emitContract(resolvedContract);
} catch (err) {
if (ambiguous.length > 0) {
throw new Error('Emit blocked: ' + ambiguous.map((d) => d.message).join('; '));
}
throw err;
} Prevention
- For every many-to-many modeled via an implicit junction, give the list backrelation and the junction FK-side relation pointing back at the parent a matching @relation(name: "...") (or @relation("...")) so the resolver picks one junction pair.
- Prefer an explicit join model with two named belongsTo relations when a model participates in more than one many-to-many to the same peer — names then become unambiguous.
- After authoring, run a diagnostics check in CI that fails on any 'PSL_AMBIGUOUS_BACKRELATION' code before emitting contract.json.
When it happens
Trigger: Reached `applyBackrelationCandidates` with `candidate.isList === true`, `matches.length === 0` on the direct FK pair, and `findJunctionFkPairs` returning `junctionPairs.length > 1`. Concretely: two or more models each have an `@@id` that is exactly the concatenation of an FK back to the candidate's model and an FK to the candidate's target model, and the list field carries no `@relation(name: "...")`.
Common situations: Adding a second join model between the same two models (e.g. `UserPostLikes` alongside `UserPostViews`) without naming either side. Self-referential many-to-many (e.g. `User friends User[]` via a `Friendship` join model that has two FKs to `User`) where neither FK carries a relation name. Copy-pasting a join model to variant it and forgetting to qualify the new relation.
Related errors
- PSL_ORPHANED_BACKRELATION
- PSL_AMBIGUOUS_BACKRELATION
- PSL_JUNCTION_TARGET_FK_NOT_ID
- PSL_JUNCTION_ID_NOT_FK_COVERING
- PSL_ORPHANED_BACKRELATION
AI-assisted analysis of prisma/prisma@1243cdffbe (2026-08-01).
Data as JSON: /data/errors/1db907a92940601c.json.
Report an issue: GitHub.