doctrine/orm · error · LengthException

Unexpected empty result for database query.

Error message

Unexpected empty result for database query.

What it means

After an INSERT/UPDATE flush, BasicEntityPersister::assignDefaultVersionAndUpsertableValues() re-SELECTs version and generated (insertable:false/updatable:false) columns for the written row by its identifier (fetchVersionAndNotUpsertableValues). fetchNumeric() returning false means the row the UnitOfWork just wrote cannot be read back, which should be impossible in a consistent transaction, so it throws LengthException to surface the inconsistency.

Source

Thrown at src/Persisters/Entity/BasicEntityPersister.php:331

        $tableName  = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
        $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);

        // FIXME: Order with composite keys might not be correct
        $sql = 'SELECT ' . implode(', ', $columnNames)
            . ' FROM ' . $tableName
            . ' 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;
    }

    /**
     * @param mixed[] $id
     *
     * @return list<ParameterType|int|string>
     * @phpstan-return list<ParameterType::*|ArrayParameterType::*|string>
     */
    final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Check for database triggers or row-level security on the table that remove/hide the row after the write
  2. Ensure the flush's write AND re-select run on the same connection and inside the same transaction (no read/write splitting mid-flush)
  3. Verify identifier mapping (custom DBAL types on the id column can make the WHERE clause not match)
  4. If another process legitimately deletes during flush, reorder the operations (perform the delete after flush, or detach the entity)
Defensive patterns

Strategy: validation

Validate before calling

// Guard the environment, not the call: assert the row is visible on the same connection before flush-heavy paths
$row = $conn->fetchOne('SELECT 1 FROM ' . $meta->getTableName() . ' WHERE ' . $meta->getSingleIdentifierColumnName() . ' = ?', [$id]);
if ($row === false) { /* do not proceed with generated-column/versioned writes */ }

Try / catch

try { $em->flush(); } catch (LengthException $e) { if ($e->getMessage() === 'Unexpected empty result for database query.') { /* inspect triggers / replica routing, do not blind-retry */ } throw $e; }

Prevention

When it happens

Trigger: flush() on an entity with #[Version] or generated columns where the row is gone at re-select time: an AFTER INSERT/UPDATE trigger deleting or moving the row, a concurrent delete committed between the write and the re-select, row-level security/tenant filters hiding the row, or a read/write-split connection routing the SELECT to a replica with lag.

Common situations: MySQL/Postgres triggers; multi-tenant RLS policies; Doctrine with a connection wrapper that sends reads to a read replica mid-flush; identifiers mangled by a custom DBAL type so the WHERE clause selects nothing.

Related errors


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