{"id":"9601f0f52415ad62","repo":"prisma/prisma","slug":"psl-junction-target-fk-not-id","errorCode":"PSL_JUNCTION_TARGET_FK_NOT_ID","errorMessage":"Backrelation list field \"${candidate.modelName}.${candidate.field.name}\" found junction model \"${nearMiss.junctionModelName}\", but its foreign key to \"${candidate.targetModelName}\" does not reference \"${candidate.targetModelName}\"'s @id. The junction's target-side foreign key must reference \"${candidate.targetModelName}\"'s full @id columns for many-to-many recognition.","messagePattern":"Backrelation list field \"(.+?)\\.(.+?)\" found junction model \"(.+?)\", but its foreign key to \"(.+?)\" does not reference \"(.+?)\"'s @id\\. The junction's target-side foreign key must reference \"(.+?)\"'s full @id columns for many-to-many recognition\\.","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts","lineNumber":387,"sourceCode":"    }\n  }\n  return { pairs, nearMisses };\n}\n\nfunction junctionNearMissDiagnostic(\n  candidate: ModelBackrelationCandidate,\n  nearMiss: JunctionNearMiss,\n  sourceId: string,\n): ContractSourceDiagnostic {\n  const listField = `${candidate.modelName}.${candidate.field.name}`;\n  const data = {\n    listField,\n    junctionModel: nearMiss.junctionModelName,\n    targetModel: candidate.targetModelName,\n  };\n  if (nearMiss.reason === 'target-fk-not-id') {\n    return {\n      code: 'PSL_JUNCTION_TARGET_FK_NOT_ID',\n      message: `Backrelation list field \"${listField}\" found junction model \"${nearMiss.junctionModelName}\", but its foreign key to \"${candidate.targetModelName}\" does not reference \"${candidate.targetModelName}\"'s @id. The junction's target-side foreign key must reference \"${candidate.targetModelName}\"'s full @id columns for many-to-many recognition.`,\n      sourceId,\n      span: candidate.field.span,\n      data,\n    };\n  }\n  return {\n    code: 'PSL_JUNCTION_ID_NOT_FK_COVERING',\n    message: `Backrelation list field \"${listField}\" found junction-shaped model \"${nearMiss.junctionModelName}\" linking \"${candidate.modelName}\" and \"${candidate.targetModelName}\", but its id does not cover exactly its foreign-key columns. Declare @@id([...]) on \"${nearMiss.junctionModelName}\" listing exactly the two foreign-key columns for many-to-many recognition.`,\n    sourceId,\n    span: candidate.field.span,\n    data,\n  };\n}\n\nfunction manyToManyRelationNode(\n  candidate: ModelBackrelationCandidate,\n  pair: JunctionFkPair,","sourceCodeStart":369,"sourceCodeEnd":405,"githubUrl":"https://github.com/prisma/prisma/blob/1243cdffbec0506c595ac8fd5865fe258c336fa0/packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts#L369-L405","documentation":"Raised during many-to-many junction recognition for a backrelation list field. The resolver found a candidate junction model linking both sides, but that junction's foreign key to the TARGET model does not reference the target model's full @id columns (childColumnsInTargetIdOrder returned undefined because the FK's referenced columns don't line up 1:1 with the target's id columns). For an N:M junction, the target-side FK must point at exactly the target's identity columns.","triggerScenarios":"Emitted at psl-relation-resolution.ts:385-392 via junctionNearMissDiagnostic when findJunctionFkPairs records a near-miss with reason `target-fk-not-id` (psl-relation-resolution.ts:362-365). Triggered when a junction model has FKs to both the parent and target models, but the target-side FK references a non-id column (e.g. a unique column or a subset of a composite id).","commonSituations":"Junction whose target-side FK points at a `@unique` column instead of the @id; target model has a composite @@id and the junction FK only references part of it; refactoring a target's primary key without updating the junction's references; pointing the junction at a natural key that is unique but not the declared id.","solutions":["Change the junction's target-side `@relation(references: [...])` to list exactly the target model's full @id columns.","If the target has a composite @@id, make the junction FK reference every column of that @@id.","Alternatively, switch the target's identity so the column the junction already references becomes the @id.","If a junction was not intended, rename or remove the backrelation list field so it isn't recognized as a many-to-many side."],"exampleFix":"// before\nmodel User { id Int @id }\nmodel Group { id Int @id }\nmodel Membership {\n  user  User  @relation(fields: [userId], references: [id])\n  group Group @relation(fields: [groupId], references: [code]) // code is unique, not @id\n  userId  Int\n  groupId Int\n  @@unique([userId, groupId])\n}\n// after\nmodel Membership {\n  user  User  @relation(fields: [userId], references: [id])\n  group Group @relation(fields: [groupId], references: [id])\n  userId  Int\n  groupId Int\n  @@unique([userId, groupId])\n}","handlingStrategy":"validation","validationCode":"// For an implicit m:n, verify the junction's target-side FK references the target's full @id.\nfunction findJunctionTargetFkNotId(models) {\n  const idCols = new Map(models.map(m => [m.name, m.fields.filter(f => f.isId).map(f => f.name)]));\n  const offenders = [];\n  for (const junction of models) {\n    for (const fk of junction.foreignKeys ?? []) {\n      const targetId = idCols.get(fk.targetModelName) ?? [];\n      const sameSet = (a, b) => a.length === b.length && a.every(c => b.includes(c));\n      if (!sameSet(fk.references, targetId)) {\n        offenders.push(`${junction.name} -> ${fk.targetModelName}`);\n      }\n    }\n  }\n  return offenders;\n}","typeGuard":"// True when a junction FK references exactly the target's id columns.\nconst fkCoversTargetId = (fk, targetIdCols) => {\n  const sameSet = (a, b) => a.length === b.length && a.every(c => b.includes(c));\n  return sameSet(fk.references, targetIdCols);\n};","tryCatchPattern":"const fatal = diagnostics.filter(\n  d => d.code === 'PSL_JUNCTION_TARGET_FK_NOT_ID'\n);\nif (fatal.length) throw new SchemaValidationError(fatal);","preventionTips":["Make the junction's target-side @relation point at the full primary key of the target model.","For composite-PK targets, list every id column in `references`.","Avoid pointing junction FKs at a unique-but-not-id column; m:n recognition requires the @id.","When renaming a target's @id column, update all junction FK references in the same change."],"tags":["psl","relation","many-to-many","junction","foreign-key","validation"],"analyzedSha":"1243cdffbec0506c595ac8fd5865fe258c336fa0","analyzedAt":"2026-08-01T19:42:02.087Z","schemaVersion":2}