{"record":{"id":"43698a9c3aff37db","repo":"doctrine/orm","slug":"can-only-process-queries-that-select-only-one-from","errorCode":null,"errorMessage":"Can only process queries that select only one FROM component","messagePattern":"Can only process queries that select only one FROM component","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/Tools/Pagination/RootTypeWalker.php","lineNumber":36,"sourceCode":"/**\n * Infers the DBAL type of the #Id (identifier) column of the given query's root entity, and\n * returns it in place of a real SQL statement.\n *\n * Obtaining this type is a necessary intermediate step for \\Doctrine\\ORM\\Tools\\Pagination\\Paginator.\n * We can best do this from a tree walker because it gives us access to the AST.\n *\n * Returning the type instead of a \"real\" SQL statement is a slight hack. However, it has the\n * benefit that the DQL -> root entity id type resolution can be cached in the query cache.\n */\nfinal class RootTypeWalker extends SqlOutputWalker\n{\n    public function walkSelectStatement(AST\\SelectStatement $selectStatement): string\n    {\n        // Get the root entity and alias from the AST fromClause\n        $from = $selectStatement->fromClause->identificationVariableDeclarations;\n\n        if (count($from) > 1) {\n            throw new RuntimeException('Can only process queries that select only one FROM component');\n        }\n\n        $fromRoot            = reset($from);\n        $rootAlias           = $fromRoot->rangeVariableDeclaration->aliasIdentificationVariable;\n        $rootClass           = $this->getMetadataForDqlAlias($rootAlias);\n        $identifierFieldName = $rootClass->getSingleIdentifierFieldName();\n\n        return PersisterHelper::getTypeOfField(\n            $identifierFieldName,\n            $rootClass,\n            $this->getQuery()\n                ->getEntityManager(),\n        )[0];\n    }\n\n    public function getFinalizer(AST\\DeleteStatement|AST\\UpdateStatement|AST\\SelectStatement $AST): SqlFinalizer\n    {\n        if (! $AST instanceof AST\\SelectStatement) {","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/doctrine/orm/blob/d9b9ff73016bf598ae07515f97289ce8074e97a5/src/Tools/Pagination/RootTypeWalker.php#L18-L54","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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'.","Paginate only the primary entity and hydrate secondary entities in a separate query keyed by the page ids.","If the second FROM entity is only used as a filter, replace it with a WHERE EXISTS (SELECT ... ) subquery."],"exampleFix":"// before\n$dql = 'SELECT u, g FROM App\\Entity\\User u, App\\Entity\\Group g WHERE u.groupId = g.id';\n$paginator = new Paginator($em->createQuery($dql)->setMaxResults(20));\n\n// after\n$dql = 'SELECT u FROM App\\Entity\\User u JOIN u.group g';\n$paginator = new Paginator($em->createQuery($dql)->setMaxResults(20));","handlingStrategy":"validation","validationCode":"// Reject multi-root queries before handing them to the Paginator\n$ast  = $query->getAST();\n$from = $ast->fromClause->identificationVariableDeclarations;\nif (count($from) > 1) {\n    throw new InvalidArgumentException('Paginator queries must have exactly one FROM component, got ' . count($from));\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Forbid comma-separated FROM in DQL via code review or a lint rule","Express cross-entity conditions with JOIN ... WITH or WHERE EXISTS","Cover paginated endpoints with tests so a multi-root regression surfaces immediately"],"tags":["doctrine-orm","paginator","pagination","dql","from-clause","root-entity"],"backgroundTag":"pagination-multi-root-query","analyzedSha":"d9b9ff73016bf598ae07515f97289ce8074e97a5","analyzedAt":"2026-08-21T06:13:15.863Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}