phacility/phabricator · error · Exception

Rows passed to "loadAllFromArray(...)" include two or more r

Error message

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.

What it means

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.

Source

Thrown at src/infrastructure/storage/lisk/LiskDAO.php:668

   * This is a lot messier than @{method:loadAllWhere}, but more flexible.
   *
   * @param  list  List of property dictionaries.
   * @return dict  List of constructed objects, keyed on ID.
   *
   * @task   load
   */
  public function loadAllFromArray(array $rows) {
    $result = array();

    $id_key = $this->getIDKey();

    foreach ($rows as $row) {
      $obj = clone $this;
      if ($id_key && isset($row[$id_key])) {
        $row_id = $row[$id_key];

        if (isset($result[$row_id])) {
          throw new Exception(
            pht(
              '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.',
              $row_id));
        }

        $result[$row_id] = $obj->loadFromArray($row);
      } else {
        $result[] = $obj->loadFromArray($row);
      }
    }

    return $result;
  }


/* -(  Examining Objects  )-------------------------------------------------- */

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Add GROUP BY on the primary key (or select DISTINCT on the ID) to the underlying query so each object appears once
  2. If fan-out is intentional, aggregate the joined values (GROUP_CONCAT, MAX) into columns instead of emitting repeated rows
  3. Do not call loadAllFromArray() with synthetic rows containing duplicate IDs — hydrate them differently
  4. Prefer two queries (objects, then related rows) when aggregation distorts the data

Example fix

-- before: join fans out one object into many rows
SELECT o.*, e.dst FROM `object` o
  JOIN `edge` e ON e.src = o.id;
-- => two rows with the same o.id => exception on hydration

-- after: collapse to one row per object
SELECT o.*, GROUP_CONCAT(e.dst) AS dsts FROM `object` o
  JOIN `edge` e ON e.src = o.id
  GROUP BY o.id;
Defensive patterns

Strategy: validation

Validate before calling

// Before hydrating joined rows, assert the IDs are unique (i.e., the
// query really is grouped):
$ids = ipull($rows, 'id');
if (count($ids) !== count(array_unique($ids))) {
  throw new Exception('Joined query fans out; add GROUP BY on the primary key.');
}

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/4bf102973911e116. Report an issue: GitHub.