{"record":{"id":"4bf102973911e116","repo":"phacility/phabricator","slug":"rows-passed-to-loadallfromarray-include-two","errorCode":null,"errorMessage":"Rows passed to \"loadAllFromArray(...)\" include two or more rows with the same ID (\"%s\"). Rows must have unique IDs. An underlying query may be missing a GROUP BY.","messagePattern":"Rows passed to \"loadAllFromArray\\(\\.\\.\\.\\)\" include two or more rows with the same ID \\(\"(.+?)\"\\)\\. Rows must have unique IDs\\. An underlying query may be missing a GROUP BY\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/infrastructure/storage/lisk/LiskDAO.php","lineNumber":668,"sourceCode":"   * This is a lot messier than @{method:loadAllWhere}, but more flexible.\n   *\n   * @param  list  List of property dictionaries.\n   * @return dict  List of constructed objects, keyed on ID.\n   *\n   * @task   load\n   */\n  public function loadAllFromArray(array $rows) {\n    $result = array();\n\n    $id_key = $this->getIDKey();\n\n    foreach ($rows as $row) {\n      $obj = clone $this;\n      if ($id_key && isset($row[$id_key])) {\n        $row_id = $row[$id_key];\n\n        if (isset($result[$row_id])) {\n          throw new Exception(\n            pht(\n              'Rows passed to \"loadAllFromArray(...)\" include two or more '.\n              'rows with the same ID (\"%s\"). Rows must have unique IDs. '.\n              'An underlying query may be missing a GROUP BY.',\n              $row_id));\n        }\n\n        $result[$row_id] = $obj->loadFromArray($row);\n      } else {\n        $result[] = $obj->loadFromArray($row);\n      }\n    }\n\n    return $result;\n  }\n\n\n/* -(  Examining Objects  )-------------------------------------------------- */","sourceCodeStart":650,"sourceCodeEnd":686,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/infrastructure/storage/lisk/LiskDAO.php#L650-L686","documentation":"loadAllFromArray() hydrates rows into objects keyed by primary key; if two rows in the input carry the same ID, it throws, because a single-table SELECT can never produce that. The message names the usual root cause: the underlying query joins a one-to-many table and is missing a GROUP BY, so one logical row fans out into several. The check protects the ID-keyed result map from silent overwrites.","triggerScenarios":"A custom loader doing loadAllFromArray() on rows from a query with JOIN against a one-to-many table without GROUP BY/DISTINCT; UNION queries that fail to dedupe; hand-built row arrays passed directly to this method with repeated IDs.","commonSituations":"Extending LiskDAO with joined loaders (e.g. object + edge/attribute in one query); optimizing N+1 queries by joining and forgetting that the right side repeats the left; ad-hoc aggregation code feeding the DAO hydration API.","solutions":["Add GROUP BY on the primary key (or select DISTINCT on the ID) to the underlying query so each object appears once","If fan-out is intentional, aggregate the joined values (GROUP_CONCAT, MAX) into columns instead of emitting repeated rows","Do not call loadAllFromArray() with synthetic rows containing duplicate IDs — hydrate them differently","Prefer two queries (objects, then related rows) when aggregation distorts the data"],"exampleFix":"-- before: join fans out one object into many rows\nSELECT o.*, e.dst FROM `object` o\n  JOIN `edge` e ON e.src = o.id;\n-- => two rows with the same o.id => exception on hydration\n\n-- after: collapse to one row per object\nSELECT o.*, GROUP_CONCAT(e.dst) AS dsts FROM `object` o\n  JOIN `edge` e ON e.src = o.id\n  GROUP BY o.id;","handlingStrategy":"validation","validationCode":"// Before hydrating joined rows, assert the IDs are unique (i.e., the\n// query really is grouped):\n$ids = ipull($rows, 'id');\nif (count($ids) !== count(array_unique($ids))) {\n  throw new Exception('Joined query fans out; add GROUP BY on the primary key.');\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always GROUP BY the primary key (or SELECT DISTINCT ids) in custom joined loaders feeding loadAllFromArray()","Aggregate the joined side (GROUP_CONCAT/MAX) instead of emitting repeated left-side rows","When in doubt, run two queries — objects first, related rows second — instead of one fanned-out join","Add a test that hydrates the custom loader's output so fan-out regressions fail in CI, not production"],"tags":["lisk","dao","join-fanout","group-by","duplicate-rows","hydration"],"backgroundTag":"duplicate-rows-in-result","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}