doctrine/orm · error · LengthException

Unexpected empty result for database query.

Error message

Unexpected empty result for database query.

What it means

JoinedSubclassPersister (entity with joined-table inheritance) re-SELECTs version/generated columns after a write, joining the base and parent tables, filtered by the flattened identifier. fetchNumeric() returning false means the just-written row is no longer visible, so the invariant is broken and a LengthException is thrown mid-flush.

Source

Thrown at src/Persisters/Entity/JoinedSubclassPersister.php:562

        $identifier     = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
        foreach ($identifier as $i => $idValue) {
            $identifier[$i] = $baseTableAlias . '.' . $idValue;
        }

        $sql = 'SELECT ' . implode(', ', $columnNames)
            . ' FROM ' . $tableName . ' ' . $baseTableAlias
            . $joinSql
            . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';

        $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
        $values = $this->conn->fetchNumeric(
            $sql,
            array_values($flatId),
            $this->extractIdentifierTypes($id, $versionedClass),
        );

        if ($values === false) {
            throw new LengthException('Unexpected empty result for database query.');
        }

        $values = array_combine(array_keys($columnNames), $values);

        if (! $values) {
            throw new LengthException('Unexpected number of database columns.');
        }

        return $values;
    }

    private function getJoinSql(string $baseTableAlias): string
    {
        $joinSql          = '';
        $identifierColumn = $this->class->getIdentifierColumnNames();

        // INNER JOIN parent tables
        foreach ($this->class->parentClasses as $parentClassName) {

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Inspect triggers and row-level security on ALL tables of the inheritance hierarchy
  2. Guarantee write and re-select share one connection and transaction
  3. Check the discriminator/identifier mapping so the WHERE matches the actual row
  4. If a concurrent delete is expected, serialize the operations (lock the row with a pessimistic lock or reorder application logic)
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the write and re-select share the connection; refuse flushes on read-only/replica connections
if (! $em->getConnection()->isTransactionActive() && $usesReplicaRouting) { /* pin to primary before flush */ }

Try / catch

try { $em->flush(); } catch (LengthException $e) { if ($e->getMessage() === 'Unexpected empty result for database query.') { /* check triggers/RLS on all hierarchy tables */ } throw $e; }

Prevention

When it happens

Trigger: flush() on a joined-inheritance entity with #[Version] or generated columns where the row disappears between write and re-select: AFTER INSERT/UPDATE triggers, concurrent deletes, row-level security, or connection routing that sends the re-select to a lagging replica.

Common situations: Audit/soft-delete triggers on child tables in inheritance hierarchies; tenant isolation policies; replica-routing connection wrappers; integration tests with READ UNCOMMITTED or nested transaction quirks.

Related errors


AI-assisted analysis of doctrine/orm@d9b9ff7301 (2026-08-21). Data as JSON: /api/errors/af9e0c4120e48a5d. Report an issue: GitHub.