From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vendor/doctrine/orm/src/Query/SqlWalker.php | 2264 +++++++++++++++++++++++++++ 1 file changed, 2264 insertions(+) create mode 100644 vendor/doctrine/orm/src/Query/SqlWalker.php (limited to 'vendor/doctrine/orm/src/Query/SqlWalker.php') diff --git a/vendor/doctrine/orm/src/Query/SqlWalker.php b/vendor/doctrine/orm/src/Query/SqlWalker.php new file mode 100644 index 0000000..c6f98c1 --- /dev/null +++ b/vendor/doctrine/orm/src/Query/SqlWalker.php @@ -0,0 +1,2264 @@ +> + */ + private array $scalarResultAliasMap = []; + + /** + * Map from Table-Alias + Column-Name to OrderBy-Direction. + * + * @var array + */ + private array $orderedColumnsMap = []; + + /** + * Map from DQL-Alias + Field-Name to SQL Column Alias. + * + * @var array> + */ + private array $scalarFields = []; + + /** + * A list of classes that appear in non-scalar SelectExpressions. + * + * @psalm-var array + */ + private array $selectedClasses = []; + + /** + * The DQL alias of the root class of the currently traversed query. + * + * @psalm-var list + */ + private array $rootAliases = []; + + /** + * Flag that indicates whether to generate SQL table aliases in the SQL. + * These should only be generated for SELECT queries, not for UPDATE/DELETE. + */ + private bool $useSqlTableAliases = true; + + /** + * The database platform abstraction. + */ + private readonly AbstractPlatform $platform; + + /** + * The quote strategy. + */ + private readonly QuoteStrategy $quoteStrategy; + + /** @psalm-param array $queryComponents The query components (symbol table). */ + public function __construct( + private readonly Query $query, + private readonly ParserResult $parserResult, + private array $queryComponents, + ) { + $this->rsm = $parserResult->getResultSetMapping(); + $this->em = $query->getEntityManager(); + $this->conn = $this->em->getConnection(); + $this->platform = $this->conn->getDatabasePlatform(); + $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); + } + + /** + * Gets the Query instance used by the walker. + */ + public function getQuery(): Query + { + return $this->query; + } + + /** + * Gets the Connection used by the walker. + */ + public function getConnection(): Connection + { + return $this->conn; + } + + /** + * Gets the EntityManager used by the walker. + */ + public function getEntityManager(): EntityManagerInterface + { + return $this->em; + } + + /** + * Gets the information about a single query component. + * + * @param string $dqlAlias The DQL alias. + * + * @return mixed[] + * @psalm-return QueryComponent + */ + public function getQueryComponent(string $dqlAlias): array + { + return $this->queryComponents[$dqlAlias]; + } + + public function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata + { + return $this->queryComponents[$dqlAlias]['metadata'] + ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); + } + + /** + * Returns internal queryComponents array. + * + * @return array + */ + public function getQueryComponents(): array + { + return $this->queryComponents; + } + + /** + * Sets or overrides a query component for a given dql alias. + * + * @psalm-param QueryComponent $queryComponent + */ + public function setQueryComponent(string $dqlAlias, array $queryComponent): void + { + $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; + + if (array_diff($requiredKeys, array_keys($queryComponent))) { + throw QueryException::invalidQueryComponent($dqlAlias); + } + + $this->queryComponents[$dqlAlias] = $queryComponent; + } + + /** + * Gets an executor that can be used to execute the result of this walker. + */ + public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor + { + return match (true) { + $statement instanceof AST\SelectStatement + => new Exec\SingleSelectExecutor($statement, $this), + $statement instanceof AST\UpdateStatement + => $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined() + ? new Exec\MultiTableUpdateExecutor($statement, $this) + : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), + $statement instanceof AST\DeleteStatement + => $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined() + ? new Exec\MultiTableDeleteExecutor($statement, $this) + : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), + }; + } + + /** + * Generates a unique, short SQL table alias. + */ + public function getSQLTableAlias(string $tableName, string $dqlAlias = ''): string + { + $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; + + if (! isset($this->tableAliasMap[$tableName])) { + $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't') + . $this->tableAliasCounter++ . '_'; + } + + return $this->tableAliasMap[$tableName]; + } + + /** + * Forces the SqlWalker to use a specific alias for a table name, rather than + * generating an alias on its own. + */ + public function setSQLTableAlias(string $tableName, string $alias, string $dqlAlias = ''): string + { + $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; + + $this->tableAliasMap[$tableName] = $alias; + + return $alias; + } + + /** + * Gets an SQL column alias for a column name. + */ + public function getSQLColumnAlias(string $columnName): string + { + return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform); + } + + /** + * Generates the SQL JOINs that are necessary for Class Table Inheritance + * for the given class. + */ + private function generateClassTableInheritanceJoins( + ClassMetadata $class, + string $dqlAlias, + ): string { + $sql = ''; + + $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + + // INNER JOIN parent class tables + foreach ($class->parentClasses as $parentClassName) { + $parentClass = $this->em->getClassMetadata($parentClassName); + $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias); + + // If this is a joined association we must use left joins to preserve the correct result. + $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; + $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; + + $sqlParts = []; + + foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { + $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; + } + + // Add filters on the root class + $sqlParts[] = $this->generateFilterConditionSQL($parentClass, $tableAlias); + + $sql .= implode(' AND ', array_filter($sqlParts)); + } + + // LEFT JOIN child class tables + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; + + $sqlParts = []; + + foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) { + $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; + } + + $sql .= implode(' AND ', $sqlParts); + } + + return $sql; + } + + private function generateOrderedCollectionOrderByItems(): string + { + $orderedColumns = []; + + foreach ($this->selectedClasses as $selectedClass) { + $dqlAlias = $selectedClass['dqlAlias']; + $qComp = $this->queryComponents[$dqlAlias]; + + if (! isset($qComp['relation']->orderBy)) { + continue; + } + + assert(isset($qComp['metadata'])); + $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); + + foreach ($qComp['relation']->orderBy as $fieldName => $orientation) { + $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); + $tableName = $qComp['metadata']->isInheritanceTypeJoined() + ? $persister->getOwningTable($fieldName) + : $qComp['metadata']->getTableName(); + + $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName; + + // OrderByClause should replace an ordered relation. see - DDC-2475 + if (isset($this->orderedColumnsMap[$orderedColumn])) { + continue; + } + + $this->orderedColumnsMap[$orderedColumn] = $orientation; + $orderedColumns[] = $orderedColumn . ' ' . $orientation; + } + } + + return implode(', ', $orderedColumns); + } + + /** + * Generates a discriminator column SQL condition for the class with the given DQL alias. + * + * @psalm-param list $dqlAliases List of root DQL aliases to inspect for discriminator restrictions. + */ + private function generateDiscriminatorColumnConditionSQL(array $dqlAliases): string + { + $sqlParts = []; + + foreach ($dqlAliases as $dqlAlias) { + $class = $this->getMetadataForDqlAlias($dqlAlias); + + if (! $class->isInheritanceTypeSingleTable()) { + continue; + } + + $sqlTableAlias = $this->useSqlTableAliases + ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.' + : ''; + + $conn = $this->em->getConnection(); + $values = []; + + if ($class->discriminatorValue !== null) { // discriminators can be 0 + $values[] = $class->getDiscriminatorColumn()->type === 'integer' && is_int($class->discriminatorValue) + ? $class->discriminatorValue + : $conn->quote((string) $class->discriminatorValue); + } + + foreach ($class->subClasses as $subclassName) { + $subclassMetadata = $this->em->getClassMetadata($subclassName); + + // Abstract entity classes show up in the list of subClasses, but may be omitted + // from the discriminator map. In that case, they have a null discriminator value. + if ($subclassMetadata->discriminatorValue === null) { + continue; + } + + $values[] = $subclassMetadata->getDiscriminatorColumn()->type === 'integer' && is_int($subclassMetadata->discriminatorValue) + ? $subclassMetadata->discriminatorValue + : $conn->quote((string) $subclassMetadata->discriminatorValue); + } + + if ($values !== []) { + $sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')'; + } else { + $sqlParts[] = '1=0'; // impossible condition + } + } + + $sql = implode(' AND ', $sqlParts); + + return count($sqlParts) > 1 ? '(' . $sql . ')' : $sql; + } + + /** + * Generates the filter SQL for a given entity and table alias. + */ + private function generateFilterConditionSQL( + ClassMetadata $targetEntity, + string $targetTableAlias, + ): string { + if (! $this->em->hasFilters()) { + return ''; + } + + switch ($targetEntity->inheritanceType) { + case ClassMetadata::INHERITANCE_TYPE_NONE: + break; + case ClassMetadata::INHERITANCE_TYPE_JOINED: + // The classes in the inheritance will be added to the query one by one, + // but only the root node is getting filtered + if ($targetEntity->name !== $targetEntity->rootEntityName) { + return ''; + } + + break; + case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE: + // With STI the table will only be queried once, make sure that the filters + // are added to the root entity + $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); + break; + default: + //@todo: throw exception? + return ''; + } + + $filterClauses = []; + foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { + $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); + if ($filterExpr !== '') { + $filterClauses[] = '(' . $filterExpr . ')'; + } + } + + return implode(' AND ', $filterClauses); + } + + /** + * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. + */ + public function walkSelectStatement(AST\SelectStatement $selectStatement): string + { + $limit = $this->query->getMaxResults(); + $offset = $this->query->getFirstResult(); + $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE; + $sql = $this->walkSelectClause($selectStatement->selectClause) + . $this->walkFromClause($selectStatement->fromClause) + . $this->walkWhereClause($selectStatement->whereClause); + + if ($selectStatement->groupByClause) { + $sql .= $this->walkGroupByClause($selectStatement->groupByClause); + } + + if ($selectStatement->havingClause) { + $sql .= $this->walkHavingClause($selectStatement->havingClause); + } + + if ($selectStatement->orderByClause) { + $sql .= $this->walkOrderByClause($selectStatement->orderByClause); + } + + $orderBySql = $this->generateOrderedCollectionOrderByItems(); + if (! $selectStatement->orderByClause && $orderBySql) { + $sql .= ' ORDER BY ' . $orderBySql; + } + + $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset); + + if ($lockMode === LockMode::NONE) { + return $sql; + } + + if ($lockMode === LockMode::PESSIMISTIC_READ) { + return $sql . ' ' . $this->getReadLockSQL($this->platform); + } + + if ($lockMode === LockMode::PESSIMISTIC_WRITE) { + return $sql . ' ' . $this->getWriteLockSQL($this->platform); + } + + if ($lockMode !== LockMode::OPTIMISTIC) { + throw QueryException::invalidLockMode(); + } + + foreach ($this->selectedClasses as $selectedClass) { + if (! $selectedClass['class']->isVersioned) { + throw OptimisticLockException::lockFailed($selectedClass['class']->name); + } + } + + return $sql; + } + + /** + * Walks down a UpdateStatement AST node, thereby generating the appropriate SQL. + */ + public function walkUpdateStatement(AST\UpdateStatement $updateStatement): string + { + $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; + + return $this->walkUpdateClause($updateStatement->updateClause) + . $this->walkWhereClause($updateStatement->whereClause); + } + + /** + * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. + */ + public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): string + { + $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; + + return $this->walkDeleteClause($deleteStatement->deleteClause) + . $this->walkWhereClause($deleteStatement->whereClause); + } + + /** + * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL. + * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers. + */ + public function walkEntityIdentificationVariable(string $identVariable): string + { + $class = $this->getMetadataForDqlAlias($identVariable); + $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable); + $sqlParts = []; + + foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { + $sqlParts[] = $tableAlias . '.' . $columnName; + } + + return implode(', ', $sqlParts); + } + + /** + * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. + */ + public function walkIdentificationVariable(string $identificationVariable, string|null $fieldName = null): string + { + $class = $this->getMetadataForDqlAlias($identificationVariable); + + if ( + $fieldName !== null && $class->isInheritanceTypeJoined() && + isset($class->fieldMappings[$fieldName]->inherited) + ) { + $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]->inherited); + } + + return $this->getSQLTableAlias($class->getTableName(), $identificationVariable); + } + + /** + * Walks down a PathExpression AST node, thereby generating the appropriate SQL. + */ + public function walkPathExpression(AST\PathExpression $pathExpr): string + { + $sql = ''; + assert($pathExpr->field !== null); + + switch ($pathExpr->type) { + case AST\PathExpression::TYPE_STATE_FIELD: + $fieldName = $pathExpr->field; + $dqlAlias = $pathExpr->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + + if ($this->useSqlTableAliases) { + $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; + } + + $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + break; + + case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: + // 1- the owning side: + // Just use the foreign key, i.e. u.group_id + $fieldName = $pathExpr->field; + $dqlAlias = $pathExpr->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + + if (isset($class->associationMappings[$fieldName]->inherited)) { + $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); + } + + $assoc = $class->associationMappings[$fieldName]; + + if (! $assoc->isOwningSide()) { + throw QueryException::associationPathInverseSideNotSupported($pathExpr); + } + + assert($assoc->isToOneOwningSide()); + + // COMPOSITE KEYS NOT (YET?) SUPPORTED + if (count($assoc->sourceToTargetKeyColumns) > 1) { + throw QueryException::associationPathCompositeKeyNotSupported(); + } + + if ($this->useSqlTableAliases) { + $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'; + } + + $sql .= reset($assoc->targetToSourceKeyColumns); + break; + + default: + throw QueryException::invalidPathExpression($pathExpr); + } + + return $sql; + } + + /** + * Walks down a SelectClause AST node, thereby generating the appropriate SQL. + */ + public function walkSelectClause(AST\SelectClause $selectClause): string + { + $sql = 'SELECT ' . ($selectClause->isDistinct ? 'DISTINCT ' : ''); + $sqlSelectExpressions = array_filter(array_map($this->walkSelectExpression(...), $selectClause->selectExpressions)); + + if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && $selectClause->isDistinct) { + $this->query->setHint(self::HINT_DISTINCT, true); + } + + $addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT + || $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS); + + foreach ($this->selectedClasses as $selectedClass) { + $class = $selectedClass['class']; + $dqlAlias = $selectedClass['dqlAlias']; + $resultAlias = $selectedClass['resultAlias']; + + // Register as entity or joined entity result + if (! isset($this->queryComponents[$dqlAlias]['relation'])) { + $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); + } else { + assert(isset($this->queryComponents[$dqlAlias]['parent'])); + + $this->rsm->addJoinedEntityResult( + $class->name, + $dqlAlias, + $this->queryComponents[$dqlAlias]['parent'], + $this->queryComponents[$dqlAlias]['relation']->fieldName, + ); + } + + if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { + // Add discriminator columns to SQL + $rootClass = $this->em->getClassMetadata($class->rootEntityName); + $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias); + $discrColumn = $rootClass->getDiscriminatorColumn(); + $columnAlias = $this->getSQLColumnAlias($discrColumn->name); + + $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn->name . ' AS ' . $columnAlias; + + $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); + $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn->fieldName, false, $discrColumn->type); + if (! empty($discrColumn->enumType)) { + $this->rsm->addEnumResult($columnAlias, $discrColumn->enumType); + } + } + + // Add foreign key columns to SQL, if necessary + if (! $addMetaColumns && ! $class->containsForeignIdentifier) { + continue; + } + + // Add foreign key columns of class and also parent classes + foreach ($class->associationMappings as $assoc) { + if ( + ! $assoc->isToOneOwningSide() + || ( ! $addMetaColumns && ! isset($assoc->id)) + ) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + $isIdentifier = (isset($assoc->id) && $assoc->id === true); + $owningClass = isset($assoc->inherited) ? $this->em->getClassMetadata($assoc->inherited) : $class; + $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias); + + foreach ($assoc->joinColumns as $joinColumn) { + $columnName = $joinColumn->name; + $columnAlias = $this->getSQLColumnAlias($columnName); + $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + + $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; + + $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType); + } + } + + // Add foreign key columns to SQL, if necessary + if (! $addMetaColumns) { + continue; + } + + // Add foreign key columns of subclasses + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + foreach ($subClass->associationMappings as $assoc) { + // Skip if association is inherited + if (isset($assoc->inherited)) { + continue; + } + + if ($assoc->isToOneOwningSide()) { + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + foreach ($assoc->joinColumns as $joinColumn) { + $columnName = $joinColumn->name; + $columnAlias = $this->getSQLColumnAlias($columnName); + $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); + + $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform); + $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; + + $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType); + } + } + } + } + } + + return $sql . implode(', ', $sqlSelectExpressions); + } + + /** + * Walks down a FromClause AST node, thereby generating the appropriate SQL. + */ + public function walkFromClause(AST\FromClause $fromClause): string + { + $identificationVarDecls = $fromClause->identificationVariableDeclarations; + $sqlParts = []; + + foreach ($identificationVarDecls as $identificationVariableDecl) { + $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl); + } + + return ' FROM ' . implode(', ', $sqlParts); + } + + /** + * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL. + */ + public function walkIdentificationVariableDeclaration(AST\IdentificationVariableDeclaration $identificationVariableDecl): string + { + $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); + + if ($identificationVariableDecl->indexBy) { + $this->walkIndexBy($identificationVariableDecl->indexBy); + } + + foreach ($identificationVariableDecl->joins as $join) { + $sql .= $this->walkJoin($join); + } + + return $sql; + } + + /** + * Walks down a IndexBy AST node. + */ + public function walkIndexBy(AST\IndexBy $indexBy): void + { + $pathExpression = $indexBy->singleValuedPathExpression; + $alias = $pathExpression->identificationVariable; + assert($pathExpression->field !== null); + + switch ($pathExpression->type) { + case AST\PathExpression::TYPE_STATE_FIELD: + $field = $pathExpression->field; + break; + + case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: + // Just use the foreign key, i.e. u.group_id + $fieldName = $pathExpression->field; + $class = $this->getMetadataForDqlAlias($alias); + + if (isset($class->associationMappings[$fieldName]->inherited)) { + $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); + } + + $association = $class->associationMappings[$fieldName]; + + if (! $association->isOwningSide()) { + throw QueryException::associationPathInverseSideNotSupported($pathExpression); + } + + assert($association->isToOneOwningSide()); + + if (count($association->sourceToTargetKeyColumns) > 1) { + throw QueryException::associationPathCompositeKeyNotSupported(); + } + + $field = reset($association->targetToSourceKeyColumns); + break; + + default: + throw QueryException::invalidPathExpression($pathExpression); + } + + if (isset($this->scalarFields[$alias][$field])) { + $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]); + + return; + } + + $this->rsm->addIndexBy($alias, $field); + } + + /** + * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL. + */ + public function walkRangeVariableDeclaration(AST\RangeVariableDeclaration $rangeVariableDeclaration): string + { + return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false); + } + + /** + * Generate appropriate SQL for RangeVariableDeclaration AST node + */ + private function generateRangeVariableDeclarationSQL( + AST\RangeVariableDeclaration $rangeVariableDeclaration, + bool $buildNestedJoins, + ): string { + $class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName); + $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable; + + if ($rangeVariableDeclaration->isRoot) { + $this->rootAliases[] = $dqlAlias; + } + + $sql = $this->platform->appendLockHint( + $this->quoteStrategy->getTableName($class, $this->platform) . ' ' . + $this->getSQLTableAlias($class->getTableName(), $dqlAlias), + $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE, + ); + + if (! $class->isInheritanceTypeJoined()) { + return $sql; + } + + $classTableInheritanceJoins = $this->generateClassTableInheritanceJoins($class, $dqlAlias); + + if (! $buildNestedJoins) { + return $sql . $classTableInheritanceJoins; + } + + return $classTableInheritanceJoins === '' ? $sql : '(' . $sql . $classTableInheritanceJoins . ')'; + } + + /** + * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. + * + * @psalm-param AST\Join::JOIN_TYPE_* $joinType + * + * @throws QueryException + */ + public function walkJoinAssociationDeclaration( + AST\JoinAssociationDeclaration $joinAssociationDeclaration, + int $joinType = AST\Join::JOIN_TYPE_INNER, + AST\ConditionalExpression|AST\Phase2OptimizableConditional|null $condExpr = null, + ): string { + $sql = ''; + + $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; + $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; + $indexBy = $joinAssociationDeclaration->indexBy; + + $relation = $this->queryComponents[$joinedDqlAlias]['relation'] ?? null; + assert($relation !== null); + $targetClass = $this->em->getClassMetadata($relation->targetEntity); + $sourceClass = $this->em->getClassMetadata($relation->sourceEntity); + $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); + + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); + $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); + + // Ensure we got the owning side, since it has all mapping info + $assoc = $this->em->getMetadataFactory()->getOwningSide($relation); + + if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) { + if ($relation->isToMany()) { + throw QueryException::iterateWithFetchJoinNotAllowed($assoc); + } + } + + $fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch; + + if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) { + throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName); + } + + // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot + // be the owning side and previously we ensured that $assoc is always the owning side of the associations. + // The owning side is necessary at this point because only it contains the JoinColumn information. + switch (true) { + case $assoc->isToOne(): + assert($assoc->isToOneOwningSide()); + $conditions = []; + + foreach ($assoc->joinColumns as $joinColumn) { + $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + if ($relation->isOwningSide()) { + $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; + + continue; + } + + $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn; + } + + // Apply remaining inheritance restrictions + $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + $targetTableJoin = [ + 'table' => $targetTableName . ' ' . $targetTableAlias, + 'condition' => implode(' AND ', $conditions), + ]; + break; + + case $assoc->isManyToMany(): + // Join relation table + $joinTable = $assoc->joinTable; + $joinTableAlias = $this->getSQLTableAlias($joinTable->name, $joinedDqlAlias); + $joinTableName = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform); + + $conditions = []; + $relationColumns = $relation->isOwningSide() + ? $assoc->joinTable->joinColumns + : $assoc->joinTable->inverseJoinColumns; + + foreach ($relationColumns as $joinColumn) { + $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; + } + + $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); + + // Join target table + $sql .= $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; + + $conditions = []; + $relationColumns = $relation->isOwningSide() + ? $assoc->joinTable->inverseJoinColumns + : $assoc->joinTable->joinColumns; + + foreach ($relationColumns as $joinColumn) { + $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); + $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); + + $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; + } + + // Apply remaining inheritance restrictions + $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + $targetTableJoin = [ + 'table' => $targetTableName . ' ' . $targetTableAlias, + 'condition' => implode(' AND ', $conditions), + ]; + break; + + default: + throw new BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY'); + } + + // Handle WITH clause + $withCondition = $condExpr === null ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')'); + + if ($targetClass->isInheritanceTypeJoined()) { + $ctiJoins = $this->generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); + // If we have WITH condition, we need to build nested joins for target class table and cti joins + if ($withCondition && $ctiJoins) { + $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition']; + } else { + $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins; + } + } else { + $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition']; + } + + if ($withCondition) { + $sql .= ' AND ' . $withCondition; + } + + // Apply the indexes + if ($indexBy) { + // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. + $this->walkIndexBy($indexBy); + } elseif ($relation->isIndexed()) { + $this->rsm->addIndexBy($joinedDqlAlias, $relation->indexBy()); + } + + return $sql; + } + + /** + * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. + */ + public function walkFunction(AST\Functions\FunctionNode $function): string + { + return $function->getSql($this); + } + + /** + * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. + */ + public function walkOrderByClause(AST\OrderByClause $orderByClause): string + { + $orderByItems = array_map($this->walkOrderByItem(...), $orderByClause->orderByItems); + + $collectionOrderByItems = $this->generateOrderedCollectionOrderByItems(); + if ($collectionOrderByItems !== '') { + $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); + } + + return ' ORDER BY ' . implode(', ', $orderByItems); + } + + /** + * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. + */ + public function walkOrderByItem(AST\OrderByItem $orderByItem): string + { + $type = strtoupper($orderByItem->type); + $expr = $orderByItem->expression; + $sql = $expr instanceof AST\Node + ? $expr->dispatch($this) + : $this->walkResultVariable($this->queryComponents[$expr]['token']->value); + + $this->orderedColumnsMap[$sql] = $type; + + if ($expr instanceof AST\Subselect) { + return '(' . $sql . ') ' . $type; + } + + return $sql . ' ' . $type; + } + + /** + * Walks down a HavingClause AST node, thereby generating the appropriate SQL. + */ + public function walkHavingClause(AST\HavingClause $havingClause): string + { + return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression); + } + + /** + * Walks down a Join AST node and creates the corresponding SQL. + */ + public function walkJoin(AST\Join $join): string + { + $joinType = $join->joinType; + $joinDeclaration = $join->joinAssociationDeclaration; + + $sql = $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER + ? ' LEFT JOIN ' + : ' INNER JOIN '; + + switch (true) { + case $joinDeclaration instanceof AST\RangeVariableDeclaration: + $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); + $dqlAlias = $joinDeclaration->aliasIdentificationVariable; + $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); + $conditions = []; + + if ($join->conditionalExpression) { + $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + } + + $isUnconditionalJoin = $conditions === []; + $condExprConjunction = $class->isInheritanceTypeJoined() && $joinType !== AST\Join::JOIN_TYPE_LEFT && $joinType !== AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin + ? ' AND ' + : ' ON '; + + $sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, ! $isUnconditionalJoin); + + // Apply remaining inheritance restrictions + $discrSql = $this->generateDiscriminatorColumnConditionSQL([$dqlAlias]); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + if ($conditions) { + $sql .= $condExprConjunction . implode(' AND ', $conditions); + } + + break; + + case $joinDeclaration instanceof AST\JoinAssociationDeclaration: + $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression); + break; + } + + return $sql; + } + + /** + * Walks down a CoalesceExpression AST node and generates the corresponding SQL. + */ + public function walkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): string + { + $sql = 'COALESCE('; + + $scalarExpressions = []; + + foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { + $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); + } + + return $sql . implode(', ', $scalarExpressions) . ')'; + } + + /** + * Walks down a NullIfExpression AST node and generates the corresponding SQL. + */ + public function walkNullIfExpression(AST\NullIfExpression $nullIfExpression): string + { + $firstExpression = is_string($nullIfExpression->firstExpression) + ? $this->conn->quote($nullIfExpression->firstExpression) + : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression); + + $secondExpression = is_string($nullIfExpression->secondExpression) + ? $this->conn->quote($nullIfExpression->secondExpression) + : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression); + + return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')'; + } + + /** + * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL. + */ + public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): string + { + $sql = 'CASE'; + + foreach ($generalCaseExpression->whenClauses as $whenClause) { + $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression); + $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression); + } + + $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END'; + + return $sql; + } + + /** + * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL. + */ + public function walkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): string + { + $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand); + + foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { + $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); + $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); + } + + $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END'; + + return $sql; + } + + /** + * Walks down a SelectExpression AST node and generates the corresponding SQL. + */ + public function walkSelectExpression(AST\SelectExpression $selectExpression): string + { + $sql = ''; + $expr = $selectExpression->expression; + $hidden = $selectExpression->hiddenAliasResultVariable; + + switch (true) { + case $expr instanceof AST\PathExpression: + if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { + throw QueryException::invalidPathExpression($expr); + } + + assert($expr->field !== null); + $fieldName = $expr->field; + $dqlAlias = $expr->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName; + $tableName = $class->isInheritanceTypeJoined() + ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) + : $class->getTableName(); + + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $fieldMapping = $class->fieldMappings[$fieldName]; + $columnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); + $col = $sqlTableAlias . '.' . $columnName; + + $type = Type::getType($fieldMapping->type); + $col = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform()); + + $sql .= $col . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + + if (! $hidden) { + $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping->type); + $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias; + + if (! empty($fieldMapping->enumType)) { + $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); + } + } + + break; + + case $expr instanceof AST\AggregateExpression: + case $expr instanceof AST\Functions\FunctionNode: + case $expr instanceof AST\SimpleArithmeticExpression: + case $expr instanceof AST\ArithmeticTerm: + case $expr instanceof AST\ArithmeticFactor: + case $expr instanceof AST\ParenthesisExpression: + case $expr instanceof AST\Literal: + case $expr instanceof AST\NullIfExpression: + case $expr instanceof AST\CoalesceExpression: + case $expr instanceof AST\GeneralCaseExpression: + case $expr instanceof AST\SimpleCaseExpression: + $columnAlias = $this->getSQLColumnAlias('sclr'); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + + if ($hidden) { + break; + } + + if (! $expr instanceof Query\AST\TypedExpression) { + // Conceptually we could resolve field type here by traverse through AST to retrieve field type, + // but this is not a feasible solution; assume 'string'. + $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); + + break; + } + + $this->rsm->addScalarResult($columnAlias, $resultAlias, Type::getTypeRegistry()->lookupName($expr->getReturnType())); + + break; + + case $expr instanceof AST\Subselect: + $columnAlias = $this->getSQLColumnAlias('sclr'); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + + if (! $hidden) { + // We cannot resolve field type here; assume 'string'. + $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); + } + + break; + + case $expr instanceof AST\NewObjectExpression: + $sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable); + break; + + default: + $dqlAlias = $expr; + $class = $this->getMetadataForDqlAlias($dqlAlias); + $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; + + if (! isset($this->selectedClasses[$dqlAlias])) { + $this->selectedClasses[$dqlAlias] = [ + 'class' => $class, + 'dqlAlias' => $dqlAlias, + 'resultAlias' => $resultAlias, + ]; + } + + $sqlParts = []; + + // Select all fields from the queried class + foreach ($class->fieldMappings as $fieldName => $mapping) { + $tableName = isset($mapping->inherited) + ? $this->em->getClassMetadata($mapping->inherited)->getTableName() + : $class->getTableName(); + + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSQLColumnAlias($mapping->columnName); + $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + + $col = $sqlTableAlias . '.' . $quotedColumnName; + + $type = Type::getType($mapping->type); + $col = $type->convertToPHPValueSQL($col, $this->platform); + + $sqlParts[] = $col . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + + $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); + + if (! empty($mapping->enumType)) { + $this->rsm->addEnumResult($columnAlias, $mapping->enumType); + } + } + + // Add any additional fields of subclasses (excluding inherited fields) + // 1) on Single Table Inheritance: always, since its marginal overhead + // 2) on Class Table Inheritance + foreach ($class->subClasses as $subClassName) { + $subClass = $this->em->getClassMetadata($subClassName); + $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); + + foreach ($subClass->fieldMappings as $fieldName => $mapping) { + if (isset($mapping->inherited)) { + continue; + } + + $columnAlias = $this->getSQLColumnAlias($mapping->columnName); + $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); + + $col = $sqlTableAlias . '.' . $quotedColumnName; + + $type = Type::getType($mapping->type); + $col = $type->convertToPHPValueSQL($col, $this->platform); + + $sqlParts[] = $col . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + + $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); + } + } + + $sql .= implode(', ', $sqlParts); + } + + return $sql; + } + + public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string + { + return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')'; + } + + /** + * Walks down a Subselect AST node, thereby generating the appropriate SQL. + */ + public function walkSubselect(AST\Subselect $subselect): string + { + $useAliasesBefore = $this->useSqlTableAliases; + $rootAliasesBefore = $this->rootAliases; + + $this->rootAliases = []; // reset the rootAliases for the subselect + $this->useSqlTableAliases = true; + + $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); + $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); + $sql .= $this->walkWhereClause($subselect->whereClause); + + $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; + $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; + $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; + + $this->rootAliases = $rootAliasesBefore; // put the main aliases back + $this->useSqlTableAliases = $useAliasesBefore; + + return $sql; + } + + /** + * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. + */ + public function walkSubselectFromClause(AST\SubselectFromClause $subselectFromClause): string + { + $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations; + $sqlParts = []; + + foreach ($identificationVarDecls as $subselectIdVarDecl) { + $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl); + } + + return ' FROM ' . implode(', ', $sqlParts); + } + + /** + * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. + */ + public function walkSimpleSelectClause(AST\SimpleSelectClause $simpleSelectClause): string + { + return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '') + . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression); + } + + public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression): string + { + return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this)); + } + + public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string + { + $sqlSelectExpressions = []; + $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++; + + foreach ($newObjectExpression->args as $argIndex => $e) { + $resultAlias = $this->scalarResultCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); + $fieldType = 'string'; + + switch (true) { + case $e instanceof AST\NewObjectExpression: + $sqlSelectExpressions[] = $e->dispatch($this); + break; + + case $e instanceof AST\Subselect: + $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias; + break; + + case $e instanceof AST\PathExpression: + assert($e->field !== null); + $dqlAlias = $e->identificationVariable; + $class = $this->getMetadataForDqlAlias($dqlAlias); + $fieldName = $e->field; + $fieldMapping = $class->fieldMappings[$fieldName]; + $fieldType = $fieldMapping->type; + $col = trim($e->dispatch($this)); + + $type = Type::getType($fieldType); + $col = $type->convertToPHPValueSQL($col, $this->platform); + + $sqlSelectExpressions[] = $col . ' AS ' . $columnAlias; + + if (! empty($fieldMapping->enumType)) { + $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); + } + + break; + + case $e instanceof AST\Literal: + switch ($e->type) { + case AST\Literal::BOOLEAN: + $fieldType = 'boolean'; + break; + + case AST\Literal::NUMERIC: + $fieldType = is_float($e->value) ? 'float' : 'integer'; + break; + } + + $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; + break; + + default: + $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; + break; + } + + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType); + + $this->rsm->newObjectMappings[$columnAlias] = [ + 'className' => $newObjectExpression->className, + 'objIndex' => $objIndex, + 'argIndex' => $argIndex, + ]; + } + + return implode(', ', $sqlSelectExpressions); + } + + /** + * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. + */ + public function walkSimpleSelectExpression(AST\SimpleSelectExpression $simpleSelectExpression): string + { + $expr = $simpleSelectExpression->expression; + $sql = ' '; + + switch (true) { + case $expr instanceof AST\PathExpression: + $sql .= $this->walkPathExpression($expr); + break; + + case $expr instanceof AST\Subselect: + $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $columnAlias = 'sclr' . $this->aliasCounter++; + $this->scalarResultAliasMap[$alias] = $columnAlias; + + $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; + break; + + case $expr instanceof AST\Functions\FunctionNode: + case $expr instanceof AST\SimpleArithmeticExpression: + case $expr instanceof AST\ArithmeticTerm: + case $expr instanceof AST\ArithmeticFactor: + case $expr instanceof AST\Literal: + case $expr instanceof AST\NullIfExpression: + case $expr instanceof AST\CoalesceExpression: + case $expr instanceof AST\GeneralCaseExpression: + case $expr instanceof AST\SimpleCaseExpression: + $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; + + $columnAlias = $this->getSQLColumnAlias('sclr'); + $this->scalarResultAliasMap[$alias] = $columnAlias; + + $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; + break; + + case $expr instanceof AST\ParenthesisExpression: + $sql .= $this->walkParenthesisExpression($expr); + break; + + default: // IdentificationVariable + $sql .= $this->walkEntityIdentificationVariable($expr); + break; + } + + return $sql; + } + + /** + * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. + */ + public function walkAggregateExpression(AST\AggregateExpression $aggExpression): string + { + return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '') + . $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) . ')'; + } + + /** + * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. + */ + public function walkGroupByClause(AST\GroupByClause $groupByClause): string + { + $sqlParts = []; + + foreach ($groupByClause->groupByItems as $groupByItem) { + $sqlParts[] = $this->walkGroupByItem($groupByItem); + } + + return ' GROUP BY ' . implode(', ', $sqlParts); + } + + /** + * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. + */ + public function walkGroupByItem(AST\PathExpression|string $groupByItem): string + { + // StateFieldPathExpression + if (! is_string($groupByItem)) { + return $this->walkPathExpression($groupByItem); + } + + // ResultVariable + if (isset($this->queryComponents[$groupByItem]['resultVariable'])) { + $resultVariable = $this->queryComponents[$groupByItem]['resultVariable']; + + if ($resultVariable instanceof AST\PathExpression) { + return $this->walkPathExpression($resultVariable); + } + + if ($resultVariable instanceof AST\Node && isset($resultVariable->pathExpression)) { + return $this->walkPathExpression($resultVariable->pathExpression); + } + + return $this->walkResultVariable($groupByItem); + } + + // IdentificationVariable + $sqlParts = []; + + foreach ($this->getMetadataForDqlAlias($groupByItem)->fieldNames as $field) { + $item = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field); + $item->type = AST\PathExpression::TYPE_STATE_FIELD; + + $sqlParts[] = $this->walkPathExpression($item); + } + + foreach ($this->getMetadataForDqlAlias($groupByItem)->associationMappings as $mapping) { + if ($mapping->isToOneOwningSide()) { + $item = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping->fieldName); + $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; + + $sqlParts[] = $this->walkPathExpression($item); + } + } + + return implode(', ', $sqlParts); + } + + /** + * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. + */ + public function walkDeleteClause(AST\DeleteClause $deleteClause): string + { + $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); + $tableName = $class->getTableName(); + $sql = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform); + + $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); + $this->rootAliases[] = $deleteClause->aliasIdentificationVariable; + + return $sql; + } + + /** + * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. + */ + public function walkUpdateClause(AST\UpdateClause $updateClause): string + { + $class = $this->em->getClassMetadata($updateClause->abstractSchemaName); + $tableName = $class->getTableName(); + $sql = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform); + + $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable); + $this->rootAliases[] = $updateClause->aliasIdentificationVariable; + + return $sql . ' SET ' . implode(', ', array_map($this->walkUpdateItem(...), $updateClause->updateItems)); + } + + /** + * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. + */ + public function walkUpdateItem(AST\UpdateItem $updateItem): string + { + $useTableAliasesBefore = $this->useSqlTableAliases; + $this->useSqlTableAliases = false; + + $sql = $this->walkPathExpression($updateItem->pathExpression) . ' = '; + $newValue = $updateItem->newValue; + + $sql .= match (true) { + $newValue instanceof AST\Node => $newValue->dispatch($this), + $newValue === null => 'NULL', + }; + + $this->useSqlTableAliases = $useTableAliasesBefore; + + return $sql; + } + + /** + * Walks down a WhereClause AST node, thereby generating the appropriate SQL. + * + * WhereClause or not, the appropriate discriminator sql is added. + */ + public function walkWhereClause(AST\WhereClause|null $whereClause): string + { + $condSql = $whereClause !== null ? $this->walkConditionalExpression($whereClause->conditionalExpression) : ''; + $discrSql = $this->generateDiscriminatorColumnConditionSQL($this->rootAliases); + + if ($this->em->hasFilters()) { + $filterClauses = []; + foreach ($this->rootAliases as $dqlAlias) { + $class = $this->getMetadataForDqlAlias($dqlAlias); + $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); + + $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); + if ($filterExpr) { + $filterClauses[] = $filterExpr; + } + } + + if (count($filterClauses)) { + if ($condSql) { + $condSql = '(' . $condSql . ') AND '; + } + + $condSql .= implode(' AND ', $filterClauses); + } + } + + if ($condSql) { + return ' WHERE ' . (! $discrSql ? $condSql : '(' . $condSql . ') AND ' . $discrSql); + } + + if ($discrSql) { + return ' WHERE ' . $discrSql; + } + + return ''; + } + + /** + * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalExpression( + AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr, + ): string { + // Phase 2 AST optimization: Skip processing of ConditionalExpression + // if only one ConditionalTerm is defined + if (! ($condExpr instanceof AST\ConditionalExpression)) { + return $this->walkConditionalTerm($condExpr); + } + + return implode(' OR ', array_map($this->walkConditionalTerm(...), $condExpr->conditionalTerms)); + } + + /** + * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalTerm( + AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm, + ): string { + // Phase 2 AST optimization: Skip processing of ConditionalTerm + // if only one ConditionalFactor is defined + if (! ($condTerm instanceof AST\ConditionalTerm)) { + return $this->walkConditionalFactor($condTerm); + } + + return implode(' AND ', array_map($this->walkConditionalFactor(...), $condTerm->conditionalFactors)); + } + + /** + * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalFactor( + AST\ConditionalFactor|AST\ConditionalPrimary $factor, + ): string { + // Phase 2 AST optimization: Skip processing of ConditionalFactor + // if only one ConditionalPrimary is defined + return ! ($factor instanceof AST\ConditionalFactor) + ? $this->walkConditionalPrimary($factor) + : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary); + } + + /** + * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. + */ + public function walkConditionalPrimary(AST\ConditionalPrimary $primary): string + { + if ($primary->isSimpleConditionalExpression()) { + return $primary->simpleConditionalExpression->dispatch($this); + } + + if ($primary->isConditionalExpression()) { + $condExpr = $primary->conditionalExpression; + + return '(' . $this->walkConditionalExpression($condExpr) . ')'; + } + + throw new LogicException('Unexpected state of ConditionalPrimary node.'); + } + + /** + * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. + */ + public function walkExistsExpression(AST\ExistsExpression $existsExpr): string + { + $sql = $existsExpr->not ? 'NOT ' : ''; + + $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')'; + + return $sql; + } + + /** + * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. + */ + public function walkCollectionMemberExpression(AST\CollectionMemberExpression $collMemberExpr): string + { + $sql = $collMemberExpr->not ? 'NOT ' : ''; + $sql .= 'EXISTS (SELECT 1 FROM '; + + $entityExpr = $collMemberExpr->entityExpression; + $collPathExpr = $collMemberExpr->collectionValuedPathExpression; + assert($collPathExpr->field !== null); + + $fieldName = $collPathExpr->field; + $dqlAlias = $collPathExpr->identificationVariable; + + $class = $this->getMetadataForDqlAlias($dqlAlias); + + switch (true) { + // InputParameter + case $entityExpr instanceof AST\InputParameter: + $dqlParamKey = $entityExpr->name; + $entitySql = '?'; + break; + + // SingleValuedAssociationPathExpression | IdentificationVariable + case $entityExpr instanceof AST\PathExpression: + $entitySql = $this->walkPathExpression($entityExpr); + break; + + default: + throw new BadMethodCallException('Not implemented'); + } + + $assoc = $class->associationMappings[$fieldName]; + + if ($assoc->isOneToMany()) { + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); + $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + + $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE '; + + $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; + assert($owningAssoc->isManyToOne()); + $sqlParts = []; + + foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { + $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform); + + $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; + } + + foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) { + if (isset($dqlParamKey)) { + $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); + } + + $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql; + } + + $sql .= implode(' AND ', $sqlParts); + } else { // many-to-many + $targetClass = $this->em->getClassMetadata($assoc->targetEntity); + + $owningAssoc = $this->em->getMetadataFactory()->getOwningSide($assoc); + assert($owningAssoc->isManyToManyOwningSide()); + $joinTable = $owningAssoc->joinTable; + + // SQL table aliases + $joinTableAlias = $this->getSQLTableAlias($joinTable->name); + $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + + $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias . ' WHERE '; + + $joinColumns = $assoc->isOwningSide() ? $joinTable->joinColumns : $joinTable->inverseJoinColumns; + $sqlParts = []; + + foreach ($joinColumns as $joinColumn) { + $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn->referencedColumnName], $class, $this->platform); + + $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' = ' . $sourceTableAlias . '.' . $targetColumn; + } + + $joinColumns = $assoc->isOwningSide() ? $joinTable->inverseJoinColumns : $joinTable->joinColumns; + + foreach ($joinColumns as $joinColumn) { + if (isset($dqlParamKey)) { + $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); + } + + $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' IN (' . $entitySql . ')'; + } + + $sql .= implode(' AND ', $sqlParts); + } + + return $sql . ')'; + } + + /** + * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. + */ + public function walkEmptyCollectionComparisonExpression(AST\EmptyCollectionComparisonExpression $emptyCollCompExpr): string + { + $sizeFunc = new AST\Functions\SizeFunction('size'); + $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression; + + return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0'); + } + + /** + * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. + */ + public function walkNullComparisonExpression(AST\NullComparisonExpression $nullCompExpr): string + { + $expression = $nullCompExpr->expression; + $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL'; + + // Handle ResultVariable + if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) { + return $this->walkResultVariable($expression) . $comparison; + } + + // Handle InputParameter mapping inclusion to ParserResult + if ($expression instanceof AST\InputParameter) { + return $this->walkInputParameter($expression) . $comparison; + } + + assert(! is_string($expression)); + + return $expression->dispatch($this) . $comparison; + } + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + */ + public function walkInListExpression(AST\InListExpression $inExpr): string + { + return $this->walkArithmeticExpression($inExpr->expression) + . ($inExpr->not ? ' NOT' : '') . ' IN (' + . implode(', ', array_map($this->walkInParameter(...), $inExpr->literals)) + . ')'; + } + + /** + * Walks down an InExpression AST node, thereby generating the appropriate SQL. + */ + public function walkInSubselectExpression(AST\InSubselectExpression $inExpr): string + { + return $this->walkArithmeticExpression($inExpr->expression) + . ($inExpr->not ? ' NOT' : '') . ' IN (' + . $this->walkSubselect($inExpr->subselect) + . ')'; + } + + /** + * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. + * + * @throws QueryException + */ + public function walkInstanceOfExpression(AST\InstanceOfExpression $instanceOfExpr): string + { + $sql = ''; + + $dqlAlias = $instanceOfExpr->identificationVariable; + $discrClass = $class = $this->getMetadataForDqlAlias($dqlAlias); + + if ($class->discriminatorColumn) { + $discrClass = $this->em->getClassMetadata($class->rootEntityName); + } + + if ($this->useSqlTableAliases) { + $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.'; + } + + $sql .= $class->getDiscriminatorColumn()->name . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); + $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr); + + return $sql; + } + + public function walkInParameter(mixed $inParam): string + { + return $inParam instanceof AST\InputParameter + ? $this->walkInputParameter($inParam) + : $this->walkArithmeticExpression($inParam); + } + + /** + * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkLiteral(AST\Literal $literal): string + { + return match ($literal->type) { + AST\Literal::STRING => $this->conn->quote($literal->value), + AST\Literal::BOOLEAN => (string) $this->conn->getDatabasePlatform()->convertBooleans(strtolower($literal->value) === 'true'), + AST\Literal::NUMERIC => (string) $literal->value, + default => throw QueryException::invalidLiteral($literal), + }; + } + + /** + * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. + */ + public function walkBetweenExpression(AST\BetweenExpression $betweenExpr): string + { + $sql = $this->walkArithmeticExpression($betweenExpr->expression); + + if ($betweenExpr->not) { + $sql .= ' NOT'; + } + + $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression) + . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression); + + return $sql; + } + + /** + * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. + */ + public function walkLikeExpression(AST\LikeExpression $likeExpr): string + { + $stringExpr = $likeExpr->stringExpression; + if (is_string($stringExpr)) { + if (! isset($this->queryComponents[$stringExpr]['resultVariable'])) { + throw new LogicException(sprintf('No result variable found for string expression "%s".', $stringExpr)); + } + + $leftExpr = $this->walkResultVariable($stringExpr); + } else { + $leftExpr = $stringExpr->dispatch($this); + } + + $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; + + if ($likeExpr->stringPattern instanceof AST\InputParameter) { + $sql .= $this->walkInputParameter($likeExpr->stringPattern); + } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) { + $sql .= $this->walkFunction($likeExpr->stringPattern); + } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) { + $sql .= $this->walkPathExpression($likeExpr->stringPattern); + } else { + $sql .= $this->walkLiteral($likeExpr->stringPattern); + } + + if ($likeExpr->escapeChar) { + $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar); + } + + return $sql; + } + + /** + * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. + */ + public function walkStateFieldPathExpression(AST\PathExpression $stateFieldPathExpression): string + { + return $this->walkPathExpression($stateFieldPathExpression); + } + + /** + * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. + */ + public function walkComparisonExpression(AST\ComparisonExpression $compExpr): string + { + $leftExpr = $compExpr->leftExpression; + $rightExpr = $compExpr->rightExpression; + $sql = ''; + + $sql .= $leftExpr instanceof AST\Node + ? $leftExpr->dispatch($this) + : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr)); + + $sql .= ' ' . $compExpr->operator . ' '; + + $sql .= $rightExpr instanceof AST\Node + ? $rightExpr->dispatch($this) + : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr)); + + return $sql; + } + + /** + * Walks down an InputParameter AST node, thereby generating the appropriate SQL. + */ + public function walkInputParameter(AST\InputParameter $inputParam): string + { + $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++); + + $parameter = $this->query->getParameter($inputParam->name); + + if ($parameter) { + $type = $parameter->getType(); + if (is_string($type) && Type::hasType($type)) { + return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform); + } + } + + return '?'; + } + + /** + * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticExpression(AST\ArithmeticExpression $arithmeticExpr): string + { + return $arithmeticExpr->isSimpleArithmeticExpression() + ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression) + : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')'; + } + + /** + * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. + */ + public function walkSimpleArithmeticExpression(AST\Node|string $simpleArithmeticExpr): string + { + if (! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) { + return $this->walkArithmeticTerm($simpleArithmeticExpr); + } + + return implode(' ', array_map($this->walkArithmeticTerm(...), $simpleArithmeticExpr->arithmeticTerms)); + } + + /** + * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticTerm(AST\Node|string $term): string + { + if (is_string($term)) { + return isset($this->queryComponents[$term]) + ? $this->walkResultVariable($this->queryComponents[$term]['token']->value) + : $term; + } + + // Phase 2 AST optimization: Skip processing of ArithmeticTerm + // if only one ArithmeticFactor is defined + if (! ($term instanceof AST\ArithmeticTerm)) { + return $this->walkArithmeticFactor($term); + } + + return implode(' ', array_map($this->walkArithmeticFactor(...), $term->arithmeticFactors)); + } + + /** + * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticFactor(AST\Node|string $factor): string + { + if (is_string($factor)) { + return isset($this->queryComponents[$factor]) + ? $this->walkResultVariable($this->queryComponents[$factor]['token']->value) + : $factor; + } + + // Phase 2 AST optimization: Skip processing of ArithmeticFactor + // if only one ArithmeticPrimary is defined + if (! ($factor instanceof AST\ArithmeticFactor)) { + return $this->walkArithmeticPrimary($factor); + } + + $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : ''); + + return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary); + } + + /** + * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkArithmeticPrimary(AST\Node|string $primary): string + { + if ($primary instanceof AST\SimpleArithmeticExpression) { + return '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; + } + + if ($primary instanceof AST\Node) { + return $primary->dispatch($this); + } + + return $this->walkEntityIdentificationVariable($primary); + } + + /** + * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkStringPrimary(AST\Node|string $stringPrimary): string + { + return is_string($stringPrimary) + ? $this->conn->quote($stringPrimary) + : $stringPrimary->dispatch($this); + } + + /** + * Walks down a ResultVariable that represents an AST node, thereby generating the appropriate SQL. + */ + public function walkResultVariable(string $resultVariable): string + { + if (! isset($this->scalarResultAliasMap[$resultVariable])) { + throw new InvalidArgumentException(sprintf('Unknown result variable: %s', $resultVariable)); + } + + $resultAlias = $this->scalarResultAliasMap[$resultVariable]; + + if (is_array($resultAlias)) { + return implode(', ', $resultAlias); + } + + return $resultAlias; + } + + /** + * @return string The list in parentheses of valid child discriminators from the given class + * + * @throws QueryException + */ + private function getChildDiscriminatorsFromClassMetadata( + ClassMetadata $rootClass, + AST\InstanceOfExpression $instanceOfExpr, + ): string { + $sqlParameterList = []; + $discriminators = []; + foreach ($instanceOfExpr->value as $parameter) { + if ($parameter instanceof AST\InputParameter) { + $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name; + $sqlParameterList[] = $this->walkInParameter($parameter); + continue; + } + + $metadata = $this->em->getClassMetadata($parameter); + + if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) { + throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name); + } + + $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em); + } + + foreach (array_keys($discriminators) as $discriminatorValue) { + $sqlParameterList[] = $rootClass->getDiscriminatorColumn()->type === 'integer' && is_int($discriminatorValue) + ? $discriminatorValue + : $this->conn->quote((string) $discriminatorValue); + } + + return '(' . implode(', ', $sqlParameterList) . ')'; + } +} -- cgit v1.2.3