prisma/prisma ยท error

PSL_JUNCTION_TARGET_FK_NOT_ID

PSL_JUNCTION_TARGET_FK_NOT_ID

Error message

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.

What it means

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.

Source

Thrown at packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts:387

    }
  }
  return { pairs, nearMisses };
}

function junctionNearMissDiagnostic(
  candidate: ModelBackrelationCandidate,
  nearMiss: JunctionNearMiss,
  sourceId: string,
): ContractSourceDiagnostic {
  const listField = `${candidate.modelName}.${candidate.field.name}`;
  const data = {
    listField,
    junctionModel: nearMiss.junctionModelName,
    targetModel: candidate.targetModelName,
  };
  if (nearMiss.reason === 'target-fk-not-id') {
    return {
      code: 'PSL_JUNCTION_TARGET_FK_NOT_ID',
      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.`,
      sourceId,
      span: candidate.field.span,
      data,
    };
  }
  return {
    code: 'PSL_JUNCTION_ID_NOT_FK_COVERING',
    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.`,
    sourceId,
    span: candidate.field.span,
    data,
  };
}

function manyToManyRelationNode(
  candidate: ModelBackrelationCandidate,
  pair: JunctionFkPair,

View on GitHub (pinned to 1243cdffbe)

Solutions

  1. Change the junction's target-side `@relation(references: [...])` to list exactly the target model's full @id columns.
  2. If the target has a composite @@id, make the junction FK reference every column of that @@id.
  3. Alternatively, switch the target's identity so the column the junction already references becomes the @id.
  4. If a junction was not intended, rename or remove the backrelation list field so it isn't recognized as a many-to-many side.

Example fix

// before
model User { id Int @id }
model Group { id Int @id }
model Membership {
  user  User  @relation(fields: [userId], references: [id])
  group Group @relation(fields: [groupId], references: [code]) // code is unique, not @id
  userId  Int
  groupId Int
  @@unique([userId, groupId])
}
// after
model Membership {
  user  User  @relation(fields: [userId], references: [id])
  group Group @relation(fields: [groupId], references: [id])
  userId  Int
  groupId Int
  @@unique([userId, groupId])
}
Defensive patterns

Strategy: validation

Validate before calling

// For an implicit m:n, verify the junction's target-side FK references the target's full @id.
function findJunctionTargetFkNotId(models) {
  const idCols = new Map(models.map(m => [m.name, m.fields.filter(f => f.isId).map(f => f.name)]));
  const offenders = [];
  for (const junction of models) {
    for (const fk of junction.foreignKeys ?? []) {
      const targetId = idCols.get(fk.targetModelName) ?? [];
      const sameSet = (a, b) => a.length === b.length && a.every(c => b.includes(c));
      if (!sameSet(fk.references, targetId)) {
        offenders.push(`${junction.name} -> ${fk.targetModelName}`);
      }
    }
  }
  return offenders;
}

Type guard

// True when a junction FK references exactly the target's id columns.
const fkCoversTargetId = (fk, targetIdCols) => {
  const sameSet = (a, b) => a.length === b.length && a.every(c => b.includes(c));
  return sameSet(fk.references, targetIdCols);
};

Try / catch

const fatal = diagnostics.filter(
  d => d.code === 'PSL_JUNCTION_TARGET_FK_NOT_ID'
);
if (fatal.length) throw new SchemaValidationError(fatal);

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


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