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

  1. 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`).
  2. If you did not intend two junctions, drop or rename the duplicate join model so only one qualifies.
  3. If both junctions are legitimate, give each list field a distinct relation name so each resolves to exactly one junction pair.
  4. 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

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


AI-assisted analysis of prisma/prisma@1243cdffbe (2026-08-01). Data as JSON: /data/errors/1db907a92940601c.json. Report an issue: GitHub.