doctrine/orm · error · RuntimeException

Can only process queries that select only one FROM component

Error message

Can only process queries that select only one FROM component

What it means

RootTypeWalker is an internal walker the Paginator attaches (Paginator::convertWhereInIdentifiersToDatabaseValues) to resolve the DBAL type of the root entity's identifier before binding the WHERE id IN (...) parameters. It reads only the first identification variable declaration of the FROM clause; a DQL with two or more FROM components (theta join style) has no single root entity, so the type cannot be resolved and it throws.

Source

Thrown at src/Tools/Pagination/RootTypeWalker.php:36

/**
 * Infers the DBAL type of the #Id (identifier) column of the given query's root entity, and
 * returns it in place of a real SQL statement.
 *
 * Obtaining this type is a necessary intermediate step for \Doctrine\ORM\Tools\Pagination\Paginator.
 * We can best do this from a tree walker because it gives us access to the AST.
 *
 * Returning the type instead of a "real" SQL statement is a slight hack. However, it has the
 * benefit that the DQL -> root entity id type resolution can be cached in the query cache.
 */
final class RootTypeWalker extends SqlOutputWalker
{
    public function walkSelectStatement(AST\SelectStatement $selectStatement): string
    {
        // Get the root entity and alias from the AST fromClause
        $from = $selectStatement->fromClause->identificationVariableDeclarations;

        if (count($from) > 1) {
            throw new RuntimeException('Can only process queries that select only one FROM component');
        }

        $fromRoot            = reset($from);
        $rootAlias           = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;
        $rootClass           = $this->getMetadataForDqlAlias($rootAlias);
        $identifierFieldName = $rootClass->getSingleIdentifierFieldName();

        return PersisterHelper::getTypeOfField(
            $identifierFieldName,
            $rootClass,
            $this->getQuery()
                ->getEntityManager(),
        )[0];
    }

    public function getFinalizer(AST\DeleteStatement|AST\UpdateStatement|AST\SelectStatement $AST): SqlFinalizer
    {
        if (! $AST instanceof AST\SelectStatement) {

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Rewrite the DQL with a single root and explicit joins: 'FROM App\Entity\User u JOIN u.groups g' or 'JOIN App\Entity\Group g WITH g.user = u'.
  2. Paginate only the primary entity and hydrate secondary entities in a separate query keyed by the page ids.
  3. If the second FROM entity is only used as a filter, replace it with a WHERE EXISTS (SELECT ... ) subquery.

Example fix

// before
$dql = 'SELECT u, g FROM App\Entity\User u, App\Entity\Group g WHERE u.groupId = g.id';
$paginator = new Paginator($em->createQuery($dql)->setMaxResults(20));

// after
$dql = 'SELECT u FROM App\Entity\User u JOIN u.group g';
$paginator = new Paginator($em->createQuery($dql)->setMaxResults(20));
Defensive patterns

Strategy: validation

Validate before calling

// Reject multi-root queries before handing them to the Paginator
$ast  = $query->getAST();
$from = $ast->fromClause->identificationVariableDeclarations;
if (count($from) > 1) {
    throw new InvalidArgumentException('Paginator queries must have exactly one FROM component, got ' . count($from));
}

Prevention

When it happens

Trigger: Calling getIterator()/foreach on a Paginator whose query has multiple FROM components (e.g. 'SELECT u, g FROM App\Entity\User u, App\Entity\Group g WHERE ...') while fetchJoinCollection is enabled (constructor default true) and maxResults is set - the Paginator internally runs RootTypeWalker before hydrating the page.

Common situations: Legacy Doctrine 1-style DQL using comma-separated FROM instead of explicit JOIN; repository methods joining unrelated entities via FROM; queries migrated between versions where the second FROM was never converted.

Related errors


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