doctrine/orm · error · RuntimeException

Setting a limit is not supported for delete or update querie

Error message

Setting a limit is not supported for delete or update queries.

What it means

setMaxResults() applies a result limit, which only makes sense for SELECT statements. Bulk DQL DELETE/UPDATE cannot be row-limited portably across database platforms, so as soon as the builder's type is Delete or Update, setMaxResults() throws RuntimeException instead of accepting the value.

Source

Thrown at src/QueryBuilder.php:595

    }

    /**
     * Gets the position of the first result the query object was set to retrieve (the "offset").
     */
    public function getFirstResult(): int
    {
        return $this->firstResult;
    }

    /**
     * Sets the maximum number of results to retrieve (the "limit").
     *
     * @return $this
     */
    public function setMaxResults(int|null $maxResults): static
    {
        if ($this->type === QueryType::Delete || $this->type === QueryType::Update) {
            throw new RuntimeException('Setting a limit is not supported for delete or update queries.');
        }

        $this->maxResults = $maxResults;

        return $this;
    }

    /**
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
     * Returns NULL if {@link setMaxResults} was not applied to this query builder.
     */
    public function getMaxResults(): int|null
    {
        return $this->maxResults;
    }

    /**
     * Either appends to or replaces a single, generic query part.

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Remove setMaxResults() from delete/update paths; DQL bulk statements affect all rows matching the WHERE.
  2. To bound affected rows, first SELECT the ids with setMaxResults(), then run the bulk statement WHERE id IN (:ids).
  3. In shared helpers, apply the limit only when you know the builder is a SELECT (QueryBuilder::getType() is protected, so track the statement kind you built).

Example fix

// before
$em->createQueryBuilder()->update(User::class, 'u')->set('u.notified', ':n')
    ->where('u.active = true')->setMaxResults(100) // RuntimeException
    ->getQuery()->execute();

// after - bound rows with a limited SELECT, then bulk update by id
$ids = $em->createQueryBuilder()->select('u.id')->from(User::class, 'u')
    ->where('u.active = true')->setMaxResults(100)
    ->getQuery()->getSingleColumnResult();
$em->createQueryBuilder()->update(User::class, 'u')->set('u.notified', ':n')
    ->where('u.id IN (:ids)')->setParameter('ids', $ids)->setParameter('n', true)
    ->getQuery()->execute();
Defensive patterns

Strategy: try-catch

Validate before calling

// QueryBuilder::getType() is protected, so validate on the produced DQL:
$dql    = strtoupper(ltrim((string) $qb->getDQL()));
$isBulk = str_starts_with($dql, 'DELETE') || str_starts_with($dql, 'UPDATE');
if ($limit !== null && ! $isBulk) {
    $qb->setMaxResults($limit);
}

Try / catch

In generic decorators that may receive bulk builders, make the limit optional and degrade on purpose:
try {
    $qb->setMaxResults($limit);
} catch (\Doctrine\ORM\RuntimeException $e) {
    // bulk DELETE/UPDATE cannot be limited: skip the limit deliberately
}

Prevention

When it happens

Trigger: $qb->delete(User::class, 'u')->where(...)->setMaxResults(100); update() followed by setMaxResults(); generic list helpers or pagination decorators that unconditionally apply a limit to whatever QueryBuilder they receive, including bulk ones.

Common situations: Porting MySQL-specific DELETE ... LIMIT / UPDATE ... LIMIT SQL to DQL; shared repository helpers that always apply pagination defaults; cleanup jobs trying to cap affected rows per run.

Related errors


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