{"record":{"id":"97660f4b96812859","repo":"remix-run/remix","slug":"hasmanythrough-relation-is-missing-through-metadat","errorCode":null,"errorMessage":"hasManyThrough relation is missing through metadata","messagePattern":"hasManyThrough relation is missing through metadata","errorType":"exception","errorClass":"DataTableQueryError","httpStatus":null,"severity":"error","filePath":"packages/data-table/src/lib/database/relations.ts","lineNumber":116,"sourceCode":"    let key = getCompositeKey(sourceRow, relation.sourceKey)\n    let matches = grouped.get(key) ?? []\n    let pagedMatches = applyPagination(matches, relation.modifiers.limit, relation.modifiers.offset)\n\n    if (relation.cardinality === 'many') {\n      return pagedMatches\n    }\n\n    return pagedMatches[0] ?? null\n  })\n}\n\nasync function loadHasManyThroughValues(\n  database: QueryExecutionContext,\n  sourceRows: Record<string, unknown>[],\n  relation: AnyRelation,\n): Promise<unknown[]> {\n  if (!relation.through) {\n    throw new DataTableQueryError('hasManyThrough relation is missing through metadata')\n  }\n\n  if (sourceRows.length === 0) {\n    return []\n  }\n\n  let throughRelation = relation.through.relation\n  let sourceTuples = uniqueTuples(sourceRows, throughRelation.sourceKey)\n\n  if (sourceTuples.length === 0) {\n    return sourceRows.map(() => [])\n  }\n\n  let throughQuery = createQuery(throughRelation.targetTable)\n  let throughPredicate = buildLinkPredicate(throughRelation.targetKey, sourceTuples)\n\n  if (throughPredicate) {\n    throughQuery = throughQuery.where(","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table/src/lib/database/relations.ts#L98-L134","documentation":"A hasManyThrough relation requires 'through' metadata naming the join table and keys. If that metadata is absent (relation.through is undefined), the loader cannot build the join query and throws a DataTableQueryError. Typically caused by constructing the relation object incorrectly or omitting the through option.","triggerScenarios":"Defining hasManyThrough() without a through option, or manually creating a relation object lacking the through field; a malformed relation map entry that reaches loadHasManyThroughValues.","commonSituations":"Configuring many-to-many relations and forgetting the join table spec; copying a plain hasMany definition where hasManyThrough was intended; typos in the option name so through is never set.","solutions":["Provide complete through metadata: hasManyThrough(() => targetTable, { through: { table: joinTable, sourceKey, targetKey } }) per the API","Verify the option key is named exactly 'through' and the value is truthy","If you build relation objects programmatically, assert the through field exists before registration"],"exampleFix":"// before\ntags: sourceTable.hasManyThrough(() => tagTable, {})\n\n// after\ntags: sourceTable.hasManyThrough(() => tagTable, {\n  through: {\n    table: () => postTagsTable,\n    sourceKey: 'postId',\n    targetKey: 'tagId',\n  },\n})","handlingStrategy":"validation","validationCode":"if (!relation.through) {\n  throw new Error(`hasManyThrough relation missing through metadata`)\n}","typeGuard":"function isCompleteHasManyThrough(relation: AnyRelation): boolean {\n  return relation.kind === 'hasManyThrough' && relation.through != null\n}","tryCatchPattern":"try {\n  await loadRelationsForRows(database, rows, relationMap)\n} catch (error) {\n  if (error instanceof DataTableQueryError && /through metadata/.test(error.message)) {\n    // skip this relation and log configuration error\n  } else throw error\n}","preventionTips":["Always pass the through option when using hasManyThrough","Unit-test relation configuration objects for required metadata","Prefer the builder API over hand-constructed relation objects"],"tags":["data-table","relations","has-many-through","configuration"],"backgroundTag":"missing-relation-configuration","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}