diff options
author | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
---|---|---|
committer | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
commit | bf6655a534a6775d30cafa67bd801276bda1d98d (patch) | |
tree | c6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Query/SqlWalker.php | |
parent | 94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff) | |
download | AppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip |
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Query/SqlWalker.php')
-rw-r--r-- | vendor/doctrine/orm/src/Query/SqlWalker.php | 2264 |
1 files changed, 2264 insertions, 0 deletions
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 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use BadMethodCallException; | ||
8 | use Doctrine\DBAL\Connection; | ||
9 | use Doctrine\DBAL\LockMode; | ||
10 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
11 | use Doctrine\DBAL\Types\Type; | ||
12 | use Doctrine\ORM\EntityManagerInterface; | ||
13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
14 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
15 | use Doctrine\ORM\OptimisticLockException; | ||
16 | use Doctrine\ORM\Query; | ||
17 | use Doctrine\ORM\Utility\HierarchyDiscriminatorResolver; | ||
18 | use Doctrine\ORM\Utility\LockSqlHelper; | ||
19 | use Doctrine\ORM\Utility\PersisterHelper; | ||
20 | use InvalidArgumentException; | ||
21 | use LogicException; | ||
22 | |||
23 | use function array_diff; | ||
24 | use function array_filter; | ||
25 | use function array_keys; | ||
26 | use function array_map; | ||
27 | use function array_merge; | ||
28 | use function assert; | ||
29 | use function count; | ||
30 | use function implode; | ||
31 | use function is_array; | ||
32 | use function is_float; | ||
33 | use function is_int; | ||
34 | use function is_numeric; | ||
35 | use function is_string; | ||
36 | use function preg_match; | ||
37 | use function reset; | ||
38 | use function sprintf; | ||
39 | use function strtolower; | ||
40 | use function strtoupper; | ||
41 | use function trim; | ||
42 | |||
43 | /** | ||
44 | * The SqlWalker walks over a DQL AST and constructs the corresponding SQL. | ||
45 | * | ||
46 | * @psalm-import-type QueryComponent from Parser | ||
47 | * @psalm-consistent-constructor | ||
48 | */ | ||
49 | class SqlWalker | ||
50 | { | ||
51 | use LockSqlHelper; | ||
52 | |||
53 | public const HINT_DISTINCT = 'doctrine.distinct'; | ||
54 | |||
55 | private readonly ResultSetMapping $rsm; | ||
56 | |||
57 | /** | ||
58 | * Counter for generating unique column aliases. | ||
59 | */ | ||
60 | private int $aliasCounter = 0; | ||
61 | |||
62 | /** | ||
63 | * Counter for generating unique table aliases. | ||
64 | */ | ||
65 | private int $tableAliasCounter = 0; | ||
66 | |||
67 | /** | ||
68 | * Counter for generating unique scalar result. | ||
69 | */ | ||
70 | private int $scalarResultCounter = 1; | ||
71 | |||
72 | /** | ||
73 | * Counter for generating unique parameter indexes. | ||
74 | */ | ||
75 | private int $sqlParamIndex = 0; | ||
76 | |||
77 | /** | ||
78 | * Counter for generating indexes. | ||
79 | */ | ||
80 | private int $newObjectCounter = 0; | ||
81 | |||
82 | private readonly EntityManagerInterface $em; | ||
83 | private readonly Connection $conn; | ||
84 | |||
85 | /** @var mixed[] */ | ||
86 | private array $tableAliasMap = []; | ||
87 | |||
88 | /** | ||
89 | * Map from result variable names to their SQL column alias names. | ||
90 | * | ||
91 | * @psalm-var array<string|int, string|list<string>> | ||
92 | */ | ||
93 | private array $scalarResultAliasMap = []; | ||
94 | |||
95 | /** | ||
96 | * Map from Table-Alias + Column-Name to OrderBy-Direction. | ||
97 | * | ||
98 | * @var array<string, string> | ||
99 | */ | ||
100 | private array $orderedColumnsMap = []; | ||
101 | |||
102 | /** | ||
103 | * Map from DQL-Alias + Field-Name to SQL Column Alias. | ||
104 | * | ||
105 | * @var array<string, array<string, string>> | ||
106 | */ | ||
107 | private array $scalarFields = []; | ||
108 | |||
109 | /** | ||
110 | * A list of classes that appear in non-scalar SelectExpressions. | ||
111 | * | ||
112 | * @psalm-var array<string, array{class: ClassMetadata, dqlAlias: string, resultAlias: string|null}> | ||
113 | */ | ||
114 | private array $selectedClasses = []; | ||
115 | |||
116 | /** | ||
117 | * The DQL alias of the root class of the currently traversed query. | ||
118 | * | ||
119 | * @psalm-var list<string> | ||
120 | */ | ||
121 | private array $rootAliases = []; | ||
122 | |||
123 | /** | ||
124 | * Flag that indicates whether to generate SQL table aliases in the SQL. | ||
125 | * These should only be generated for SELECT queries, not for UPDATE/DELETE. | ||
126 | */ | ||
127 | private bool $useSqlTableAliases = true; | ||
128 | |||
129 | /** | ||
130 | * The database platform abstraction. | ||
131 | */ | ||
132 | private readonly AbstractPlatform $platform; | ||
133 | |||
134 | /** | ||
135 | * The quote strategy. | ||
136 | */ | ||
137 | private readonly QuoteStrategy $quoteStrategy; | ||
138 | |||
139 | /** @psalm-param array<string, QueryComponent> $queryComponents The query components (symbol table). */ | ||
140 | public function __construct( | ||
141 | private readonly Query $query, | ||
142 | private readonly ParserResult $parserResult, | ||
143 | private array $queryComponents, | ||
144 | ) { | ||
145 | $this->rsm = $parserResult->getResultSetMapping(); | ||
146 | $this->em = $query->getEntityManager(); | ||
147 | $this->conn = $this->em->getConnection(); | ||
148 | $this->platform = $this->conn->getDatabasePlatform(); | ||
149 | $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); | ||
150 | } | ||
151 | |||
152 | /** | ||
153 | * Gets the Query instance used by the walker. | ||
154 | */ | ||
155 | public function getQuery(): Query | ||
156 | { | ||
157 | return $this->query; | ||
158 | } | ||
159 | |||
160 | /** | ||
161 | * Gets the Connection used by the walker. | ||
162 | */ | ||
163 | public function getConnection(): Connection | ||
164 | { | ||
165 | return $this->conn; | ||
166 | } | ||
167 | |||
168 | /** | ||
169 | * Gets the EntityManager used by the walker. | ||
170 | */ | ||
171 | public function getEntityManager(): EntityManagerInterface | ||
172 | { | ||
173 | return $this->em; | ||
174 | } | ||
175 | |||
176 | /** | ||
177 | * Gets the information about a single query component. | ||
178 | * | ||
179 | * @param string $dqlAlias The DQL alias. | ||
180 | * | ||
181 | * @return mixed[] | ||
182 | * @psalm-return QueryComponent | ||
183 | */ | ||
184 | public function getQueryComponent(string $dqlAlias): array | ||
185 | { | ||
186 | return $this->queryComponents[$dqlAlias]; | ||
187 | } | ||
188 | |||
189 | public function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
190 | { | ||
191 | return $this->queryComponents[$dqlAlias]['metadata'] | ||
192 | ?? throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
193 | } | ||
194 | |||
195 | /** | ||
196 | * Returns internal queryComponents array. | ||
197 | * | ||
198 | * @return array<string, QueryComponent> | ||
199 | */ | ||
200 | public function getQueryComponents(): array | ||
201 | { | ||
202 | return $this->queryComponents; | ||
203 | } | ||
204 | |||
205 | /** | ||
206 | * Sets or overrides a query component for a given dql alias. | ||
207 | * | ||
208 | * @psalm-param QueryComponent $queryComponent | ||
209 | */ | ||
210 | public function setQueryComponent(string $dqlAlias, array $queryComponent): void | ||
211 | { | ||
212 | $requiredKeys = ['metadata', 'parent', 'relation', 'map', 'nestingLevel', 'token']; | ||
213 | |||
214 | if (array_diff($requiredKeys, array_keys($queryComponent))) { | ||
215 | throw QueryException::invalidQueryComponent($dqlAlias); | ||
216 | } | ||
217 | |||
218 | $this->queryComponents[$dqlAlias] = $queryComponent; | ||
219 | } | ||
220 | |||
221 | /** | ||
222 | * Gets an executor that can be used to execute the result of this walker. | ||
223 | */ | ||
224 | public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor | ||
225 | { | ||
226 | return match (true) { | ||
227 | $statement instanceof AST\SelectStatement | ||
228 | => new Exec\SingleSelectExecutor($statement, $this), | ||
229 | $statement instanceof AST\UpdateStatement | ||
230 | => $this->em->getClassMetadata($statement->updateClause->abstractSchemaName)->isInheritanceTypeJoined() | ||
231 | ? new Exec\MultiTableUpdateExecutor($statement, $this) | ||
232 | : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), | ||
233 | $statement instanceof AST\DeleteStatement | ||
234 | => $this->em->getClassMetadata($statement->deleteClause->abstractSchemaName)->isInheritanceTypeJoined() | ||
235 | ? new Exec\MultiTableDeleteExecutor($statement, $this) | ||
236 | : new Exec\SingleTableDeleteUpdateExecutor($statement, $this), | ||
237 | }; | ||
238 | } | ||
239 | |||
240 | /** | ||
241 | * Generates a unique, short SQL table alias. | ||
242 | */ | ||
243 | public function getSQLTableAlias(string $tableName, string $dqlAlias = ''): string | ||
244 | { | ||
245 | $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; | ||
246 | |||
247 | if (! isset($this->tableAliasMap[$tableName])) { | ||
248 | $this->tableAliasMap[$tableName] = (preg_match('/[a-z]/i', $tableName[0]) ? strtolower($tableName[0]) : 't') | ||
249 | . $this->tableAliasCounter++ . '_'; | ||
250 | } | ||
251 | |||
252 | return $this->tableAliasMap[$tableName]; | ||
253 | } | ||
254 | |||
255 | /** | ||
256 | * Forces the SqlWalker to use a specific alias for a table name, rather than | ||
257 | * generating an alias on its own. | ||
258 | */ | ||
259 | public function setSQLTableAlias(string $tableName, string $alias, string $dqlAlias = ''): string | ||
260 | { | ||
261 | $tableName .= $dqlAlias ? '@[' . $dqlAlias . ']' : ''; | ||
262 | |||
263 | $this->tableAliasMap[$tableName] = $alias; | ||
264 | |||
265 | return $alias; | ||
266 | } | ||
267 | |||
268 | /** | ||
269 | * Gets an SQL column alias for a column name. | ||
270 | */ | ||
271 | public function getSQLColumnAlias(string $columnName): string | ||
272 | { | ||
273 | return $this->quoteStrategy->getColumnAlias($columnName, $this->aliasCounter++, $this->platform); | ||
274 | } | ||
275 | |||
276 | /** | ||
277 | * Generates the SQL JOINs that are necessary for Class Table Inheritance | ||
278 | * for the given class. | ||
279 | */ | ||
280 | private function generateClassTableInheritanceJoins( | ||
281 | ClassMetadata $class, | ||
282 | string $dqlAlias, | ||
283 | ): string { | ||
284 | $sql = ''; | ||
285 | |||
286 | $baseTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
287 | |||
288 | // INNER JOIN parent class tables | ||
289 | foreach ($class->parentClasses as $parentClassName) { | ||
290 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
291 | $tableAlias = $this->getSQLTableAlias($parentClass->getTableName(), $dqlAlias); | ||
292 | |||
293 | // If this is a joined association we must use left joins to preserve the correct result. | ||
294 | $sql .= isset($this->queryComponents[$dqlAlias]['relation']) ? ' LEFT ' : ' INNER '; | ||
295 | $sql .= 'JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
296 | |||
297 | $sqlParts = []; | ||
298 | |||
299 | foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { | ||
300 | $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; | ||
301 | } | ||
302 | |||
303 | // Add filters on the root class | ||
304 | $sqlParts[] = $this->generateFilterConditionSQL($parentClass, $tableAlias); | ||
305 | |||
306 | $sql .= implode(' AND ', array_filter($sqlParts)); | ||
307 | } | ||
308 | |||
309 | // LEFT JOIN child class tables | ||
310 | foreach ($class->subClasses as $subClassName) { | ||
311 | $subClass = $this->em->getClassMetadata($subClassName); | ||
312 | $tableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
313 | |||
314 | $sql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
315 | |||
316 | $sqlParts = []; | ||
317 | |||
318 | foreach ($this->quoteStrategy->getIdentifierColumnNames($subClass, $this->platform) as $columnName) { | ||
319 | $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; | ||
320 | } | ||
321 | |||
322 | $sql .= implode(' AND ', $sqlParts); | ||
323 | } | ||
324 | |||
325 | return $sql; | ||
326 | } | ||
327 | |||
328 | private function generateOrderedCollectionOrderByItems(): string | ||
329 | { | ||
330 | $orderedColumns = []; | ||
331 | |||
332 | foreach ($this->selectedClasses as $selectedClass) { | ||
333 | $dqlAlias = $selectedClass['dqlAlias']; | ||
334 | $qComp = $this->queryComponents[$dqlAlias]; | ||
335 | |||
336 | if (! isset($qComp['relation']->orderBy)) { | ||
337 | continue; | ||
338 | } | ||
339 | |||
340 | assert(isset($qComp['metadata'])); | ||
341 | $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); | ||
342 | |||
343 | foreach ($qComp['relation']->orderBy as $fieldName => $orientation) { | ||
344 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); | ||
345 | $tableName = $qComp['metadata']->isInheritanceTypeJoined() | ||
346 | ? $persister->getOwningTable($fieldName) | ||
347 | : $qComp['metadata']->getTableName(); | ||
348 | |||
349 | $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName; | ||
350 | |||
351 | // OrderByClause should replace an ordered relation. see - DDC-2475 | ||
352 | if (isset($this->orderedColumnsMap[$orderedColumn])) { | ||
353 | continue; | ||
354 | } | ||
355 | |||
356 | $this->orderedColumnsMap[$orderedColumn] = $orientation; | ||
357 | $orderedColumns[] = $orderedColumn . ' ' . $orientation; | ||
358 | } | ||
359 | } | ||
360 | |||
361 | return implode(', ', $orderedColumns); | ||
362 | } | ||
363 | |||
364 | /** | ||
365 | * Generates a discriminator column SQL condition for the class with the given DQL alias. | ||
366 | * | ||
367 | * @psalm-param list<string> $dqlAliases List of root DQL aliases to inspect for discriminator restrictions. | ||
368 | */ | ||
369 | private function generateDiscriminatorColumnConditionSQL(array $dqlAliases): string | ||
370 | { | ||
371 | $sqlParts = []; | ||
372 | |||
373 | foreach ($dqlAliases as $dqlAlias) { | ||
374 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
375 | |||
376 | if (! $class->isInheritanceTypeSingleTable()) { | ||
377 | continue; | ||
378 | } | ||
379 | |||
380 | $sqlTableAlias = $this->useSqlTableAliases | ||
381 | ? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.' | ||
382 | : ''; | ||
383 | |||
384 | $conn = $this->em->getConnection(); | ||
385 | $values = []; | ||
386 | |||
387 | if ($class->discriminatorValue !== null) { // discriminators can be 0 | ||
388 | $values[] = $class->getDiscriminatorColumn()->type === 'integer' && is_int($class->discriminatorValue) | ||
389 | ? $class->discriminatorValue | ||
390 | : $conn->quote((string) $class->discriminatorValue); | ||
391 | } | ||
392 | |||
393 | foreach ($class->subClasses as $subclassName) { | ||
394 | $subclassMetadata = $this->em->getClassMetadata($subclassName); | ||
395 | |||
396 | // Abstract entity classes show up in the list of subClasses, but may be omitted | ||
397 | // from the discriminator map. In that case, they have a null discriminator value. | ||
398 | if ($subclassMetadata->discriminatorValue === null) { | ||
399 | continue; | ||
400 | } | ||
401 | |||
402 | $values[] = $subclassMetadata->getDiscriminatorColumn()->type === 'integer' && is_int($subclassMetadata->discriminatorValue) | ||
403 | ? $subclassMetadata->discriminatorValue | ||
404 | : $conn->quote((string) $subclassMetadata->discriminatorValue); | ||
405 | } | ||
406 | |||
407 | if ($values !== []) { | ||
408 | $sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')'; | ||
409 | } else { | ||
410 | $sqlParts[] = '1=0'; // impossible condition | ||
411 | } | ||
412 | } | ||
413 | |||
414 | $sql = implode(' AND ', $sqlParts); | ||
415 | |||
416 | return count($sqlParts) > 1 ? '(' . $sql . ')' : $sql; | ||
417 | } | ||
418 | |||
419 | /** | ||
420 | * Generates the filter SQL for a given entity and table alias. | ||
421 | */ | ||
422 | private function generateFilterConditionSQL( | ||
423 | ClassMetadata $targetEntity, | ||
424 | string $targetTableAlias, | ||
425 | ): string { | ||
426 | if (! $this->em->hasFilters()) { | ||
427 | return ''; | ||
428 | } | ||
429 | |||
430 | switch ($targetEntity->inheritanceType) { | ||
431 | case ClassMetadata::INHERITANCE_TYPE_NONE: | ||
432 | break; | ||
433 | case ClassMetadata::INHERITANCE_TYPE_JOINED: | ||
434 | // The classes in the inheritance will be added to the query one by one, | ||
435 | // but only the root node is getting filtered | ||
436 | if ($targetEntity->name !== $targetEntity->rootEntityName) { | ||
437 | return ''; | ||
438 | } | ||
439 | |||
440 | break; | ||
441 | case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE: | ||
442 | // With STI the table will only be queried once, make sure that the filters | ||
443 | // are added to the root entity | ||
444 | $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); | ||
445 | break; | ||
446 | default: | ||
447 | //@todo: throw exception? | ||
448 | return ''; | ||
449 | } | ||
450 | |||
451 | $filterClauses = []; | ||
452 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { | ||
453 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); | ||
454 | if ($filterExpr !== '') { | ||
455 | $filterClauses[] = '(' . $filterExpr . ')'; | ||
456 | } | ||
457 | } | ||
458 | |||
459 | return implode(' AND ', $filterClauses); | ||
460 | } | ||
461 | |||
462 | /** | ||
463 | * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. | ||
464 | */ | ||
465 | public function walkSelectStatement(AST\SelectStatement $selectStatement): string | ||
466 | { | ||
467 | $limit = $this->query->getMaxResults(); | ||
468 | $offset = $this->query->getFirstResult(); | ||
469 | $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE; | ||
470 | $sql = $this->walkSelectClause($selectStatement->selectClause) | ||
471 | . $this->walkFromClause($selectStatement->fromClause) | ||
472 | . $this->walkWhereClause($selectStatement->whereClause); | ||
473 | |||
474 | if ($selectStatement->groupByClause) { | ||
475 | $sql .= $this->walkGroupByClause($selectStatement->groupByClause); | ||
476 | } | ||
477 | |||
478 | if ($selectStatement->havingClause) { | ||
479 | $sql .= $this->walkHavingClause($selectStatement->havingClause); | ||
480 | } | ||
481 | |||
482 | if ($selectStatement->orderByClause) { | ||
483 | $sql .= $this->walkOrderByClause($selectStatement->orderByClause); | ||
484 | } | ||
485 | |||
486 | $orderBySql = $this->generateOrderedCollectionOrderByItems(); | ||
487 | if (! $selectStatement->orderByClause && $orderBySql) { | ||
488 | $sql .= ' ORDER BY ' . $orderBySql; | ||
489 | } | ||
490 | |||
491 | $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset); | ||
492 | |||
493 | if ($lockMode === LockMode::NONE) { | ||
494 | return $sql; | ||
495 | } | ||
496 | |||
497 | if ($lockMode === LockMode::PESSIMISTIC_READ) { | ||
498 | return $sql . ' ' . $this->getReadLockSQL($this->platform); | ||
499 | } | ||
500 | |||
501 | if ($lockMode === LockMode::PESSIMISTIC_WRITE) { | ||
502 | return $sql . ' ' . $this->getWriteLockSQL($this->platform); | ||
503 | } | ||
504 | |||
505 | if ($lockMode !== LockMode::OPTIMISTIC) { | ||
506 | throw QueryException::invalidLockMode(); | ||
507 | } | ||
508 | |||
509 | foreach ($this->selectedClasses as $selectedClass) { | ||
510 | if (! $selectedClass['class']->isVersioned) { | ||
511 | throw OptimisticLockException::lockFailed($selectedClass['class']->name); | ||
512 | } | ||
513 | } | ||
514 | |||
515 | return $sql; | ||
516 | } | ||
517 | |||
518 | /** | ||
519 | * Walks down a UpdateStatement AST node, thereby generating the appropriate SQL. | ||
520 | */ | ||
521 | public function walkUpdateStatement(AST\UpdateStatement $updateStatement): string | ||
522 | { | ||
523 | $this->useSqlTableAliases = false; | ||
524 | $this->rsm->isSelect = false; | ||
525 | |||
526 | return $this->walkUpdateClause($updateStatement->updateClause) | ||
527 | . $this->walkWhereClause($updateStatement->whereClause); | ||
528 | } | ||
529 | |||
530 | /** | ||
531 | * Walks down a DeleteStatement AST node, thereby generating the appropriate SQL. | ||
532 | */ | ||
533 | public function walkDeleteStatement(AST\DeleteStatement $deleteStatement): string | ||
534 | { | ||
535 | $this->useSqlTableAliases = false; | ||
536 | $this->rsm->isSelect = false; | ||
537 | |||
538 | return $this->walkDeleteClause($deleteStatement->deleteClause) | ||
539 | . $this->walkWhereClause($deleteStatement->whereClause); | ||
540 | } | ||
541 | |||
542 | /** | ||
543 | * Walks down an IdentificationVariable AST node, thereby generating the appropriate SQL. | ||
544 | * This one differs of ->walkIdentificationVariable() because it generates the entity identifiers. | ||
545 | */ | ||
546 | public function walkEntityIdentificationVariable(string $identVariable): string | ||
547 | { | ||
548 | $class = $this->getMetadataForDqlAlias($identVariable); | ||
549 | $tableAlias = $this->getSQLTableAlias($class->getTableName(), $identVariable); | ||
550 | $sqlParts = []; | ||
551 | |||
552 | foreach ($this->quoteStrategy->getIdentifierColumnNames($class, $this->platform) as $columnName) { | ||
553 | $sqlParts[] = $tableAlias . '.' . $columnName; | ||
554 | } | ||
555 | |||
556 | return implode(', ', $sqlParts); | ||
557 | } | ||
558 | |||
559 | /** | ||
560 | * Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL. | ||
561 | */ | ||
562 | public function walkIdentificationVariable(string $identificationVariable, string|null $fieldName = null): string | ||
563 | { | ||
564 | $class = $this->getMetadataForDqlAlias($identificationVariable); | ||
565 | |||
566 | if ( | ||
567 | $fieldName !== null && $class->isInheritanceTypeJoined() && | ||
568 | isset($class->fieldMappings[$fieldName]->inherited) | ||
569 | ) { | ||
570 | $class = $this->em->getClassMetadata($class->fieldMappings[$fieldName]->inherited); | ||
571 | } | ||
572 | |||
573 | return $this->getSQLTableAlias($class->getTableName(), $identificationVariable); | ||
574 | } | ||
575 | |||
576 | /** | ||
577 | * Walks down a PathExpression AST node, thereby generating the appropriate SQL. | ||
578 | */ | ||
579 | public function walkPathExpression(AST\PathExpression $pathExpr): string | ||
580 | { | ||
581 | $sql = ''; | ||
582 | assert($pathExpr->field !== null); | ||
583 | |||
584 | switch ($pathExpr->type) { | ||
585 | case AST\PathExpression::TYPE_STATE_FIELD: | ||
586 | $fieldName = $pathExpr->field; | ||
587 | $dqlAlias = $pathExpr->identificationVariable; | ||
588 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
589 | |||
590 | if ($this->useSqlTableAliases) { | ||
591 | $sql .= $this->walkIdentificationVariable($dqlAlias, $fieldName) . '.'; | ||
592 | } | ||
593 | |||
594 | $sql .= $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
595 | break; | ||
596 | |||
597 | case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: | ||
598 | // 1- the owning side: | ||
599 | // Just use the foreign key, i.e. u.group_id | ||
600 | $fieldName = $pathExpr->field; | ||
601 | $dqlAlias = $pathExpr->identificationVariable; | ||
602 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
603 | |||
604 | if (isset($class->associationMappings[$fieldName]->inherited)) { | ||
605 | $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); | ||
606 | } | ||
607 | |||
608 | $assoc = $class->associationMappings[$fieldName]; | ||
609 | |||
610 | if (! $assoc->isOwningSide()) { | ||
611 | throw QueryException::associationPathInverseSideNotSupported($pathExpr); | ||
612 | } | ||
613 | |||
614 | assert($assoc->isToOneOwningSide()); | ||
615 | |||
616 | // COMPOSITE KEYS NOT (YET?) SUPPORTED | ||
617 | if (count($assoc->sourceToTargetKeyColumns) > 1) { | ||
618 | throw QueryException::associationPathCompositeKeyNotSupported(); | ||
619 | } | ||
620 | |||
621 | if ($this->useSqlTableAliases) { | ||
622 | $sql .= $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'; | ||
623 | } | ||
624 | |||
625 | $sql .= reset($assoc->targetToSourceKeyColumns); | ||
626 | break; | ||
627 | |||
628 | default: | ||
629 | throw QueryException::invalidPathExpression($pathExpr); | ||
630 | } | ||
631 | |||
632 | return $sql; | ||
633 | } | ||
634 | |||
635 | /** | ||
636 | * Walks down a SelectClause AST node, thereby generating the appropriate SQL. | ||
637 | */ | ||
638 | public function walkSelectClause(AST\SelectClause $selectClause): string | ||
639 | { | ||
640 | $sql = 'SELECT ' . ($selectClause->isDistinct ? 'DISTINCT ' : ''); | ||
641 | $sqlSelectExpressions = array_filter(array_map($this->walkSelectExpression(...), $selectClause->selectExpressions)); | ||
642 | |||
643 | if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && $selectClause->isDistinct) { | ||
644 | $this->query->setHint(self::HINT_DISTINCT, true); | ||
645 | } | ||
646 | |||
647 | $addMetaColumns = $this->query->getHydrationMode() === Query::HYDRATE_OBJECT | ||
648 | || $this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS); | ||
649 | |||
650 | foreach ($this->selectedClasses as $selectedClass) { | ||
651 | $class = $selectedClass['class']; | ||
652 | $dqlAlias = $selectedClass['dqlAlias']; | ||
653 | $resultAlias = $selectedClass['resultAlias']; | ||
654 | |||
655 | // Register as entity or joined entity result | ||
656 | if (! isset($this->queryComponents[$dqlAlias]['relation'])) { | ||
657 | $this->rsm->addEntityResult($class->name, $dqlAlias, $resultAlias); | ||
658 | } else { | ||
659 | assert(isset($this->queryComponents[$dqlAlias]['parent'])); | ||
660 | |||
661 | $this->rsm->addJoinedEntityResult( | ||
662 | $class->name, | ||
663 | $dqlAlias, | ||
664 | $this->queryComponents[$dqlAlias]['parent'], | ||
665 | $this->queryComponents[$dqlAlias]['relation']->fieldName, | ||
666 | ); | ||
667 | } | ||
668 | |||
669 | if ($class->isInheritanceTypeSingleTable() || $class->isInheritanceTypeJoined()) { | ||
670 | // Add discriminator columns to SQL | ||
671 | $rootClass = $this->em->getClassMetadata($class->rootEntityName); | ||
672 | $tblAlias = $this->getSQLTableAlias($rootClass->getTableName(), $dqlAlias); | ||
673 | $discrColumn = $rootClass->getDiscriminatorColumn(); | ||
674 | $columnAlias = $this->getSQLColumnAlias($discrColumn->name); | ||
675 | |||
676 | $sqlSelectExpressions[] = $tblAlias . '.' . $discrColumn->name . ' AS ' . $columnAlias; | ||
677 | |||
678 | $this->rsm->setDiscriminatorColumn($dqlAlias, $columnAlias); | ||
679 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $discrColumn->fieldName, false, $discrColumn->type); | ||
680 | if (! empty($discrColumn->enumType)) { | ||
681 | $this->rsm->addEnumResult($columnAlias, $discrColumn->enumType); | ||
682 | } | ||
683 | } | ||
684 | |||
685 | // Add foreign key columns to SQL, if necessary | ||
686 | if (! $addMetaColumns && ! $class->containsForeignIdentifier) { | ||
687 | continue; | ||
688 | } | ||
689 | |||
690 | // Add foreign key columns of class and also parent classes | ||
691 | foreach ($class->associationMappings as $assoc) { | ||
692 | if ( | ||
693 | ! $assoc->isToOneOwningSide() | ||
694 | || ( ! $addMetaColumns && ! isset($assoc->id)) | ||
695 | ) { | ||
696 | continue; | ||
697 | } | ||
698 | |||
699 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
700 | $isIdentifier = (isset($assoc->id) && $assoc->id === true); | ||
701 | $owningClass = isset($assoc->inherited) ? $this->em->getClassMetadata($assoc->inherited) : $class; | ||
702 | $sqlTableAlias = $this->getSQLTableAlias($owningClass->getTableName(), $dqlAlias); | ||
703 | |||
704 | foreach ($assoc->joinColumns as $joinColumn) { | ||
705 | $columnName = $joinColumn->name; | ||
706 | $columnAlias = $this->getSQLColumnAlias($columnName); | ||
707 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
708 | |||
709 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
710 | $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
711 | |||
712 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $isIdentifier, $columnType); | ||
713 | } | ||
714 | } | ||
715 | |||
716 | // Add foreign key columns to SQL, if necessary | ||
717 | if (! $addMetaColumns) { | ||
718 | continue; | ||
719 | } | ||
720 | |||
721 | // Add foreign key columns of subclasses | ||
722 | foreach ($class->subClasses as $subClassName) { | ||
723 | $subClass = $this->em->getClassMetadata($subClassName); | ||
724 | $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
725 | |||
726 | foreach ($subClass->associationMappings as $assoc) { | ||
727 | // Skip if association is inherited | ||
728 | if (isset($assoc->inherited)) { | ||
729 | continue; | ||
730 | } | ||
731 | |||
732 | if ($assoc->isToOneOwningSide()) { | ||
733 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
734 | |||
735 | foreach ($assoc->joinColumns as $joinColumn) { | ||
736 | $columnName = $joinColumn->name; | ||
737 | $columnAlias = $this->getSQLColumnAlias($columnName); | ||
738 | $columnType = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
739 | |||
740 | $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform); | ||
741 | $sqlSelectExpressions[] = $sqlTableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
742 | |||
743 | $this->rsm->addMetaResult($dqlAlias, $columnAlias, $columnName, $subClass->isIdentifier($columnName), $columnType); | ||
744 | } | ||
745 | } | ||
746 | } | ||
747 | } | ||
748 | } | ||
749 | |||
750 | return $sql . implode(', ', $sqlSelectExpressions); | ||
751 | } | ||
752 | |||
753 | /** | ||
754 | * Walks down a FromClause AST node, thereby generating the appropriate SQL. | ||
755 | */ | ||
756 | public function walkFromClause(AST\FromClause $fromClause): string | ||
757 | { | ||
758 | $identificationVarDecls = $fromClause->identificationVariableDeclarations; | ||
759 | $sqlParts = []; | ||
760 | |||
761 | foreach ($identificationVarDecls as $identificationVariableDecl) { | ||
762 | $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl); | ||
763 | } | ||
764 | |||
765 | return ' FROM ' . implode(', ', $sqlParts); | ||
766 | } | ||
767 | |||
768 | /** | ||
769 | * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL. | ||
770 | */ | ||
771 | public function walkIdentificationVariableDeclaration(AST\IdentificationVariableDeclaration $identificationVariableDecl): string | ||
772 | { | ||
773 | $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); | ||
774 | |||
775 | if ($identificationVariableDecl->indexBy) { | ||
776 | $this->walkIndexBy($identificationVariableDecl->indexBy); | ||
777 | } | ||
778 | |||
779 | foreach ($identificationVariableDecl->joins as $join) { | ||
780 | $sql .= $this->walkJoin($join); | ||
781 | } | ||
782 | |||
783 | return $sql; | ||
784 | } | ||
785 | |||
786 | /** | ||
787 | * Walks down a IndexBy AST node. | ||
788 | */ | ||
789 | public function walkIndexBy(AST\IndexBy $indexBy): void | ||
790 | { | ||
791 | $pathExpression = $indexBy->singleValuedPathExpression; | ||
792 | $alias = $pathExpression->identificationVariable; | ||
793 | assert($pathExpression->field !== null); | ||
794 | |||
795 | switch ($pathExpression->type) { | ||
796 | case AST\PathExpression::TYPE_STATE_FIELD: | ||
797 | $field = $pathExpression->field; | ||
798 | break; | ||
799 | |||
800 | case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: | ||
801 | // Just use the foreign key, i.e. u.group_id | ||
802 | $fieldName = $pathExpression->field; | ||
803 | $class = $this->getMetadataForDqlAlias($alias); | ||
804 | |||
805 | if (isset($class->associationMappings[$fieldName]->inherited)) { | ||
806 | $class = $this->em->getClassMetadata($class->associationMappings[$fieldName]->inherited); | ||
807 | } | ||
808 | |||
809 | $association = $class->associationMappings[$fieldName]; | ||
810 | |||
811 | if (! $association->isOwningSide()) { | ||
812 | throw QueryException::associationPathInverseSideNotSupported($pathExpression); | ||
813 | } | ||
814 | |||
815 | assert($association->isToOneOwningSide()); | ||
816 | |||
817 | if (count($association->sourceToTargetKeyColumns) > 1) { | ||
818 | throw QueryException::associationPathCompositeKeyNotSupported(); | ||
819 | } | ||
820 | |||
821 | $field = reset($association->targetToSourceKeyColumns); | ||
822 | break; | ||
823 | |||
824 | default: | ||
825 | throw QueryException::invalidPathExpression($pathExpression); | ||
826 | } | ||
827 | |||
828 | if (isset($this->scalarFields[$alias][$field])) { | ||
829 | $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]); | ||
830 | |||
831 | return; | ||
832 | } | ||
833 | |||
834 | $this->rsm->addIndexBy($alias, $field); | ||
835 | } | ||
836 | |||
837 | /** | ||
838 | * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL. | ||
839 | */ | ||
840 | public function walkRangeVariableDeclaration(AST\RangeVariableDeclaration $rangeVariableDeclaration): string | ||
841 | { | ||
842 | return $this->generateRangeVariableDeclarationSQL($rangeVariableDeclaration, false); | ||
843 | } | ||
844 | |||
845 | /** | ||
846 | * Generate appropriate SQL for RangeVariableDeclaration AST node | ||
847 | */ | ||
848 | private function generateRangeVariableDeclarationSQL( | ||
849 | AST\RangeVariableDeclaration $rangeVariableDeclaration, | ||
850 | bool $buildNestedJoins, | ||
851 | ): string { | ||
852 | $class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName); | ||
853 | $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable; | ||
854 | |||
855 | if ($rangeVariableDeclaration->isRoot) { | ||
856 | $this->rootAliases[] = $dqlAlias; | ||
857 | } | ||
858 | |||
859 | $sql = $this->platform->appendLockHint( | ||
860 | $this->quoteStrategy->getTableName($class, $this->platform) . ' ' . | ||
861 | $this->getSQLTableAlias($class->getTableName(), $dqlAlias), | ||
862 | $this->query->getHint(Query::HINT_LOCK_MODE) ?: LockMode::NONE, | ||
863 | ); | ||
864 | |||
865 | if (! $class->isInheritanceTypeJoined()) { | ||
866 | return $sql; | ||
867 | } | ||
868 | |||
869 | $classTableInheritanceJoins = $this->generateClassTableInheritanceJoins($class, $dqlAlias); | ||
870 | |||
871 | if (! $buildNestedJoins) { | ||
872 | return $sql . $classTableInheritanceJoins; | ||
873 | } | ||
874 | |||
875 | return $classTableInheritanceJoins === '' ? $sql : '(' . $sql . $classTableInheritanceJoins . ')'; | ||
876 | } | ||
877 | |||
878 | /** | ||
879 | * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. | ||
880 | * | ||
881 | * @psalm-param AST\Join::JOIN_TYPE_* $joinType | ||
882 | * | ||
883 | * @throws QueryException | ||
884 | */ | ||
885 | public function walkJoinAssociationDeclaration( | ||
886 | AST\JoinAssociationDeclaration $joinAssociationDeclaration, | ||
887 | int $joinType = AST\Join::JOIN_TYPE_INNER, | ||
888 | AST\ConditionalExpression|AST\Phase2OptimizableConditional|null $condExpr = null, | ||
889 | ): string { | ||
890 | $sql = ''; | ||
891 | |||
892 | $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; | ||
893 | $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; | ||
894 | $indexBy = $joinAssociationDeclaration->indexBy; | ||
895 | |||
896 | $relation = $this->queryComponents[$joinedDqlAlias]['relation'] ?? null; | ||
897 | assert($relation !== null); | ||
898 | $targetClass = $this->em->getClassMetadata($relation->targetEntity); | ||
899 | $sourceClass = $this->em->getClassMetadata($relation->sourceEntity); | ||
900 | $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
901 | |||
902 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); | ||
903 | $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); | ||
904 | |||
905 | // Ensure we got the owning side, since it has all mapping info | ||
906 | $assoc = $this->em->getMetadataFactory()->getOwningSide($relation); | ||
907 | |||
908 | if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) === true && (! $this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) { | ||
909 | if ($relation->isToMany()) { | ||
910 | throw QueryException::iterateWithFetchJoinNotAllowed($assoc); | ||
911 | } | ||
912 | } | ||
913 | |||
914 | $fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch; | ||
915 | |||
916 | if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) { | ||
917 | throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName); | ||
918 | } | ||
919 | |||
920 | // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot | ||
921 | // be the owning side and previously we ensured that $assoc is always the owning side of the associations. | ||
922 | // The owning side is necessary at this point because only it contains the JoinColumn information. | ||
923 | switch (true) { | ||
924 | case $assoc->isToOne(): | ||
925 | assert($assoc->isToOneOwningSide()); | ||
926 | $conditions = []; | ||
927 | |||
928 | foreach ($assoc->joinColumns as $joinColumn) { | ||
929 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
930 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
931 | |||
932 | if ($relation->isOwningSide()) { | ||
933 | $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; | ||
934 | |||
935 | continue; | ||
936 | } | ||
937 | |||
938 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn; | ||
939 | } | ||
940 | |||
941 | // Apply remaining inheritance restrictions | ||
942 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); | ||
943 | |||
944 | if ($discrSql) { | ||
945 | $conditions[] = $discrSql; | ||
946 | } | ||
947 | |||
948 | // Apply the filters | ||
949 | $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); | ||
950 | |||
951 | if ($filterExpr) { | ||
952 | $conditions[] = $filterExpr; | ||
953 | } | ||
954 | |||
955 | $targetTableJoin = [ | ||
956 | 'table' => $targetTableName . ' ' . $targetTableAlias, | ||
957 | 'condition' => implode(' AND ', $conditions), | ||
958 | ]; | ||
959 | break; | ||
960 | |||
961 | case $assoc->isManyToMany(): | ||
962 | // Join relation table | ||
963 | $joinTable = $assoc->joinTable; | ||
964 | $joinTableAlias = $this->getSQLTableAlias($joinTable->name, $joinedDqlAlias); | ||
965 | $joinTableName = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform); | ||
966 | |||
967 | $conditions = []; | ||
968 | $relationColumns = $relation->isOwningSide() | ||
969 | ? $assoc->joinTable->joinColumns | ||
970 | : $assoc->joinTable->inverseJoinColumns; | ||
971 | |||
972 | foreach ($relationColumns as $joinColumn) { | ||
973 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
974 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
975 | |||
976 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; | ||
977 | } | ||
978 | |||
979 | $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); | ||
980 | |||
981 | // Join target table | ||
982 | $sql .= $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; | ||
983 | |||
984 | $conditions = []; | ||
985 | $relationColumns = $relation->isOwningSide() | ||
986 | ? $assoc->joinTable->inverseJoinColumns | ||
987 | : $assoc->joinTable->joinColumns; | ||
988 | |||
989 | foreach ($relationColumns as $joinColumn) { | ||
990 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
991 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
992 | |||
993 | $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; | ||
994 | } | ||
995 | |||
996 | // Apply remaining inheritance restrictions | ||
997 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$joinedDqlAlias]); | ||
998 | |||
999 | if ($discrSql) { | ||
1000 | $conditions[] = $discrSql; | ||
1001 | } | ||
1002 | |||
1003 | // Apply the filters | ||
1004 | $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); | ||
1005 | |||
1006 | if ($filterExpr) { | ||
1007 | $conditions[] = $filterExpr; | ||
1008 | } | ||
1009 | |||
1010 | $targetTableJoin = [ | ||
1011 | 'table' => $targetTableName . ' ' . $targetTableAlias, | ||
1012 | 'condition' => implode(' AND ', $conditions), | ||
1013 | ]; | ||
1014 | break; | ||
1015 | |||
1016 | default: | ||
1017 | throw new BadMethodCallException('Type of association must be one of *_TO_ONE or MANY_TO_MANY'); | ||
1018 | } | ||
1019 | |||
1020 | // Handle WITH clause | ||
1021 | $withCondition = $condExpr === null ? '' : ('(' . $this->walkConditionalExpression($condExpr) . ')'); | ||
1022 | |||
1023 | if ($targetClass->isInheritanceTypeJoined()) { | ||
1024 | $ctiJoins = $this->generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); | ||
1025 | // If we have WITH condition, we need to build nested joins for target class table and cti joins | ||
1026 | if ($withCondition && $ctiJoins) { | ||
1027 | $sql .= '(' . $targetTableJoin['table'] . $ctiJoins . ') ON ' . $targetTableJoin['condition']; | ||
1028 | } else { | ||
1029 | $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition'] . $ctiJoins; | ||
1030 | } | ||
1031 | } else { | ||
1032 | $sql .= $targetTableJoin['table'] . ' ON ' . $targetTableJoin['condition']; | ||
1033 | } | ||
1034 | |||
1035 | if ($withCondition) { | ||
1036 | $sql .= ' AND ' . $withCondition; | ||
1037 | } | ||
1038 | |||
1039 | // Apply the indexes | ||
1040 | if ($indexBy) { | ||
1041 | // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. | ||
1042 | $this->walkIndexBy($indexBy); | ||
1043 | } elseif ($relation->isIndexed()) { | ||
1044 | $this->rsm->addIndexBy($joinedDqlAlias, $relation->indexBy()); | ||
1045 | } | ||
1046 | |||
1047 | return $sql; | ||
1048 | } | ||
1049 | |||
1050 | /** | ||
1051 | * Walks down a FunctionNode AST node, thereby generating the appropriate SQL. | ||
1052 | */ | ||
1053 | public function walkFunction(AST\Functions\FunctionNode $function): string | ||
1054 | { | ||
1055 | return $function->getSql($this); | ||
1056 | } | ||
1057 | |||
1058 | /** | ||
1059 | * Walks down an OrderByClause AST node, thereby generating the appropriate SQL. | ||
1060 | */ | ||
1061 | public function walkOrderByClause(AST\OrderByClause $orderByClause): string | ||
1062 | { | ||
1063 | $orderByItems = array_map($this->walkOrderByItem(...), $orderByClause->orderByItems); | ||
1064 | |||
1065 | $collectionOrderByItems = $this->generateOrderedCollectionOrderByItems(); | ||
1066 | if ($collectionOrderByItems !== '') { | ||
1067 | $orderByItems = array_merge($orderByItems, (array) $collectionOrderByItems); | ||
1068 | } | ||
1069 | |||
1070 | return ' ORDER BY ' . implode(', ', $orderByItems); | ||
1071 | } | ||
1072 | |||
1073 | /** | ||
1074 | * Walks down an OrderByItem AST node, thereby generating the appropriate SQL. | ||
1075 | */ | ||
1076 | public function walkOrderByItem(AST\OrderByItem $orderByItem): string | ||
1077 | { | ||
1078 | $type = strtoupper($orderByItem->type); | ||
1079 | $expr = $orderByItem->expression; | ||
1080 | $sql = $expr instanceof AST\Node | ||
1081 | ? $expr->dispatch($this) | ||
1082 | : $this->walkResultVariable($this->queryComponents[$expr]['token']->value); | ||
1083 | |||
1084 | $this->orderedColumnsMap[$sql] = $type; | ||
1085 | |||
1086 | if ($expr instanceof AST\Subselect) { | ||
1087 | return '(' . $sql . ') ' . $type; | ||
1088 | } | ||
1089 | |||
1090 | return $sql . ' ' . $type; | ||
1091 | } | ||
1092 | |||
1093 | /** | ||
1094 | * Walks down a HavingClause AST node, thereby generating the appropriate SQL. | ||
1095 | */ | ||
1096 | public function walkHavingClause(AST\HavingClause $havingClause): string | ||
1097 | { | ||
1098 | return ' HAVING ' . $this->walkConditionalExpression($havingClause->conditionalExpression); | ||
1099 | } | ||
1100 | |||
1101 | /** | ||
1102 | * Walks down a Join AST node and creates the corresponding SQL. | ||
1103 | */ | ||
1104 | public function walkJoin(AST\Join $join): string | ||
1105 | { | ||
1106 | $joinType = $join->joinType; | ||
1107 | $joinDeclaration = $join->joinAssociationDeclaration; | ||
1108 | |||
1109 | $sql = $joinType === AST\Join::JOIN_TYPE_LEFT || $joinType === AST\Join::JOIN_TYPE_LEFTOUTER | ||
1110 | ? ' LEFT JOIN ' | ||
1111 | : ' INNER JOIN '; | ||
1112 | |||
1113 | switch (true) { | ||
1114 | case $joinDeclaration instanceof AST\RangeVariableDeclaration: | ||
1115 | $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); | ||
1116 | $dqlAlias = $joinDeclaration->aliasIdentificationVariable; | ||
1117 | $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); | ||
1118 | $conditions = []; | ||
1119 | |||
1120 | if ($join->conditionalExpression) { | ||
1121 | $conditions[] = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; | ||
1122 | } | ||
1123 | |||
1124 | $isUnconditionalJoin = $conditions === []; | ||
1125 | $condExprConjunction = $class->isInheritanceTypeJoined() && $joinType !== AST\Join::JOIN_TYPE_LEFT && $joinType !== AST\Join::JOIN_TYPE_LEFTOUTER && $isUnconditionalJoin | ||
1126 | ? ' AND ' | ||
1127 | : ' ON '; | ||
1128 | |||
1129 | $sql .= $this->generateRangeVariableDeclarationSQL($joinDeclaration, ! $isUnconditionalJoin); | ||
1130 | |||
1131 | // Apply remaining inheritance restrictions | ||
1132 | $discrSql = $this->generateDiscriminatorColumnConditionSQL([$dqlAlias]); | ||
1133 | |||
1134 | if ($discrSql) { | ||
1135 | $conditions[] = $discrSql; | ||
1136 | } | ||
1137 | |||
1138 | // Apply the filters | ||
1139 | $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); | ||
1140 | |||
1141 | if ($filterExpr) { | ||
1142 | $conditions[] = $filterExpr; | ||
1143 | } | ||
1144 | |||
1145 | if ($conditions) { | ||
1146 | $sql .= $condExprConjunction . implode(' AND ', $conditions); | ||
1147 | } | ||
1148 | |||
1149 | break; | ||
1150 | |||
1151 | case $joinDeclaration instanceof AST\JoinAssociationDeclaration: | ||
1152 | $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression); | ||
1153 | break; | ||
1154 | } | ||
1155 | |||
1156 | return $sql; | ||
1157 | } | ||
1158 | |||
1159 | /** | ||
1160 | * Walks down a CoalesceExpression AST node and generates the corresponding SQL. | ||
1161 | */ | ||
1162 | public function walkCoalesceExpression(AST\CoalesceExpression $coalesceExpression): string | ||
1163 | { | ||
1164 | $sql = 'COALESCE('; | ||
1165 | |||
1166 | $scalarExpressions = []; | ||
1167 | |||
1168 | foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { | ||
1169 | $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); | ||
1170 | } | ||
1171 | |||
1172 | return $sql . implode(', ', $scalarExpressions) . ')'; | ||
1173 | } | ||
1174 | |||
1175 | /** | ||
1176 | * Walks down a NullIfExpression AST node and generates the corresponding SQL. | ||
1177 | */ | ||
1178 | public function walkNullIfExpression(AST\NullIfExpression $nullIfExpression): string | ||
1179 | { | ||
1180 | $firstExpression = is_string($nullIfExpression->firstExpression) | ||
1181 | ? $this->conn->quote($nullIfExpression->firstExpression) | ||
1182 | : $this->walkSimpleArithmeticExpression($nullIfExpression->firstExpression); | ||
1183 | |||
1184 | $secondExpression = is_string($nullIfExpression->secondExpression) | ||
1185 | ? $this->conn->quote($nullIfExpression->secondExpression) | ||
1186 | : $this->walkSimpleArithmeticExpression($nullIfExpression->secondExpression); | ||
1187 | |||
1188 | return 'NULLIF(' . $firstExpression . ', ' . $secondExpression . ')'; | ||
1189 | } | ||
1190 | |||
1191 | /** | ||
1192 | * Walks down a GeneralCaseExpression AST node and generates the corresponding SQL. | ||
1193 | */ | ||
1194 | public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCaseExpression): string | ||
1195 | { | ||
1196 | $sql = 'CASE'; | ||
1197 | |||
1198 | foreach ($generalCaseExpression->whenClauses as $whenClause) { | ||
1199 | $sql .= ' WHEN ' . $this->walkConditionalExpression($whenClause->caseConditionExpression); | ||
1200 | $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($whenClause->thenScalarExpression); | ||
1201 | } | ||
1202 | |||
1203 | $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($generalCaseExpression->elseScalarExpression) . ' END'; | ||
1204 | |||
1205 | return $sql; | ||
1206 | } | ||
1207 | |||
1208 | /** | ||
1209 | * Walks down a SimpleCaseExpression AST node and generates the corresponding SQL. | ||
1210 | */ | ||
1211 | public function walkSimpleCaseExpression(AST\SimpleCaseExpression $simpleCaseExpression): string | ||
1212 | { | ||
1213 | $sql = 'CASE ' . $this->walkStateFieldPathExpression($simpleCaseExpression->caseOperand); | ||
1214 | |||
1215 | foreach ($simpleCaseExpression->simpleWhenClauses as $simpleWhenClause) { | ||
1216 | $sql .= ' WHEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->caseScalarExpression); | ||
1217 | $sql .= ' THEN ' . $this->walkSimpleArithmeticExpression($simpleWhenClause->thenScalarExpression); | ||
1218 | } | ||
1219 | |||
1220 | $sql .= ' ELSE ' . $this->walkSimpleArithmeticExpression($simpleCaseExpression->elseScalarExpression) . ' END'; | ||
1221 | |||
1222 | return $sql; | ||
1223 | } | ||
1224 | |||
1225 | /** | ||
1226 | * Walks down a SelectExpression AST node and generates the corresponding SQL. | ||
1227 | */ | ||
1228 | public function walkSelectExpression(AST\SelectExpression $selectExpression): string | ||
1229 | { | ||
1230 | $sql = ''; | ||
1231 | $expr = $selectExpression->expression; | ||
1232 | $hidden = $selectExpression->hiddenAliasResultVariable; | ||
1233 | |||
1234 | switch (true) { | ||
1235 | case $expr instanceof AST\PathExpression: | ||
1236 | if ($expr->type !== AST\PathExpression::TYPE_STATE_FIELD) { | ||
1237 | throw QueryException::invalidPathExpression($expr); | ||
1238 | } | ||
1239 | |||
1240 | assert($expr->field !== null); | ||
1241 | $fieldName = $expr->field; | ||
1242 | $dqlAlias = $expr->identificationVariable; | ||
1243 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1244 | |||
1245 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $fieldName; | ||
1246 | $tableName = $class->isInheritanceTypeJoined() | ||
1247 | ? $this->em->getUnitOfWork()->getEntityPersister($class->name)->getOwningTable($fieldName) | ||
1248 | : $class->getTableName(); | ||
1249 | |||
1250 | $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); | ||
1251 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
1252 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
1253 | $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); | ||
1254 | $col = $sqlTableAlias . '.' . $columnName; | ||
1255 | |||
1256 | $type = Type::getType($fieldMapping->type); | ||
1257 | $col = $type->convertToPHPValueSQL($col, $this->conn->getDatabasePlatform()); | ||
1258 | |||
1259 | $sql .= $col . ' AS ' . $columnAlias; | ||
1260 | |||
1261 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1262 | |||
1263 | if (! $hidden) { | ||
1264 | $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldMapping->type); | ||
1265 | $this->scalarFields[$dqlAlias][$fieldName] = $columnAlias; | ||
1266 | |||
1267 | if (! empty($fieldMapping->enumType)) { | ||
1268 | $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
1269 | } | ||
1270 | } | ||
1271 | |||
1272 | break; | ||
1273 | |||
1274 | case $expr instanceof AST\AggregateExpression: | ||
1275 | case $expr instanceof AST\Functions\FunctionNode: | ||
1276 | case $expr instanceof AST\SimpleArithmeticExpression: | ||
1277 | case $expr instanceof AST\ArithmeticTerm: | ||
1278 | case $expr instanceof AST\ArithmeticFactor: | ||
1279 | case $expr instanceof AST\ParenthesisExpression: | ||
1280 | case $expr instanceof AST\Literal: | ||
1281 | case $expr instanceof AST\NullIfExpression: | ||
1282 | case $expr instanceof AST\CoalesceExpression: | ||
1283 | case $expr instanceof AST\GeneralCaseExpression: | ||
1284 | case $expr instanceof AST\SimpleCaseExpression: | ||
1285 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1286 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1287 | |||
1288 | $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; | ||
1289 | |||
1290 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1291 | |||
1292 | if ($hidden) { | ||
1293 | break; | ||
1294 | } | ||
1295 | |||
1296 | if (! $expr instanceof Query\AST\TypedExpression) { | ||
1297 | // Conceptually we could resolve field type here by traverse through AST to retrieve field type, | ||
1298 | // but this is not a feasible solution; assume 'string'. | ||
1299 | $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); | ||
1300 | |||
1301 | break; | ||
1302 | } | ||
1303 | |||
1304 | $this->rsm->addScalarResult($columnAlias, $resultAlias, Type::getTypeRegistry()->lookupName($expr->getReturnType())); | ||
1305 | |||
1306 | break; | ||
1307 | |||
1308 | case $expr instanceof AST\Subselect: | ||
1309 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1310 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1311 | |||
1312 | $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; | ||
1313 | |||
1314 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1315 | |||
1316 | if (! $hidden) { | ||
1317 | // We cannot resolve field type here; assume 'string'. | ||
1318 | $this->rsm->addScalarResult($columnAlias, $resultAlias, 'string'); | ||
1319 | } | ||
1320 | |||
1321 | break; | ||
1322 | |||
1323 | case $expr instanceof AST\NewObjectExpression: | ||
1324 | $sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable); | ||
1325 | break; | ||
1326 | |||
1327 | default: | ||
1328 | $dqlAlias = $expr; | ||
1329 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1330 | $resultAlias = $selectExpression->fieldIdentificationVariable ?: null; | ||
1331 | |||
1332 | if (! isset($this->selectedClasses[$dqlAlias])) { | ||
1333 | $this->selectedClasses[$dqlAlias] = [ | ||
1334 | 'class' => $class, | ||
1335 | 'dqlAlias' => $dqlAlias, | ||
1336 | 'resultAlias' => $resultAlias, | ||
1337 | ]; | ||
1338 | } | ||
1339 | |||
1340 | $sqlParts = []; | ||
1341 | |||
1342 | // Select all fields from the queried class | ||
1343 | foreach ($class->fieldMappings as $fieldName => $mapping) { | ||
1344 | $tableName = isset($mapping->inherited) | ||
1345 | ? $this->em->getClassMetadata($mapping->inherited)->getTableName() | ||
1346 | : $class->getTableName(); | ||
1347 | |||
1348 | $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); | ||
1349 | $columnAlias = $this->getSQLColumnAlias($mapping->columnName); | ||
1350 | $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); | ||
1351 | |||
1352 | $col = $sqlTableAlias . '.' . $quotedColumnName; | ||
1353 | |||
1354 | $type = Type::getType($mapping->type); | ||
1355 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
1356 | |||
1357 | $sqlParts[] = $col . ' AS ' . $columnAlias; | ||
1358 | |||
1359 | $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; | ||
1360 | |||
1361 | $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); | ||
1362 | |||
1363 | if (! empty($mapping->enumType)) { | ||
1364 | $this->rsm->addEnumResult($columnAlias, $mapping->enumType); | ||
1365 | } | ||
1366 | } | ||
1367 | |||
1368 | // Add any additional fields of subclasses (excluding inherited fields) | ||
1369 | // 1) on Single Table Inheritance: always, since its marginal overhead | ||
1370 | // 2) on Class Table Inheritance | ||
1371 | foreach ($class->subClasses as $subClassName) { | ||
1372 | $subClass = $this->em->getClassMetadata($subClassName); | ||
1373 | $sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias); | ||
1374 | |||
1375 | foreach ($subClass->fieldMappings as $fieldName => $mapping) { | ||
1376 | if (isset($mapping->inherited)) { | ||
1377 | continue; | ||
1378 | } | ||
1379 | |||
1380 | $columnAlias = $this->getSQLColumnAlias($mapping->columnName); | ||
1381 | $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform); | ||
1382 | |||
1383 | $col = $sqlTableAlias . '.' . $quotedColumnName; | ||
1384 | |||
1385 | $type = Type::getType($mapping->type); | ||
1386 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
1387 | |||
1388 | $sqlParts[] = $col . ' AS ' . $columnAlias; | ||
1389 | |||
1390 | $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; | ||
1391 | |||
1392 | $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); | ||
1393 | } | ||
1394 | } | ||
1395 | |||
1396 | $sql .= implode(', ', $sqlParts); | ||
1397 | } | ||
1398 | |||
1399 | return $sql; | ||
1400 | } | ||
1401 | |||
1402 | public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string | ||
1403 | { | ||
1404 | return ' ' . strtoupper($qExpr->type) . '(' . $this->walkSubselect($qExpr->subselect) . ')'; | ||
1405 | } | ||
1406 | |||
1407 | /** | ||
1408 | * Walks down a Subselect AST node, thereby generating the appropriate SQL. | ||
1409 | */ | ||
1410 | public function walkSubselect(AST\Subselect $subselect): string | ||
1411 | { | ||
1412 | $useAliasesBefore = $this->useSqlTableAliases; | ||
1413 | $rootAliasesBefore = $this->rootAliases; | ||
1414 | |||
1415 | $this->rootAliases = []; // reset the rootAliases for the subselect | ||
1416 | $this->useSqlTableAliases = true; | ||
1417 | |||
1418 | $sql = $this->walkSimpleSelectClause($subselect->simpleSelectClause); | ||
1419 | $sql .= $this->walkSubselectFromClause($subselect->subselectFromClause); | ||
1420 | $sql .= $this->walkWhereClause($subselect->whereClause); | ||
1421 | |||
1422 | $sql .= $subselect->groupByClause ? $this->walkGroupByClause($subselect->groupByClause) : ''; | ||
1423 | $sql .= $subselect->havingClause ? $this->walkHavingClause($subselect->havingClause) : ''; | ||
1424 | $sql .= $subselect->orderByClause ? $this->walkOrderByClause($subselect->orderByClause) : ''; | ||
1425 | |||
1426 | $this->rootAliases = $rootAliasesBefore; // put the main aliases back | ||
1427 | $this->useSqlTableAliases = $useAliasesBefore; | ||
1428 | |||
1429 | return $sql; | ||
1430 | } | ||
1431 | |||
1432 | /** | ||
1433 | * Walks down a SubselectFromClause AST node, thereby generating the appropriate SQL. | ||
1434 | */ | ||
1435 | public function walkSubselectFromClause(AST\SubselectFromClause $subselectFromClause): string | ||
1436 | { | ||
1437 | $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations; | ||
1438 | $sqlParts = []; | ||
1439 | |||
1440 | foreach ($identificationVarDecls as $subselectIdVarDecl) { | ||
1441 | $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl); | ||
1442 | } | ||
1443 | |||
1444 | return ' FROM ' . implode(', ', $sqlParts); | ||
1445 | } | ||
1446 | |||
1447 | /** | ||
1448 | * Walks down a SimpleSelectClause AST node, thereby generating the appropriate SQL. | ||
1449 | */ | ||
1450 | public function walkSimpleSelectClause(AST\SimpleSelectClause $simpleSelectClause): string | ||
1451 | { | ||
1452 | return 'SELECT' . ($simpleSelectClause->isDistinct ? ' DISTINCT' : '') | ||
1453 | . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression); | ||
1454 | } | ||
1455 | |||
1456 | public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression): string | ||
1457 | { | ||
1458 | return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this)); | ||
1459 | } | ||
1460 | |||
1461 | public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string | ||
1462 | { | ||
1463 | $sqlSelectExpressions = []; | ||
1464 | $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++; | ||
1465 | |||
1466 | foreach ($newObjectExpression->args as $argIndex => $e) { | ||
1467 | $resultAlias = $this->scalarResultCounter++; | ||
1468 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1469 | $fieldType = 'string'; | ||
1470 | |||
1471 | switch (true) { | ||
1472 | case $e instanceof AST\NewObjectExpression: | ||
1473 | $sqlSelectExpressions[] = $e->dispatch($this); | ||
1474 | break; | ||
1475 | |||
1476 | case $e instanceof AST\Subselect: | ||
1477 | $sqlSelectExpressions[] = '(' . $e->dispatch($this) . ') AS ' . $columnAlias; | ||
1478 | break; | ||
1479 | |||
1480 | case $e instanceof AST\PathExpression: | ||
1481 | assert($e->field !== null); | ||
1482 | $dqlAlias = $e->identificationVariable; | ||
1483 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1484 | $fieldName = $e->field; | ||
1485 | $fieldMapping = $class->fieldMappings[$fieldName]; | ||
1486 | $fieldType = $fieldMapping->type; | ||
1487 | $col = trim($e->dispatch($this)); | ||
1488 | |||
1489 | $type = Type::getType($fieldType); | ||
1490 | $col = $type->convertToPHPValueSQL($col, $this->platform); | ||
1491 | |||
1492 | $sqlSelectExpressions[] = $col . ' AS ' . $columnAlias; | ||
1493 | |||
1494 | if (! empty($fieldMapping->enumType)) { | ||
1495 | $this->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
1496 | } | ||
1497 | |||
1498 | break; | ||
1499 | |||
1500 | case $e instanceof AST\Literal: | ||
1501 | switch ($e->type) { | ||
1502 | case AST\Literal::BOOLEAN: | ||
1503 | $fieldType = 'boolean'; | ||
1504 | break; | ||
1505 | |||
1506 | case AST\Literal::NUMERIC: | ||
1507 | $fieldType = is_float($e->value) ? 'float' : 'integer'; | ||
1508 | break; | ||
1509 | } | ||
1510 | |||
1511 | $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; | ||
1512 | break; | ||
1513 | |||
1514 | default: | ||
1515 | $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; | ||
1516 | break; | ||
1517 | } | ||
1518 | |||
1519 | $this->scalarResultAliasMap[$resultAlias] = $columnAlias; | ||
1520 | $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType); | ||
1521 | |||
1522 | $this->rsm->newObjectMappings[$columnAlias] = [ | ||
1523 | 'className' => $newObjectExpression->className, | ||
1524 | 'objIndex' => $objIndex, | ||
1525 | 'argIndex' => $argIndex, | ||
1526 | ]; | ||
1527 | } | ||
1528 | |||
1529 | return implode(', ', $sqlSelectExpressions); | ||
1530 | } | ||
1531 | |||
1532 | /** | ||
1533 | * Walks down a SimpleSelectExpression AST node, thereby generating the appropriate SQL. | ||
1534 | */ | ||
1535 | public function walkSimpleSelectExpression(AST\SimpleSelectExpression $simpleSelectExpression): string | ||
1536 | { | ||
1537 | $expr = $simpleSelectExpression->expression; | ||
1538 | $sql = ' '; | ||
1539 | |||
1540 | switch (true) { | ||
1541 | case $expr instanceof AST\PathExpression: | ||
1542 | $sql .= $this->walkPathExpression($expr); | ||
1543 | break; | ||
1544 | |||
1545 | case $expr instanceof AST\Subselect: | ||
1546 | $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1547 | |||
1548 | $columnAlias = 'sclr' . $this->aliasCounter++; | ||
1549 | $this->scalarResultAliasMap[$alias] = $columnAlias; | ||
1550 | |||
1551 | $sql .= '(' . $this->walkSubselect($expr) . ') AS ' . $columnAlias; | ||
1552 | break; | ||
1553 | |||
1554 | case $expr instanceof AST\Functions\FunctionNode: | ||
1555 | case $expr instanceof AST\SimpleArithmeticExpression: | ||
1556 | case $expr instanceof AST\ArithmeticTerm: | ||
1557 | case $expr instanceof AST\ArithmeticFactor: | ||
1558 | case $expr instanceof AST\Literal: | ||
1559 | case $expr instanceof AST\NullIfExpression: | ||
1560 | case $expr instanceof AST\CoalesceExpression: | ||
1561 | case $expr instanceof AST\GeneralCaseExpression: | ||
1562 | case $expr instanceof AST\SimpleCaseExpression: | ||
1563 | $alias = $simpleSelectExpression->fieldIdentificationVariable ?: $this->scalarResultCounter++; | ||
1564 | |||
1565 | $columnAlias = $this->getSQLColumnAlias('sclr'); | ||
1566 | $this->scalarResultAliasMap[$alias] = $columnAlias; | ||
1567 | |||
1568 | $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; | ||
1569 | break; | ||
1570 | |||
1571 | case $expr instanceof AST\ParenthesisExpression: | ||
1572 | $sql .= $this->walkParenthesisExpression($expr); | ||
1573 | break; | ||
1574 | |||
1575 | default: // IdentificationVariable | ||
1576 | $sql .= $this->walkEntityIdentificationVariable($expr); | ||
1577 | break; | ||
1578 | } | ||
1579 | |||
1580 | return $sql; | ||
1581 | } | ||
1582 | |||
1583 | /** | ||
1584 | * Walks down an AggregateExpression AST node, thereby generating the appropriate SQL. | ||
1585 | */ | ||
1586 | public function walkAggregateExpression(AST\AggregateExpression $aggExpression): string | ||
1587 | { | ||
1588 | return $aggExpression->functionName . '(' . ($aggExpression->isDistinct ? 'DISTINCT ' : '') | ||
1589 | . $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) . ')'; | ||
1590 | } | ||
1591 | |||
1592 | /** | ||
1593 | * Walks down a GroupByClause AST node, thereby generating the appropriate SQL. | ||
1594 | */ | ||
1595 | public function walkGroupByClause(AST\GroupByClause $groupByClause): string | ||
1596 | { | ||
1597 | $sqlParts = []; | ||
1598 | |||
1599 | foreach ($groupByClause->groupByItems as $groupByItem) { | ||
1600 | $sqlParts[] = $this->walkGroupByItem($groupByItem); | ||
1601 | } | ||
1602 | |||
1603 | return ' GROUP BY ' . implode(', ', $sqlParts); | ||
1604 | } | ||
1605 | |||
1606 | /** | ||
1607 | * Walks down a GroupByItem AST node, thereby generating the appropriate SQL. | ||
1608 | */ | ||
1609 | public function walkGroupByItem(AST\PathExpression|string $groupByItem): string | ||
1610 | { | ||
1611 | // StateFieldPathExpression | ||
1612 | if (! is_string($groupByItem)) { | ||
1613 | return $this->walkPathExpression($groupByItem); | ||
1614 | } | ||
1615 | |||
1616 | // ResultVariable | ||
1617 | if (isset($this->queryComponents[$groupByItem]['resultVariable'])) { | ||
1618 | $resultVariable = $this->queryComponents[$groupByItem]['resultVariable']; | ||
1619 | |||
1620 | if ($resultVariable instanceof AST\PathExpression) { | ||
1621 | return $this->walkPathExpression($resultVariable); | ||
1622 | } | ||
1623 | |||
1624 | if ($resultVariable instanceof AST\Node && isset($resultVariable->pathExpression)) { | ||
1625 | return $this->walkPathExpression($resultVariable->pathExpression); | ||
1626 | } | ||
1627 | |||
1628 | return $this->walkResultVariable($groupByItem); | ||
1629 | } | ||
1630 | |||
1631 | // IdentificationVariable | ||
1632 | $sqlParts = []; | ||
1633 | |||
1634 | foreach ($this->getMetadataForDqlAlias($groupByItem)->fieldNames as $field) { | ||
1635 | $item = new AST\PathExpression(AST\PathExpression::TYPE_STATE_FIELD, $groupByItem, $field); | ||
1636 | $item->type = AST\PathExpression::TYPE_STATE_FIELD; | ||
1637 | |||
1638 | $sqlParts[] = $this->walkPathExpression($item); | ||
1639 | } | ||
1640 | |||
1641 | foreach ($this->getMetadataForDqlAlias($groupByItem)->associationMappings as $mapping) { | ||
1642 | if ($mapping->isToOneOwningSide()) { | ||
1643 | $item = new AST\PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $groupByItem, $mapping->fieldName); | ||
1644 | $item->type = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; | ||
1645 | |||
1646 | $sqlParts[] = $this->walkPathExpression($item); | ||
1647 | } | ||
1648 | } | ||
1649 | |||
1650 | return implode(', ', $sqlParts); | ||
1651 | } | ||
1652 | |||
1653 | /** | ||
1654 | * Walks down a DeleteClause AST node, thereby generating the appropriate SQL. | ||
1655 | */ | ||
1656 | public function walkDeleteClause(AST\DeleteClause $deleteClause): string | ||
1657 | { | ||
1658 | $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); | ||
1659 | $tableName = $class->getTableName(); | ||
1660 | $sql = 'DELETE FROM ' . $this->quoteStrategy->getTableName($class, $this->platform); | ||
1661 | |||
1662 | $this->setSQLTableAlias($tableName, $tableName, $deleteClause->aliasIdentificationVariable); | ||
1663 | $this->rootAliases[] = $deleteClause->aliasIdentificationVariable; | ||
1664 | |||
1665 | return $sql; | ||
1666 | } | ||
1667 | |||
1668 | /** | ||
1669 | * Walks down an UpdateClause AST node, thereby generating the appropriate SQL. | ||
1670 | */ | ||
1671 | public function walkUpdateClause(AST\UpdateClause $updateClause): string | ||
1672 | { | ||
1673 | $class = $this->em->getClassMetadata($updateClause->abstractSchemaName); | ||
1674 | $tableName = $class->getTableName(); | ||
1675 | $sql = 'UPDATE ' . $this->quoteStrategy->getTableName($class, $this->platform); | ||
1676 | |||
1677 | $this->setSQLTableAlias($tableName, $tableName, $updateClause->aliasIdentificationVariable); | ||
1678 | $this->rootAliases[] = $updateClause->aliasIdentificationVariable; | ||
1679 | |||
1680 | return $sql . ' SET ' . implode(', ', array_map($this->walkUpdateItem(...), $updateClause->updateItems)); | ||
1681 | } | ||
1682 | |||
1683 | /** | ||
1684 | * Walks down an UpdateItem AST node, thereby generating the appropriate SQL. | ||
1685 | */ | ||
1686 | public function walkUpdateItem(AST\UpdateItem $updateItem): string | ||
1687 | { | ||
1688 | $useTableAliasesBefore = $this->useSqlTableAliases; | ||
1689 | $this->useSqlTableAliases = false; | ||
1690 | |||
1691 | $sql = $this->walkPathExpression($updateItem->pathExpression) . ' = '; | ||
1692 | $newValue = $updateItem->newValue; | ||
1693 | |||
1694 | $sql .= match (true) { | ||
1695 | $newValue instanceof AST\Node => $newValue->dispatch($this), | ||
1696 | $newValue === null => 'NULL', | ||
1697 | }; | ||
1698 | |||
1699 | $this->useSqlTableAliases = $useTableAliasesBefore; | ||
1700 | |||
1701 | return $sql; | ||
1702 | } | ||
1703 | |||
1704 | /** | ||
1705 | * Walks down a WhereClause AST node, thereby generating the appropriate SQL. | ||
1706 | * | ||
1707 | * WhereClause or not, the appropriate discriminator sql is added. | ||
1708 | */ | ||
1709 | public function walkWhereClause(AST\WhereClause|null $whereClause): string | ||
1710 | { | ||
1711 | $condSql = $whereClause !== null ? $this->walkConditionalExpression($whereClause->conditionalExpression) : ''; | ||
1712 | $discrSql = $this->generateDiscriminatorColumnConditionSQL($this->rootAliases); | ||
1713 | |||
1714 | if ($this->em->hasFilters()) { | ||
1715 | $filterClauses = []; | ||
1716 | foreach ($this->rootAliases as $dqlAlias) { | ||
1717 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1718 | $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); | ||
1719 | |||
1720 | $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); | ||
1721 | if ($filterExpr) { | ||
1722 | $filterClauses[] = $filterExpr; | ||
1723 | } | ||
1724 | } | ||
1725 | |||
1726 | if (count($filterClauses)) { | ||
1727 | if ($condSql) { | ||
1728 | $condSql = '(' . $condSql . ') AND '; | ||
1729 | } | ||
1730 | |||
1731 | $condSql .= implode(' AND ', $filterClauses); | ||
1732 | } | ||
1733 | } | ||
1734 | |||
1735 | if ($condSql) { | ||
1736 | return ' WHERE ' . (! $discrSql ? $condSql : '(' . $condSql . ') AND ' . $discrSql); | ||
1737 | } | ||
1738 | |||
1739 | if ($discrSql) { | ||
1740 | return ' WHERE ' . $discrSql; | ||
1741 | } | ||
1742 | |||
1743 | return ''; | ||
1744 | } | ||
1745 | |||
1746 | /** | ||
1747 | * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL. | ||
1748 | */ | ||
1749 | public function walkConditionalExpression( | ||
1750 | AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr, | ||
1751 | ): string { | ||
1752 | // Phase 2 AST optimization: Skip processing of ConditionalExpression | ||
1753 | // if only one ConditionalTerm is defined | ||
1754 | if (! ($condExpr instanceof AST\ConditionalExpression)) { | ||
1755 | return $this->walkConditionalTerm($condExpr); | ||
1756 | } | ||
1757 | |||
1758 | return implode(' OR ', array_map($this->walkConditionalTerm(...), $condExpr->conditionalTerms)); | ||
1759 | } | ||
1760 | |||
1761 | /** | ||
1762 | * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. | ||
1763 | */ | ||
1764 | public function walkConditionalTerm( | ||
1765 | AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm, | ||
1766 | ): string { | ||
1767 | // Phase 2 AST optimization: Skip processing of ConditionalTerm | ||
1768 | // if only one ConditionalFactor is defined | ||
1769 | if (! ($condTerm instanceof AST\ConditionalTerm)) { | ||
1770 | return $this->walkConditionalFactor($condTerm); | ||
1771 | } | ||
1772 | |||
1773 | return implode(' AND ', array_map($this->walkConditionalFactor(...), $condTerm->conditionalFactors)); | ||
1774 | } | ||
1775 | |||
1776 | /** | ||
1777 | * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. | ||
1778 | */ | ||
1779 | public function walkConditionalFactor( | ||
1780 | AST\ConditionalFactor|AST\ConditionalPrimary $factor, | ||
1781 | ): string { | ||
1782 | // Phase 2 AST optimization: Skip processing of ConditionalFactor | ||
1783 | // if only one ConditionalPrimary is defined | ||
1784 | return ! ($factor instanceof AST\ConditionalFactor) | ||
1785 | ? $this->walkConditionalPrimary($factor) | ||
1786 | : ($factor->not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary); | ||
1787 | } | ||
1788 | |||
1789 | /** | ||
1790 | * Walks down a ConditionalPrimary AST node, thereby generating the appropriate SQL. | ||
1791 | */ | ||
1792 | public function walkConditionalPrimary(AST\ConditionalPrimary $primary): string | ||
1793 | { | ||
1794 | if ($primary->isSimpleConditionalExpression()) { | ||
1795 | return $primary->simpleConditionalExpression->dispatch($this); | ||
1796 | } | ||
1797 | |||
1798 | if ($primary->isConditionalExpression()) { | ||
1799 | $condExpr = $primary->conditionalExpression; | ||
1800 | |||
1801 | return '(' . $this->walkConditionalExpression($condExpr) . ')'; | ||
1802 | } | ||
1803 | |||
1804 | throw new LogicException('Unexpected state of ConditionalPrimary node.'); | ||
1805 | } | ||
1806 | |||
1807 | /** | ||
1808 | * Walks down an ExistsExpression AST node, thereby generating the appropriate SQL. | ||
1809 | */ | ||
1810 | public function walkExistsExpression(AST\ExistsExpression $existsExpr): string | ||
1811 | { | ||
1812 | $sql = $existsExpr->not ? 'NOT ' : ''; | ||
1813 | |||
1814 | $sql .= 'EXISTS (' . $this->walkSubselect($existsExpr->subselect) . ')'; | ||
1815 | |||
1816 | return $sql; | ||
1817 | } | ||
1818 | |||
1819 | /** | ||
1820 | * Walks down a CollectionMemberExpression AST node, thereby generating the appropriate SQL. | ||
1821 | */ | ||
1822 | public function walkCollectionMemberExpression(AST\CollectionMemberExpression $collMemberExpr): string | ||
1823 | { | ||
1824 | $sql = $collMemberExpr->not ? 'NOT ' : ''; | ||
1825 | $sql .= 'EXISTS (SELECT 1 FROM '; | ||
1826 | |||
1827 | $entityExpr = $collMemberExpr->entityExpression; | ||
1828 | $collPathExpr = $collMemberExpr->collectionValuedPathExpression; | ||
1829 | assert($collPathExpr->field !== null); | ||
1830 | |||
1831 | $fieldName = $collPathExpr->field; | ||
1832 | $dqlAlias = $collPathExpr->identificationVariable; | ||
1833 | |||
1834 | $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1835 | |||
1836 | switch (true) { | ||
1837 | // InputParameter | ||
1838 | case $entityExpr instanceof AST\InputParameter: | ||
1839 | $dqlParamKey = $entityExpr->name; | ||
1840 | $entitySql = '?'; | ||
1841 | break; | ||
1842 | |||
1843 | // SingleValuedAssociationPathExpression | IdentificationVariable | ||
1844 | case $entityExpr instanceof AST\PathExpression: | ||
1845 | $entitySql = $this->walkPathExpression($entityExpr); | ||
1846 | break; | ||
1847 | |||
1848 | default: | ||
1849 | throw new BadMethodCallException('Not implemented'); | ||
1850 | } | ||
1851 | |||
1852 | $assoc = $class->associationMappings[$fieldName]; | ||
1853 | |||
1854 | if ($assoc->isOneToMany()) { | ||
1855 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
1856 | $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName()); | ||
1857 | $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
1858 | |||
1859 | $sql .= $this->quoteStrategy->getTableName($targetClass, $this->platform) . ' ' . $targetTableAlias . ' WHERE '; | ||
1860 | |||
1861 | $owningAssoc = $targetClass->associationMappings[$assoc->mappedBy]; | ||
1862 | assert($owningAssoc->isManyToOne()); | ||
1863 | $sqlParts = []; | ||
1864 | |||
1865 | foreach ($owningAssoc->targetToSourceKeyColumns as $targetColumn => $sourceColumn) { | ||
1866 | $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$targetColumn], $class, $this->platform); | ||
1867 | |||
1868 | $sqlParts[] = $sourceTableAlias . '.' . $targetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; | ||
1869 | } | ||
1870 | |||
1871 | foreach ($this->quoteStrategy->getIdentifierColumnNames($targetClass, $this->platform) as $targetColumnName) { | ||
1872 | if (isset($dqlParamKey)) { | ||
1873 | $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); | ||
1874 | } | ||
1875 | |||
1876 | $sqlParts[] = $targetTableAlias . '.' . $targetColumnName . ' = ' . $entitySql; | ||
1877 | } | ||
1878 | |||
1879 | $sql .= implode(' AND ', $sqlParts); | ||
1880 | } else { // many-to-many | ||
1881 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
1882 | |||
1883 | $owningAssoc = $this->em->getMetadataFactory()->getOwningSide($assoc); | ||
1884 | assert($owningAssoc->isManyToManyOwningSide()); | ||
1885 | $joinTable = $owningAssoc->joinTable; | ||
1886 | |||
1887 | // SQL table aliases | ||
1888 | $joinTableAlias = $this->getSQLTableAlias($joinTable->name); | ||
1889 | $sourceTableAlias = $this->getSQLTableAlias($class->getTableName(), $dqlAlias); | ||
1890 | |||
1891 | $sql .= $this->quoteStrategy->getJoinTableName($owningAssoc, $targetClass, $this->platform) . ' ' . $joinTableAlias . ' WHERE '; | ||
1892 | |||
1893 | $joinColumns = $assoc->isOwningSide() ? $joinTable->joinColumns : $joinTable->inverseJoinColumns; | ||
1894 | $sqlParts = []; | ||
1895 | |||
1896 | foreach ($joinColumns as $joinColumn) { | ||
1897 | $targetColumn = $this->quoteStrategy->getColumnName($class->fieldNames[$joinColumn->referencedColumnName], $class, $this->platform); | ||
1898 | |||
1899 | $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' = ' . $sourceTableAlias . '.' . $targetColumn; | ||
1900 | } | ||
1901 | |||
1902 | $joinColumns = $assoc->isOwningSide() ? $joinTable->inverseJoinColumns : $joinTable->joinColumns; | ||
1903 | |||
1904 | foreach ($joinColumns as $joinColumn) { | ||
1905 | if (isset($dqlParamKey)) { | ||
1906 | $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); | ||
1907 | } | ||
1908 | |||
1909 | $sqlParts[] = $joinTableAlias . '.' . $joinColumn->name . ' IN (' . $entitySql . ')'; | ||
1910 | } | ||
1911 | |||
1912 | $sql .= implode(' AND ', $sqlParts); | ||
1913 | } | ||
1914 | |||
1915 | return $sql . ')'; | ||
1916 | } | ||
1917 | |||
1918 | /** | ||
1919 | * Walks down an EmptyCollectionComparisonExpression AST node, thereby generating the appropriate SQL. | ||
1920 | */ | ||
1921 | public function walkEmptyCollectionComparisonExpression(AST\EmptyCollectionComparisonExpression $emptyCollCompExpr): string | ||
1922 | { | ||
1923 | $sizeFunc = new AST\Functions\SizeFunction('size'); | ||
1924 | $sizeFunc->collectionPathExpression = $emptyCollCompExpr->expression; | ||
1925 | |||
1926 | return $sizeFunc->getSql($this) . ($emptyCollCompExpr->not ? ' > 0' : ' = 0'); | ||
1927 | } | ||
1928 | |||
1929 | /** | ||
1930 | * Walks down a NullComparisonExpression AST node, thereby generating the appropriate SQL. | ||
1931 | */ | ||
1932 | public function walkNullComparisonExpression(AST\NullComparisonExpression $nullCompExpr): string | ||
1933 | { | ||
1934 | $expression = $nullCompExpr->expression; | ||
1935 | $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL'; | ||
1936 | |||
1937 | // Handle ResultVariable | ||
1938 | if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) { | ||
1939 | return $this->walkResultVariable($expression) . $comparison; | ||
1940 | } | ||
1941 | |||
1942 | // Handle InputParameter mapping inclusion to ParserResult | ||
1943 | if ($expression instanceof AST\InputParameter) { | ||
1944 | return $this->walkInputParameter($expression) . $comparison; | ||
1945 | } | ||
1946 | |||
1947 | assert(! is_string($expression)); | ||
1948 | |||
1949 | return $expression->dispatch($this) . $comparison; | ||
1950 | } | ||
1951 | |||
1952 | /** | ||
1953 | * Walks down an InExpression AST node, thereby generating the appropriate SQL. | ||
1954 | */ | ||
1955 | public function walkInListExpression(AST\InListExpression $inExpr): string | ||
1956 | { | ||
1957 | return $this->walkArithmeticExpression($inExpr->expression) | ||
1958 | . ($inExpr->not ? ' NOT' : '') . ' IN (' | ||
1959 | . implode(', ', array_map($this->walkInParameter(...), $inExpr->literals)) | ||
1960 | . ')'; | ||
1961 | } | ||
1962 | |||
1963 | /** | ||
1964 | * Walks down an InExpression AST node, thereby generating the appropriate SQL. | ||
1965 | */ | ||
1966 | public function walkInSubselectExpression(AST\InSubselectExpression $inExpr): string | ||
1967 | { | ||
1968 | return $this->walkArithmeticExpression($inExpr->expression) | ||
1969 | . ($inExpr->not ? ' NOT' : '') . ' IN (' | ||
1970 | . $this->walkSubselect($inExpr->subselect) | ||
1971 | . ')'; | ||
1972 | } | ||
1973 | |||
1974 | /** | ||
1975 | * Walks down an InstanceOfExpression AST node, thereby generating the appropriate SQL. | ||
1976 | * | ||
1977 | * @throws QueryException | ||
1978 | */ | ||
1979 | public function walkInstanceOfExpression(AST\InstanceOfExpression $instanceOfExpr): string | ||
1980 | { | ||
1981 | $sql = ''; | ||
1982 | |||
1983 | $dqlAlias = $instanceOfExpr->identificationVariable; | ||
1984 | $discrClass = $class = $this->getMetadataForDqlAlias($dqlAlias); | ||
1985 | |||
1986 | if ($class->discriminatorColumn) { | ||
1987 | $discrClass = $this->em->getClassMetadata($class->rootEntityName); | ||
1988 | } | ||
1989 | |||
1990 | if ($this->useSqlTableAliases) { | ||
1991 | $sql .= $this->getSQLTableAlias($discrClass->getTableName(), $dqlAlias) . '.'; | ||
1992 | } | ||
1993 | |||
1994 | $sql .= $class->getDiscriminatorColumn()->name . ($instanceOfExpr->not ? ' NOT IN ' : ' IN '); | ||
1995 | $sql .= $this->getChildDiscriminatorsFromClassMetadata($discrClass, $instanceOfExpr); | ||
1996 | |||
1997 | return $sql; | ||
1998 | } | ||
1999 | |||
2000 | public function walkInParameter(mixed $inParam): string | ||
2001 | { | ||
2002 | return $inParam instanceof AST\InputParameter | ||
2003 | ? $this->walkInputParameter($inParam) | ||
2004 | : $this->walkArithmeticExpression($inParam); | ||
2005 | } | ||
2006 | |||
2007 | /** | ||
2008 | * Walks down a literal that represents an AST node, thereby generating the appropriate SQL. | ||
2009 | */ | ||
2010 | public function walkLiteral(AST\Literal $literal): string | ||
2011 | { | ||
2012 | return match ($literal->type) { | ||
2013 | AST\Literal::STRING => $this->conn->quote($literal->value), | ||
2014 | AST\Literal::BOOLEAN => (string) $this->conn->getDatabasePlatform()->convertBooleans(strtolower($literal->value) === 'true'), | ||
2015 | AST\Literal::NUMERIC => (string) $literal->value, | ||
2016 | default => throw QueryException::invalidLiteral($literal), | ||
2017 | }; | ||
2018 | } | ||
2019 | |||
2020 | /** | ||
2021 | * Walks down a BetweenExpression AST node, thereby generating the appropriate SQL. | ||
2022 | */ | ||
2023 | public function walkBetweenExpression(AST\BetweenExpression $betweenExpr): string | ||
2024 | { | ||
2025 | $sql = $this->walkArithmeticExpression($betweenExpr->expression); | ||
2026 | |||
2027 | if ($betweenExpr->not) { | ||
2028 | $sql .= ' NOT'; | ||
2029 | } | ||
2030 | |||
2031 | $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression) | ||
2032 | . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression); | ||
2033 | |||
2034 | return $sql; | ||
2035 | } | ||
2036 | |||
2037 | /** | ||
2038 | * Walks down a LikeExpression AST node, thereby generating the appropriate SQL. | ||
2039 | */ | ||
2040 | public function walkLikeExpression(AST\LikeExpression $likeExpr): string | ||
2041 | { | ||
2042 | $stringExpr = $likeExpr->stringExpression; | ||
2043 | if (is_string($stringExpr)) { | ||
2044 | if (! isset($this->queryComponents[$stringExpr]['resultVariable'])) { | ||
2045 | throw new LogicException(sprintf('No result variable found for string expression "%s".', $stringExpr)); | ||
2046 | } | ||
2047 | |||
2048 | $leftExpr = $this->walkResultVariable($stringExpr); | ||
2049 | } else { | ||
2050 | $leftExpr = $stringExpr->dispatch($this); | ||
2051 | } | ||
2052 | |||
2053 | $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; | ||
2054 | |||
2055 | if ($likeExpr->stringPattern instanceof AST\InputParameter) { | ||
2056 | $sql .= $this->walkInputParameter($likeExpr->stringPattern); | ||
2057 | } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) { | ||
2058 | $sql .= $this->walkFunction($likeExpr->stringPattern); | ||
2059 | } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) { | ||
2060 | $sql .= $this->walkPathExpression($likeExpr->stringPattern); | ||
2061 | } else { | ||
2062 | $sql .= $this->walkLiteral($likeExpr->stringPattern); | ||
2063 | } | ||
2064 | |||
2065 | if ($likeExpr->escapeChar) { | ||
2066 | $sql .= ' ESCAPE ' . $this->walkLiteral($likeExpr->escapeChar); | ||
2067 | } | ||
2068 | |||
2069 | return $sql; | ||
2070 | } | ||
2071 | |||
2072 | /** | ||
2073 | * Walks down a StateFieldPathExpression AST node, thereby generating the appropriate SQL. | ||
2074 | */ | ||
2075 | public function walkStateFieldPathExpression(AST\PathExpression $stateFieldPathExpression): string | ||
2076 | { | ||
2077 | return $this->walkPathExpression($stateFieldPathExpression); | ||
2078 | } | ||
2079 | |||
2080 | /** | ||
2081 | * Walks down a ComparisonExpression AST node, thereby generating the appropriate SQL. | ||
2082 | */ | ||
2083 | public function walkComparisonExpression(AST\ComparisonExpression $compExpr): string | ||
2084 | { | ||
2085 | $leftExpr = $compExpr->leftExpression; | ||
2086 | $rightExpr = $compExpr->rightExpression; | ||
2087 | $sql = ''; | ||
2088 | |||
2089 | $sql .= $leftExpr instanceof AST\Node | ||
2090 | ? $leftExpr->dispatch($this) | ||
2091 | : (is_numeric($leftExpr) ? $leftExpr : $this->conn->quote($leftExpr)); | ||
2092 | |||
2093 | $sql .= ' ' . $compExpr->operator . ' '; | ||
2094 | |||
2095 | $sql .= $rightExpr instanceof AST\Node | ||
2096 | ? $rightExpr->dispatch($this) | ||
2097 | : (is_numeric($rightExpr) ? $rightExpr : $this->conn->quote($rightExpr)); | ||
2098 | |||
2099 | return $sql; | ||
2100 | } | ||
2101 | |||
2102 | /** | ||
2103 | * Walks down an InputParameter AST node, thereby generating the appropriate SQL. | ||
2104 | */ | ||
2105 | public function walkInputParameter(AST\InputParameter $inputParam): string | ||
2106 | { | ||
2107 | $this->parserResult->addParameterMapping($inputParam->name, $this->sqlParamIndex++); | ||
2108 | |||
2109 | $parameter = $this->query->getParameter($inputParam->name); | ||
2110 | |||
2111 | if ($parameter) { | ||
2112 | $type = $parameter->getType(); | ||
2113 | if (is_string($type) && Type::hasType($type)) { | ||
2114 | return Type::getType($type)->convertToDatabaseValueSQL('?', $this->platform); | ||
2115 | } | ||
2116 | } | ||
2117 | |||
2118 | return '?'; | ||
2119 | } | ||
2120 | |||
2121 | /** | ||
2122 | * Walks down an ArithmeticExpression AST node, thereby generating the appropriate SQL. | ||
2123 | */ | ||
2124 | public function walkArithmeticExpression(AST\ArithmeticExpression $arithmeticExpr): string | ||
2125 | { | ||
2126 | return $arithmeticExpr->isSimpleArithmeticExpression() | ||
2127 | ? $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression) | ||
2128 | : '(' . $this->walkSubselect($arithmeticExpr->subselect) . ')'; | ||
2129 | } | ||
2130 | |||
2131 | /** | ||
2132 | * Walks down an SimpleArithmeticExpression AST node, thereby generating the appropriate SQL. | ||
2133 | */ | ||
2134 | public function walkSimpleArithmeticExpression(AST\Node|string $simpleArithmeticExpr): string | ||
2135 | { | ||
2136 | if (! ($simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression)) { | ||
2137 | return $this->walkArithmeticTerm($simpleArithmeticExpr); | ||
2138 | } | ||
2139 | |||
2140 | return implode(' ', array_map($this->walkArithmeticTerm(...), $simpleArithmeticExpr->arithmeticTerms)); | ||
2141 | } | ||
2142 | |||
2143 | /** | ||
2144 | * Walks down an ArithmeticTerm AST node, thereby generating the appropriate SQL. | ||
2145 | */ | ||
2146 | public function walkArithmeticTerm(AST\Node|string $term): string | ||
2147 | { | ||
2148 | if (is_string($term)) { | ||
2149 | return isset($this->queryComponents[$term]) | ||
2150 | ? $this->walkResultVariable($this->queryComponents[$term]['token']->value) | ||
2151 | : $term; | ||
2152 | } | ||
2153 | |||
2154 | // Phase 2 AST optimization: Skip processing of ArithmeticTerm | ||
2155 | // if only one ArithmeticFactor is defined | ||
2156 | if (! ($term instanceof AST\ArithmeticTerm)) { | ||
2157 | return $this->walkArithmeticFactor($term); | ||
2158 | } | ||
2159 | |||
2160 | return implode(' ', array_map($this->walkArithmeticFactor(...), $term->arithmeticFactors)); | ||
2161 | } | ||
2162 | |||
2163 | /** | ||
2164 | * Walks down an ArithmeticFactor that represents an AST node, thereby generating the appropriate SQL. | ||
2165 | */ | ||
2166 | public function walkArithmeticFactor(AST\Node|string $factor): string | ||
2167 | { | ||
2168 | if (is_string($factor)) { | ||
2169 | return isset($this->queryComponents[$factor]) | ||
2170 | ? $this->walkResultVariable($this->queryComponents[$factor]['token']->value) | ||
2171 | : $factor; | ||
2172 | } | ||
2173 | |||
2174 | // Phase 2 AST optimization: Skip processing of ArithmeticFactor | ||
2175 | // if only one ArithmeticPrimary is defined | ||
2176 | if (! ($factor instanceof AST\ArithmeticFactor)) { | ||
2177 | return $this->walkArithmeticPrimary($factor); | ||
2178 | } | ||
2179 | |||
2180 | $sign = $factor->isNegativeSigned() ? '-' : ($factor->isPositiveSigned() ? '+' : ''); | ||
2181 | |||
2182 | return $sign . $this->walkArithmeticPrimary($factor->arithmeticPrimary); | ||
2183 | } | ||
2184 | |||
2185 | /** | ||
2186 | * Walks down an ArithmeticPrimary that represents an AST node, thereby generating the appropriate SQL. | ||
2187 | */ | ||
2188 | public function walkArithmeticPrimary(AST\Node|string $primary): string | ||
2189 | { | ||
2190 | if ($primary instanceof AST\SimpleArithmeticExpression) { | ||
2191 | return '(' . $this->walkSimpleArithmeticExpression($primary) . ')'; | ||
2192 | } | ||
2193 | |||
2194 | if ($primary instanceof AST\Node) { | ||
2195 | return $primary->dispatch($this); | ||
2196 | } | ||
2197 | |||
2198 | return $this->walkEntityIdentificationVariable($primary); | ||
2199 | } | ||
2200 | |||
2201 | /** | ||
2202 | * Walks down a StringPrimary that represents an AST node, thereby generating the appropriate SQL. | ||
2203 | */ | ||
2204 | public function walkStringPrimary(AST\Node|string $stringPrimary): string | ||
2205 | { | ||
2206 | return is_string($stringPrimary) | ||
2207 | ? $this->conn->quote($stringPrimary) | ||
2208 | : $stringPrimary->dispatch($this); | ||
2209 | } | ||
2210 | |||
2211 | /** | ||
2212 | * Walks down a ResultVariable that represents an AST node, thereby generating the appropriate SQL. | ||
2213 | */ | ||
2214 | public function walkResultVariable(string $resultVariable): string | ||
2215 | { | ||
2216 | if (! isset($this->scalarResultAliasMap[$resultVariable])) { | ||
2217 | throw new InvalidArgumentException(sprintf('Unknown result variable: %s', $resultVariable)); | ||
2218 | } | ||
2219 | |||
2220 | $resultAlias = $this->scalarResultAliasMap[$resultVariable]; | ||
2221 | |||
2222 | if (is_array($resultAlias)) { | ||
2223 | return implode(', ', $resultAlias); | ||
2224 | } | ||
2225 | |||
2226 | return $resultAlias; | ||
2227 | } | ||
2228 | |||
2229 | /** | ||
2230 | * @return string The list in parentheses of valid child discriminators from the given class | ||
2231 | * | ||
2232 | * @throws QueryException | ||
2233 | */ | ||
2234 | private function getChildDiscriminatorsFromClassMetadata( | ||
2235 | ClassMetadata $rootClass, | ||
2236 | AST\InstanceOfExpression $instanceOfExpr, | ||
2237 | ): string { | ||
2238 | $sqlParameterList = []; | ||
2239 | $discriminators = []; | ||
2240 | foreach ($instanceOfExpr->value as $parameter) { | ||
2241 | if ($parameter instanceof AST\InputParameter) { | ||
2242 | $this->rsm->discriminatorParameters[$parameter->name] = $parameter->name; | ||
2243 | $sqlParameterList[] = $this->walkInParameter($parameter); | ||
2244 | continue; | ||
2245 | } | ||
2246 | |||
2247 | $metadata = $this->em->getClassMetadata($parameter); | ||
2248 | |||
2249 | if ($metadata->getName() !== $rootClass->name && ! $metadata->getReflectionClass()->isSubclassOf($rootClass->name)) { | ||
2250 | throw QueryException::instanceOfUnrelatedClass($parameter, $rootClass->name); | ||
2251 | } | ||
2252 | |||
2253 | $discriminators += HierarchyDiscriminatorResolver::resolveDiscriminatorsForClass($metadata, $this->em); | ||
2254 | } | ||
2255 | |||
2256 | foreach (array_keys($discriminators) as $discriminatorValue) { | ||
2257 | $sqlParameterList[] = $rootClass->getDiscriminatorColumn()->type === 'integer' && is_int($discriminatorValue) | ||
2258 | ? $discriminatorValue | ||
2259 | : $this->conn->quote((string) $discriminatorValue); | ||
2260 | } | ||
2261 | |||
2262 | return '(' . implode(', ', $sqlParameterList) . ')'; | ||
2263 | } | ||
2264 | } | ||