{"id":"1db907a92940601c","repo":"prisma/prisma","slug":"psl-ambiguous-backrelation-1db907","errorCode":"PSL_AMBIGUOUS_BACKRELATION","errorMessage":"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.","messagePattern":"Backrelation list field \"(.+?)\\.(.+?)\" 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 \"(.+?)\" to disambiguate\\.","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts","lineNumber":493,"sourceCode":"    if (matches.length === 0) {\n      // A singular candidate is the back side of a 1:1 — many-to-many junction\n      // matching only makes sense for a list-typed backrelation.\n      if (candidate.isList) {\n        const { pairs: junctionPairs, nearMisses } = findJunctionFkPairs({\n          candidate,\n          fkRelationsByDeclaringModel: input.fkRelationsByDeclaringModel,\n          modelIdColumns: input.modelIdColumns,\n        });\n        const junctionPair = junctionPairs[0];\n        if (junctionPairs.length === 1 && junctionPair) {\n          relationsForModel(input.modelRelations, candidate.modelName).push(\n            manyToManyRelationNode(candidate, junctionPair),\n          );\n          continue;\n        }\n        if (junctionPairs.length > 1) {\n          input.diagnostics.push({\n            code: 'PSL_AMBIGUOUS_BACKRELATION',\n            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.`,\n            sourceId: input.sourceId,\n            span: candidate.field.span,\n          });\n          continue;\n        }\n        const nearMiss = nearMisses[0];\n        if (nearMiss) {\n          input.diagnostics.push(junctionNearMissDiagnostic(candidate, nearMiss, input.sourceId));\n          continue;\n        }\n      }\n      input.diagnostics.push({\n        code: 'PSL_ORPHANED_BACKRELATION',\n        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' : ''}.`,\n        sourceId: input.sourceId,\n        span: candidate.field.span,\n      });","sourceCodeStart":475,"sourceCodeEnd":511,"githubUrl":"https://github.com/prisma/prisma/blob/1243cdffbec0506c595ac8fd5865fe258c336fa0/packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts#L475-L511","documentation":"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.","triggerScenarios":"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: \"...\")`.","commonSituations":"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.","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."],"exampleFix":"// before\nmodel User {\n  id       Int     @id\n  posts    Post[]\n}\nmodel Post {\n  id      Int    @id\n  // two junction-shaped models both qualify\n}\nmodel UserPostLikes  { user User[] @relation(fields: [userId], references: [id]) ... }\nmodel UserPostViews  { user User[] @relation(fields: [userId], references: [id]) ... }\n\n// after — name the parent side of the chosen junction\nmodel User {\n  id          Int      @id\n  likedPosts  Post[]   @relation(\"UserPostLikes\")\n}","handlingStrategy":"validation","validationCode":"// After contract resolution, assert no ambiguous many-to-many backrelation diagnostics.\n// `diagnostics` is the array the authoring API populates during PSL resolution.\nimport type { ContractSourceDiagnostic } from '@prisma-next/contract/types';\n\nconst AMBIGUOUS_M2M = 'PSL_AMBIGUOUS_BACKRELATION';\n\nfunction findAmbiguousM2MBackrelations(diagnostics: readonly ContractSourceDiagnostic[]): ContractSourceDiagnostic[] {\n  return diagnostics.filter((d) => d.code === AMBIGUOUS_M2M && d.message.includes('matches multiple junction FK pairs'));\n}\n\n// Call after the authoring pass that runs relation resolution:\nconst ambiguous = findAmbiguousM2MBackrelations(diagnostics);\nif (ambiguous.length > 0) {\n  throw new Error('Ambiguous many-to-many backrelation(s); add @relation(name) to disambiguate:\\n' + ambiguous.map((d) => d.message).join('\\n'));\n}","typeGuard":"function isAmbiguousBackrelationDiagnostic(d: unknown): d is ContractSourceDiagnostic & { code: 'PSL_AMBIGUOUS_BACKRELATION' } {\n  return typeof d === 'object' && d !== null && (d as ContractSourceDiagnostic).code === 'PSL_AMBIGUOUS_BACKRELATION';\n}","tryCatchPattern":"// Diagnostics are collected, not thrown. Do NOT wrap resolution in try/catch expecting this error;\n// instead read the diagnostics array returned/Populated by the authoring API.\n// If a downstream step (e.g. contract emit) throws on error diagnostics, catch and surface:\ntry {\n  emitContract(resolvedContract);\n} catch (err) {\n  if (ambiguous.length > 0) {\n    throw new Error('Emit blocked: ' + ambiguous.map((d) => d.message).join('; '));\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["psl","relations","many-to-many","junction","schema"],"analyzedSha":"1243cdffbec0506c595ac8fd5865fe258c336fa0","analyzedAt":"2026-08-01T19:42:02.087Z","schemaVersion":2}