1<?php
2namespace TYPO3\CMS\Extbase\Persistence\Generic\Storage;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Backend\Utility\BackendUtility;
18use TYPO3\CMS\Core\Database\Connection;
19use TYPO3\CMS\Core\Database\ConnectionPool;
20use TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression;
21use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
22use TYPO3\CMS\Core\Database\Query\QueryBuilder;
23use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
24use TYPO3\CMS\Core\Utility\GeneralUtility;
25use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
26use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject;
27use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
28use TYPO3\CMS\Extbase\Persistence\Generic\Exception;
29use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InconsistentQuerySettingsException;
30use TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidRelationConfigurationException;
31use TYPO3\CMS\Extbase\Persistence\Generic\Exception\MissingColumnMapException;
32use TYPO3\CMS\Extbase\Persistence\Generic\Exception\RepositoryException;
33use TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnsupportedOrderException;
34use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\ColumnMap;
35use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper;
36use TYPO3\CMS\Extbase\Persistence\Generic\Qom;
37use TYPO3\CMS\Extbase\Persistence\Generic\QuerySettingsInterface;
38use TYPO3\CMS\Extbase\Persistence\Generic\Storage\Exception\BadConstraintException;
39use TYPO3\CMS\Extbase\Persistence\QueryInterface;
40use TYPO3\CMS\Extbase\Service\EnvironmentService;
41use TYPO3\CMS\Frontend\Page\PageRepository;
42
43/**
44 * QueryParser, converting the qom to string representation
45 * @internal only to be used within Extbase, not part of TYPO3 Core API.
46 */
47class Typo3DbQueryParser
48{
49    /**
50     * @var DataMapper
51     */
52    protected $dataMapper;
53
54    /**
55     * The TYPO3 page repository. Used for language and workspace overlay
56     *
57     * @var PageRepository
58     */
59    protected $pageRepository;
60
61    /**
62     * @var EnvironmentService
63     */
64    protected $environmentService;
65
66    /**
67     * @var ConfigurationManagerInterface
68     */
69    protected $configurationManager;
70
71    /**
72     * Instance of the Doctrine query builder
73     *
74     * @var QueryBuilder
75     */
76    protected $queryBuilder;
77
78    /**
79     * Maps domain model properties to their corresponding table aliases that are used in the query, e.g.:
80     *
81     * 'property1' => 'tableName',
82     * 'property1.property2' => 'tableName1',
83     *
84     * @var array
85     */
86    protected $tablePropertyMap = [];
87
88    /**
89     * Maps tablenames to their aliases to be used in where clauses etc.
90     * Mainly used for joins on the same table etc.
91     *
92     * @var array
93     */
94    protected $tableAliasMap = [];
95
96    /**
97     * Stores all tables used in for SQL joins
98     *
99     * @var array
100     */
101    protected $unionTableAliasCache = [];
102
103    /**
104     * @var string
105     */
106    protected $tableName = '';
107
108    /**
109     * @var bool
110     */
111    protected $suggestDistinctQuery = false;
112
113    /**
114     * @var ObjectManagerInterface
115     */
116    protected $objectManager;
117
118    /**
119     * @param ObjectManagerInterface $objectManager
120     */
121    public function injectObjectManager(ObjectManagerInterface $objectManager)
122    {
123        $this->objectManager = $objectManager;
124    }
125
126    /**
127     * Object initialization called when object is created with ObjectManager, after constructor
128     */
129    public function initializeObject()
130    {
131        $this->dataMapper = $this->objectManager->get(DataMapper::class);
132    }
133
134    /**
135     * @param EnvironmentService $environmentService
136     */
137    public function injectEnvironmentService(EnvironmentService $environmentService)
138    {
139        $this->environmentService = $environmentService;
140    }
141
142    /**
143     * @param ConfigurationManagerInterface $configurationManager
144     */
145    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager)
146    {
147        $this->configurationManager = $configurationManager;
148    }
149
150    /**
151     * Whether using a distinct query is suggested.
152     * This information is defined during parsing of the current query
153     * for RELATION_HAS_MANY & RELATION_HAS_AND_BELONGS_TO_MANY relations.
154     *
155     * @return bool
156     */
157    public function isDistinctQuerySuggested(): bool
158    {
159        return $this->suggestDistinctQuery;
160    }
161
162    /**
163     * Returns a ready to be executed QueryBuilder object, based on the query
164     *
165     * @param QueryInterface $query
166     * @return QueryBuilder
167     */
168    public function convertQueryToDoctrineQueryBuilder(QueryInterface $query)
169    {
170        // Reset all properties
171        $this->tablePropertyMap = [];
172        $this->tableAliasMap = [];
173        $this->unionTableAliasCache = [];
174        $this->tableName = '';
175
176        if ($query->getStatement() && $query->getStatement()->getStatement() instanceof QueryBuilder) {
177            $this->queryBuilder = clone $query->getStatement()->getStatement();
178            return $this->queryBuilder;
179        }
180        // Find the right table name
181        $source = $query->getSource();
182        $this->initializeQueryBuilder($source);
183
184        $constraint = $query->getConstraint();
185        if ($constraint instanceof Qom\ConstraintInterface) {
186            $wherePredicates = $this->parseConstraint($constraint, $source);
187            if (!empty($wherePredicates)) {
188                $this->queryBuilder->andWhere($wherePredicates);
189            }
190        }
191
192        $this->parseOrderings($query->getOrderings(), $source);
193        $this->addTypo3Constraints($query);
194
195        return $this->queryBuilder;
196    }
197
198    /**
199     * Creates the queryBuilder object whether it is a regular select or a JOIN
200     *
201     * @param Qom\SourceInterface $source The source
202     */
203    protected function initializeQueryBuilder(Qom\SourceInterface $source)
204    {
205        if ($source instanceof Qom\SelectorInterface) {
206            $className = $source->getNodeTypeName();
207            $tableName = $this->dataMapper->getDataMap($className)->getTableName();
208            $this->tableName = $tableName;
209
210            $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
211                ->getQueryBuilderForTable($tableName);
212
213            $this->queryBuilder
214                ->getRestrictions()
215                ->removeAll();
216
217            $tableAlias = $this->getUniqueAlias($tableName);
218
219            $this->queryBuilder
220                ->select($tableAlias . '.*')
221                ->from($tableName, $tableAlias);
222
223            $this->addRecordTypeConstraint($className);
224        } elseif ($source instanceof Qom\JoinInterface) {
225            $leftSource = $source->getLeft();
226            $leftTableName = $leftSource->getSelectorName();
227
228            $this->queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
229                ->getQueryBuilderForTable($leftTableName);
230            $leftTableAlias = $this->getUniqueAlias($leftTableName);
231            $this->queryBuilder
232                ->select($leftTableAlias . '.*')
233                ->from($leftTableName, $leftTableAlias);
234            $this->parseJoin($source, $leftTableAlias);
235        }
236    }
237
238    /**
239     * Transforms a constraint into SQL and parameter arrays
240     *
241     * @param Qom\ConstraintInterface $constraint The constraint
242     * @param Qom\SourceInterface $source The source
243     * @return CompositeExpression|string
244     * @throws \RuntimeException
245     */
246    protected function parseConstraint(Qom\ConstraintInterface $constraint, Qom\SourceInterface $source)
247    {
248        if ($constraint instanceof Qom\AndInterface) {
249            $constraint1 = $constraint->getConstraint1();
250            $constraint2 = $constraint->getConstraint2();
251            if (($constraint1 instanceof Qom\ConstraintInterface)
252                && ($constraint2 instanceof Qom\ConstraintInterface)
253            ) {
254                return $this->queryBuilder->expr()->andX(
255                    $this->parseConstraint($constraint1, $source),
256                    $this->parseConstraint($constraint2, $source)
257                );
258            }
259            return '';
260        }
261        if ($constraint instanceof Qom\OrInterface) {
262            $constraint1 = $constraint->getConstraint1();
263            $constraint2 = $constraint->getConstraint2();
264            if (($constraint1 instanceof Qom\ConstraintInterface)
265                && ($constraint2 instanceof Qom\ConstraintInterface)
266            ) {
267                return $this->queryBuilder->expr()->orX(
268                    $this->parseConstraint($constraint->getConstraint1(), $source),
269                    $this->parseConstraint($constraint->getConstraint2(), $source)
270                );
271            }
272            return '';
273        }
274        if ($constraint instanceof Qom\NotInterface) {
275            return ' NOT(' . $this->parseConstraint($constraint->getConstraint(), $source) . ')';
276        }
277        if ($constraint instanceof Qom\ComparisonInterface) {
278            return $this->parseComparison($constraint, $source);
279        }
280        throw new \RuntimeException('not implemented', 1476199898);
281    }
282
283    /**
284     * Transforms orderings into SQL.
285     *
286     * @param array $orderings An array of orderings (Qom\Ordering)
287     * @param Qom\SourceInterface $source The source
288     * @throws UnsupportedOrderException
289     */
290    protected function parseOrderings(array $orderings, Qom\SourceInterface $source)
291    {
292        foreach ($orderings as $propertyName => $order) {
293            if ($order !== QueryInterface::ORDER_ASCENDING && $order !== QueryInterface::ORDER_DESCENDING) {
294                throw new UnsupportedOrderException('Unsupported order encountered.', 1242816074);
295            }
296            $className = null;
297            $tableName = '';
298            if ($source instanceof Qom\SelectorInterface) {
299                $className = $source->getNodeTypeName();
300                $tableName = $this->dataMapper->convertClassNameToTableName($className);
301                $fullPropertyPath = '';
302                while (strpos($propertyName, '.') !== false) {
303                    $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
304                }
305            } elseif ($source instanceof Qom\JoinInterface) {
306                $tableName = $source->getLeft()->getSelectorName();
307            }
308            $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
309            if ($tableName !== '') {
310                $this->queryBuilder->addOrderBy($tableName . '.' . $columnName, $order);
311            } else {
312                $this->queryBuilder->addOrderBy($columnName, $order);
313            }
314        }
315    }
316
317    /**
318     * add TYPO3 Constraints for all tables to the queryBuilder
319     *
320     * @param QueryInterface $query
321     */
322    protected function addTypo3Constraints(QueryInterface $query)
323    {
324        $index = 0;
325        foreach ($this->tableAliasMap as $tableAlias => $tableName) {
326            if ($index === 0 || !$this->configurationManager->isFeatureEnabled('consistentTranslationOverlayHandling')) {
327                // With the new behaviour enabled, we only add the pid and language check for the first table (aggregate root).
328                // We know the first table is always the main table for the current query run.
329                $additionalWhereClauses = $this->getAdditionalWhereClause($query->getQuerySettings(), $tableName, $tableAlias);
330            } else {
331                $additionalWhereClauses = [];
332            }
333            $index++;
334            $statement = $this->getVisibilityConstraintStatement($query->getQuerySettings(), $tableName, $tableAlias);
335            if ($statement !== '') {
336                $additionalWhereClauses[] = $statement;
337            }
338            if (!empty($additionalWhereClauses)) {
339                if (in_array($tableAlias, $this->unionTableAliasCache, true)) {
340                    $this->queryBuilder->andWhere(
341                        $this->queryBuilder->expr()->orX(
342                            $this->queryBuilder->expr()->andX(...$additionalWhereClauses),
343                            $this->queryBuilder->expr()->isNull($tableAlias . '.uid')
344                        )
345                    );
346                } else {
347                    $this->queryBuilder->andWhere(...$additionalWhereClauses);
348                }
349            }
350        }
351    }
352
353    /**
354     * Parse a Comparison into SQL and parameter arrays.
355     *
356     * @param Qom\ComparisonInterface $comparison The comparison to parse
357     * @param Qom\SourceInterface $source The source
358     * @return string
359     * @throws \RuntimeException
360     * @throws RepositoryException
361     * @throws BadConstraintException
362     */
363    protected function parseComparison(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source)
364    {
365        if ($comparison->getOperator() === QueryInterface::OPERATOR_CONTAINS) {
366            if ($comparison->getOperand2() === null) {
367                throw new BadConstraintException('The value for the CONTAINS operator must not be null.', 1484828468);
368            }
369            $value = $this->dataMapper->getPlainValue($comparison->getOperand2());
370            if (!$source instanceof Qom\SelectorInterface) {
371                throw new \RuntimeException('Source is not of type "SelectorInterface"', 1395362539);
372            }
373            $className = $source->getNodeTypeName();
374            $tableName = $this->dataMapper->convertClassNameToTableName($className);
375            $operand1 = $comparison->getOperand1();
376            $propertyName = $operand1->getPropertyName();
377            $fullPropertyPath = '';
378            while (strpos($propertyName, '.') !== false) {
379                $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
380            }
381            $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
382            $dataMap = $this->dataMapper->getDataMap($className);
383            $columnMap = $dataMap->getColumnMap($propertyName);
384            $typeOfRelation = $columnMap instanceof ColumnMap ? $columnMap->getTypeOfRelation() : null;
385            if ($typeOfRelation === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
386                $relationTableName = $columnMap->getRelationTableName();
387                $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
388                $queryBuilderForSubselect
389                        ->select($columnMap->getParentKeyFieldName())
390                        ->from($relationTableName)
391                        ->where(
392                            $queryBuilderForSubselect->expr()->eq(
393                                $columnMap->getChildKeyFieldName(),
394                                $this->queryBuilder->createNamedParameter($value)
395                            )
396                        );
397                $additionalWhereForMatchFields = $this->getAdditionalMatchFieldsStatement($queryBuilderForSubselect->expr(), $columnMap, $relationTableName, $relationTableName);
398                if ($additionalWhereForMatchFields) {
399                    $queryBuilderForSubselect->andWhere($additionalWhereForMatchFields);
400                }
401
402                return $this->queryBuilder->expr()->comparison(
403                    $this->queryBuilder->quoteIdentifier($tableName . '.uid'),
404                    'IN',
405                    '(' . $queryBuilderForSubselect->getSQL() . ')'
406                );
407            }
408            if ($typeOfRelation === ColumnMap::RELATION_HAS_MANY) {
409                $parentKeyFieldName = $columnMap->getParentKeyFieldName();
410                if (isset($parentKeyFieldName)) {
411                    $childTableName = $columnMap->getChildTableName();
412
413                    // Build the SQL statement of the subselect
414                    $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
415                    $queryBuilderForSubselect
416                            ->select($parentKeyFieldName)
417                            ->from($childTableName)
418                            ->where(
419                                $queryBuilderForSubselect->expr()->eq(
420                                    'uid',
421                                    (int)$value
422                                )
423                            );
424
425                    // Add it to the main query
426                    return $this->queryBuilder->expr()->eq(
427                        $tableName . '.uid',
428                        '(' . $queryBuilderForSubselect->getSQL() . ')'
429                    );
430                }
431                return $this->queryBuilder->expr()->inSet(
432                    $tableName . '.' . $columnName,
433                    $this->queryBuilder->quote($value)
434                );
435            }
436            throw new RepositoryException('Unsupported or non-existing property name "' . $propertyName . '" used in relation matching.', 1327065745);
437        }
438        return $this->parseDynamicOperand($comparison, $source);
439    }
440
441    /**
442     * Parse a DynamicOperand into SQL and parameter arrays.
443     *
444     * @param Qom\ComparisonInterface $comparison
445     * @param Qom\SourceInterface $source The source
446     * @return string
447     * @throws Exception
448     * @throws BadConstraintException
449     */
450    protected function parseDynamicOperand(Qom\ComparisonInterface $comparison, Qom\SourceInterface $source)
451    {
452        $value = $comparison->getOperand2();
453        $fieldName = $this->parseOperand($comparison->getOperand1(), $source);
454        $expr = null;
455        $exprBuilder = $this->queryBuilder->expr();
456        switch ($comparison->getOperator()) {
457            case QueryInterface::OPERATOR_IN:
458                $hasValue = false;
459                $plainValues = [];
460                foreach ($value as $singleValue) {
461                    $plainValue = $this->dataMapper->getPlainValue($singleValue);
462                    if ($plainValue !== null) {
463                        $hasValue = true;
464                        $plainValues[] = $this->createTypedNamedParameter($singleValue);
465                    }
466                }
467                if (!$hasValue) {
468                    throw new BadConstraintException(
469                        'The IN operator needs a non-empty value list to compare against. ' .
470                        'The given value list is empty.',
471                        1484828466
472                    );
473                }
474                $expr = $exprBuilder->comparison($fieldName, 'IN', '(' . implode(', ', $plainValues) . ')');
475                break;
476            case QueryInterface::OPERATOR_EQUAL_TO:
477                if ($value === null) {
478                    $expr = $fieldName . ' IS NULL';
479                } else {
480                    $placeHolder = $this->createTypedNamedParameter($value);
481                    $expr = $exprBuilder->comparison($fieldName, $exprBuilder::EQ, $placeHolder);
482                }
483                break;
484            case QueryInterface::OPERATOR_EQUAL_TO_NULL:
485                $expr = $fieldName . ' IS NULL';
486                break;
487            case QueryInterface::OPERATOR_NOT_EQUAL_TO:
488                if ($value === null) {
489                    $expr = $fieldName . ' IS NOT NULL';
490                } else {
491                    $placeHolder = $this->createTypedNamedParameter($value);
492                    $expr = $exprBuilder->comparison($fieldName, $exprBuilder::NEQ, $placeHolder);
493                }
494                break;
495            case QueryInterface::OPERATOR_NOT_EQUAL_TO_NULL:
496                $expr = $fieldName . ' IS NOT NULL';
497                break;
498            case QueryInterface::OPERATOR_LESS_THAN:
499                $placeHolder = $this->createTypedNamedParameter($value);
500                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LT, $placeHolder);
501                break;
502            case QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO:
503                $placeHolder = $this->createTypedNamedParameter($value);
504                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::LTE, $placeHolder);
505                break;
506            case QueryInterface::OPERATOR_GREATER_THAN:
507                $placeHolder = $this->createTypedNamedParameter($value);
508                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GT, $placeHolder);
509                break;
510            case QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO:
511                $placeHolder = $this->createTypedNamedParameter($value);
512                $expr = $exprBuilder->comparison($fieldName, $exprBuilder::GTE, $placeHolder);
513                break;
514            case QueryInterface::OPERATOR_LIKE:
515                $placeHolder = $this->createTypedNamedParameter($value, \PDO::PARAM_STR);
516                $expr = $exprBuilder->comparison($fieldName, 'LIKE', $placeHolder);
517                break;
518            default:
519                throw new Exception(
520                    'Unsupported operator encountered.',
521                    1242816073
522                );
523        }
524        return $expr;
525    }
526
527    /**
528     * Maps plain value of operand to PDO types to help Doctrine and/or the database driver process the value
529     * correctly when building the query.
530     *
531     * @param mixed $value The parameter value
532     * @return int
533     * @throws \InvalidArgumentException
534     */
535    protected function getParameterType($value): int
536    {
537        $parameterType = gettype($value);
538        switch ($parameterType) {
539            case 'integer':
540                return \PDO::PARAM_INT;
541            case 'string':
542                return \PDO::PARAM_STR;
543            default:
544                throw new \InvalidArgumentException(
545                    'Unsupported parameter type encountered. Expected integer or string, ' . $parameterType . ' given.',
546                    1494878863
547                );
548        }
549    }
550
551    /**
552     * Create a named parameter for the QueryBuilder and guess the parameter type based on the
553     * output of DataMapper::getPlainValue(). The type of the named parameter can be forced to
554     * one of the \PDO::PARAM_* types by specifying the $forceType argument.
555     *
556     * @param mixed $value The input value that should be sent to the database
557     * @param int|null $forceType The \PDO::PARAM_* type that should be forced
558     * @return string The placeholder string to be used in the query
559     * @see \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::getPlainValue()
560     */
561    protected function createTypedNamedParameter($value, int $forceType = null): string
562    {
563        $consistentHandlingEnabled = $this->configurationManager->isFeatureEnabled('consistentTranslationOverlayHandling');
564        if ($consistentHandlingEnabled
565            && $value instanceof AbstractDomainObject
566            && $value->_hasProperty('_localizedUid')
567            && $value->_getProperty('_localizedUid') > 0
568        ) {
569            $plainValue = (int)$value->_getProperty('_localizedUid');
570        } else {
571            $plainValue = $this->dataMapper->getPlainValue($value);
572        }
573        $parameterType = $forceType ?? $this->getParameterType($plainValue);
574        $placeholder = $this->queryBuilder->createNamedParameter($plainValue, $parameterType);
575
576        return $placeholder;
577    }
578
579    /**
580     * @param Qom\DynamicOperandInterface $operand
581     * @param Qom\SourceInterface $source The source
582     * @return string
583     * @throws \InvalidArgumentException
584     */
585    protected function parseOperand(Qom\DynamicOperandInterface $operand, Qom\SourceInterface $source)
586    {
587        if ($operand instanceof Qom\LowerCaseInterface) {
588            $constraintSQL = 'LOWER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
589        } elseif ($operand instanceof Qom\UpperCaseInterface) {
590            $constraintSQL = 'UPPER(' . $this->parseOperand($operand->getOperand(), $source) . ')';
591        } elseif ($operand instanceof Qom\PropertyValueInterface) {
592            $propertyName = $operand->getPropertyName();
593            $className = '';
594            if ($source instanceof Qom\SelectorInterface) {
595                $className = $source->getNodeTypeName();
596                $tableName = $this->dataMapper->convertClassNameToTableName($className);
597                $fullPropertyPath = '';
598                while (strpos($propertyName, '.') !== false) {
599                    $this->addUnionStatement($className, $tableName, $propertyName, $fullPropertyPath);
600                }
601            } elseif ($source instanceof Qom\JoinInterface) {
602                $tableName = $source->getJoinCondition()->getSelector1Name();
603            }
604            $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
605            $constraintSQL = (!empty($tableName) ? $tableName . '.' : '') . $columnName;
606            $constraintSQL = $this->queryBuilder->getConnection()->quoteIdentifier($constraintSQL);
607        } else {
608            throw new \InvalidArgumentException('Given operand has invalid type "' . get_class($operand) . '".', 1395710211);
609        }
610        return $constraintSQL;
611    }
612
613    /**
614     * Add a constraint to ensure that the record type of the returned tuples is matching the data type of the repository.
615     *
616     * @param string $className The class name
617     */
618    protected function addRecordTypeConstraint($className)
619    {
620        if ($className !== null) {
621            $dataMap = $this->dataMapper->getDataMap($className);
622            if ($dataMap->getRecordTypeColumnName() !== null) {
623                $recordTypes = [];
624                if ($dataMap->getRecordType() !== null) {
625                    $recordTypes[] = $dataMap->getRecordType();
626                }
627                foreach ($dataMap->getSubclasses() as $subclassName) {
628                    $subclassDataMap = $this->dataMapper->getDataMap($subclassName);
629                    if ($subclassDataMap->getRecordType() !== null) {
630                        $recordTypes[] = $subclassDataMap->getRecordType();
631                    }
632                }
633                if (!empty($recordTypes)) {
634                    $recordTypeStatements = [];
635                    foreach ($recordTypes as $recordType) {
636                        $tableName = $dataMap->getTableName();
637                        $recordTypeStatements[] = $this->queryBuilder->expr()->eq(
638                            $tableName . '.' . $dataMap->getRecordTypeColumnName(),
639                            $this->queryBuilder->createNamedParameter($recordType)
640                        );
641                    }
642                    $this->queryBuilder->andWhere(
643                        $this->queryBuilder->expr()->orX(...$recordTypeStatements)
644                    );
645                }
646            }
647        }
648    }
649
650    /**
651     * Builds a condition for filtering records by the configured match field,
652     * e.g. MM_match_fields, foreign_match_fields or foreign_table_field.
653     *
654     * @param ExpressionBuilder $exprBuilder
655     * @param ColumnMap $columnMap The column man for which the condition should be build.
656     * @param string $childTableAlias The alias of the child record table used in the query.
657     * @param string $parentTable The real name of the parent table (used for building the foreign_table_field condition).
658     * @return string The match field conditions or an empty string.
659     */
660    protected function getAdditionalMatchFieldsStatement($exprBuilder, $columnMap, $childTableAlias, $parentTable = null)
661    {
662        $additionalWhereForMatchFields = [];
663        $relationTableMatchFields = $columnMap->getRelationTableMatchFields();
664        if (is_array($relationTableMatchFields) && !empty($relationTableMatchFields)) {
665            foreach ($relationTableMatchFields as $fieldName => $value) {
666                $additionalWhereForMatchFields[] = $exprBuilder->eq($childTableAlias . '.' . $fieldName, $this->queryBuilder->createNamedParameter($value));
667            }
668        }
669
670        if (isset($parentTable)) {
671            $parentTableFieldName = $columnMap->getParentTableFieldName();
672            if (!empty($parentTableFieldName)) {
673                $additionalWhereForMatchFields[] = $exprBuilder->eq($childTableAlias . '.' . $parentTableFieldName, $this->queryBuilder->createNamedParameter($parentTable));
674            }
675        }
676
677        if (!empty($additionalWhereForMatchFields)) {
678            return $exprBuilder->andX(...$additionalWhereForMatchFields);
679        }
680        return '';
681    }
682
683    /**
684     * Adds additional WHERE statements according to the query settings.
685     *
686     * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
687     * @param string $tableName The table name to add the additional where clause for
688     * @param string $tableAlias The table alias used in the query.
689     * @return array
690     */
691    protected function getAdditionalWhereClause(QuerySettingsInterface $querySettings, $tableName, $tableAlias = null)
692    {
693        $whereClause = [];
694        if ($querySettings->getRespectSysLanguage()) {
695            if ($this->configurationManager->isFeatureEnabled('consistentTranslationOverlayHandling')) {
696                $systemLanguageStatement = $this->getLanguageStatement($tableName, $tableAlias, $querySettings);
697            } else {
698                $systemLanguageStatement = $this->getSysLanguageStatement($tableName, $tableAlias, $querySettings);
699            }
700
701            if (!empty($systemLanguageStatement)) {
702                $whereClause[] = $systemLanguageStatement;
703            }
704        }
705
706        if ($querySettings->getRespectStoragePage()) {
707            $pageIdStatement = $this->getPageIdStatement($tableName, $tableAlias, $querySettings->getStoragePageIds());
708            if (!empty($pageIdStatement)) {
709                $whereClause[] = $pageIdStatement;
710            }
711        }
712
713        return $whereClause;
714    }
715
716    /**
717     * Adds enableFields and deletedClause to the query if necessary
718     *
719     * @param QuerySettingsInterface $querySettings
720     * @param string $tableName The database table name
721     * @param string $tableAlias
722     * @return string
723     */
724    protected function getVisibilityConstraintStatement(QuerySettingsInterface $querySettings, $tableName, $tableAlias)
725    {
726        $statement = '';
727        if (is_array($GLOBALS['TCA'][$tableName]['ctrl'] ?? null)) {
728            $ignoreEnableFields = $querySettings->getIgnoreEnableFields();
729            $enableFieldsToBeIgnored = $querySettings->getEnableFieldsToBeIgnored();
730            $includeDeleted = $querySettings->getIncludeDeleted();
731            if ($this->environmentService->isEnvironmentInFrontendMode()) {
732                $statement .= $this->getFrontendConstraintStatement($tableName, $ignoreEnableFields, $enableFieldsToBeIgnored, $includeDeleted);
733            } else {
734                // TYPO3_MODE === 'BE'
735                $statement .= $this->getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted);
736            }
737            if (!empty($statement)) {
738                $statement = $this->replaceTableNameWithAlias($statement, $tableName, $tableAlias);
739                $statement = strtolower(substr($statement, 1, 3)) === 'and' ? substr($statement, 5) : $statement;
740            }
741        }
742        return $statement;
743    }
744
745    /**
746     * Returns constraint statement for frontend context
747     *
748     * @param string $tableName
749     * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
750     * @param array $enableFieldsToBeIgnored If $ignoreEnableFields is true, this array specifies enable fields to be ignored. If it is NULL or an empty array (default) all enable fields are ignored.
751     * @param bool $includeDeleted A flag indicating whether deleted records should be included
752     * @return string
753     * @throws InconsistentQuerySettingsException
754     */
755    protected function getFrontendConstraintStatement($tableName, $ignoreEnableFields, array $enableFieldsToBeIgnored = [], $includeDeleted)
756    {
757        $statement = '';
758        if ($ignoreEnableFields && !$includeDeleted) {
759            if (!empty($enableFieldsToBeIgnored)) {
760                // array_combine() is necessary because of the way \TYPO3\CMS\Frontend\Page\PageRepository::enableFields() is implemented
761                $statement .= $this->getPageRepository()->enableFields($tableName, -1, array_combine($enableFieldsToBeIgnored, $enableFieldsToBeIgnored));
762            } elseif (!empty($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
763                $statement .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
764            }
765        } elseif (!$ignoreEnableFields && !$includeDeleted) {
766            $statement .= $this->getPageRepository()->enableFields($tableName);
767        } elseif (!$ignoreEnableFields && $includeDeleted) {
768            throw new InconsistentQuerySettingsException('Query setting "ignoreEnableFields=FALSE" can not be used together with "includeDeleted=TRUE" in frontend context.', 1460975922);
769        }
770        return $statement;
771    }
772
773    /**
774     * Returns constraint statement for backend context
775     *
776     * @param string $tableName
777     * @param bool $ignoreEnableFields A flag indicating whether the enable fields should be ignored
778     * @param bool $includeDeleted A flag indicating whether deleted records should be included
779     * @return string
780     */
781    protected function getBackendConstraintStatement($tableName, $ignoreEnableFields, $includeDeleted)
782    {
783        $statement = '';
784        if (!$ignoreEnableFields) {
785            $statement .= BackendUtility::BEenableFields($tableName);
786        }
787        if (!$includeDeleted && !empty($GLOBALS['TCA'][$tableName]['ctrl']['delete'])) {
788            $statement .= ' AND ' . $tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['delete'] . '=0';
789        }
790        return $statement;
791    }
792
793    /**
794     * Builds the language field statement in a legacy way (when consistentTranslationOverlayHandling flag is disabled)
795     *
796     * @param string $tableName The database table name
797     * @param string $tableAlias The table alias used in the query.
798     * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
799     * @return string
800     */
801    protected function getSysLanguageStatement($tableName, $tableAlias, $querySettings)
802    {
803        if (is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
804            if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
805                // Select all entries for the current language
806                // If any language is set -> get those entries which are not translated yet
807                // They will be removed by \TYPO3\CMS\Frontend\Page\PageRepository::getRecordOverlay if not matching overlay mode
808                $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
809
810                if (isset($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
811                    && $querySettings->getLanguageUid() > 0
812                ) {
813                    $mode = $querySettings->getLanguageMode();
814
815                    if ($mode === 'strict') {
816                        $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
817                        $queryBuilderForSubselect->getRestrictions()->removeAll()->add(new DeletedRestriction());
818                        $queryBuilderForSubselect
819                            ->select($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
820                            ->from($tableName)
821                            ->where(
822                                $queryBuilderForSubselect->expr()->andX(
823                                    $queryBuilderForSubselect->expr()->gt($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0),
824                                    $queryBuilderForSubselect->expr()->eq($tableName . '.' . $languageField, (int)$querySettings->getLanguageUid())
825                                )
826                            );
827                        return $this->queryBuilder->expr()->orX(
828                            $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, -1),
829                            $this->queryBuilder->expr()->andX(
830                                $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid()),
831                                $this->queryBuilder->expr()->eq($tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0)
832                            ),
833                            $this->queryBuilder->expr()->andX(
834                                $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
835                                $this->queryBuilder->expr()->in(
836                                    $tableAlias . '.uid',
837                                    $queryBuilderForSubselect->getSQL()
838                                )
839                            )
840                        );
841                    }
842                    $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
843                    $queryBuilderForSubselect->getRestrictions()->removeAll()->add(new DeletedRestriction());
844                    $queryBuilderForSubselect
845                            ->select($tableAlias . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'])
846                            ->from($tableName)
847                            ->where(
848                                $queryBuilderForSubselect->expr()->andX(
849                                    $queryBuilderForSubselect->expr()->gt($tableName . '.' . $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'], 0),
850                                    $queryBuilderForSubselect->expr()->eq($tableName . '.' . $languageField, (int)$querySettings->getLanguageUid())
851                                )
852                            );
853                    return $this->queryBuilder->expr()->orX(
854                        $this->queryBuilder->expr()->in($tableAlias . '.' . $languageField, [(int)$querySettings->getLanguageUid(), -1]),
855                        $this->queryBuilder->expr()->andX(
856                            $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
857                            $this->queryBuilder->expr()->notIn(
858                                $tableAlias . '.uid',
859                                $queryBuilderForSubselect->getSQL()
860                            )
861                        )
862                    );
863                }
864                return $this->queryBuilder->expr()->in(
865                    $tableAlias . '.' . $languageField,
866                    [(int)$querySettings->getLanguageUid(), -1]
867                );
868            }
869        }
870        return '';
871    }
872
873    /**
874     * Builds the language field statement
875     *
876     * @param string $tableName The database table name
877     * @param string $tableAlias The table alias used in the query.
878     * @param QuerySettingsInterface $querySettings The TYPO3 CMS specific query settings
879     * @return string
880     */
881    protected function getLanguageStatement($tableName, $tableAlias, QuerySettingsInterface $querySettings)
882    {
883        if (empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
884            return '';
885        }
886
887        // Select all entries for the current language
888        // If any language is set -> get those entries which are not translated yet
889        // They will be removed by \TYPO3\CMS\Frontend\Page\PageRepository::getRecordOverlay if not matching overlay mode
890        $languageField = $GLOBALS['TCA'][$tableName]['ctrl']['languageField'];
891
892        $transOrigPointerField = $GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField'] ?? '';
893        if (!$transOrigPointerField || !$querySettings->getLanguageUid()) {
894            return $this->queryBuilder->expr()->in(
895                $tableAlias . '.' . $languageField,
896                [(int)$querySettings->getLanguageUid(), -1]
897            );
898        }
899
900        $mode = $querySettings->getLanguageOverlayMode();
901        if (!$mode) {
902            return $this->queryBuilder->expr()->in(
903                $tableAlias . '.' . $languageField,
904                [(int)$querySettings->getLanguageUid(), -1]
905            );
906        }
907
908        $defLangTableAlias = $tableAlias . '_dl';
909        $defaultLanguageRecordsSubSelect = $this->queryBuilder->getConnection()->createQueryBuilder();
910        $defaultLanguageRecordsSubSelect
911            ->select($defLangTableAlias . '.uid')
912            ->from($tableName, $defLangTableAlias)
913            ->where(
914                $defaultLanguageRecordsSubSelect->expr()->andX(
915                    $defaultLanguageRecordsSubSelect->expr()->eq($defLangTableAlias . '.' . $transOrigPointerField, 0),
916                    $defaultLanguageRecordsSubSelect->expr()->eq($defLangTableAlias . '.' . $languageField, 0)
917                )
918            );
919
920        $andConditions = [];
921        // records in language 'all'
922        $andConditions[] = $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, -1);
923        // translated records where a default language exists
924        $andConditions[] = $this->queryBuilder->expr()->andX(
925            $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid()),
926            $this->queryBuilder->expr()->in(
927                $tableAlias . '.' . $transOrigPointerField,
928                $defaultLanguageRecordsSubSelect->getSQL()
929            )
930        );
931        if ($mode !== 'hideNonTranslated') {
932            // $mode = TRUE
933            // returns records from current language which have default language
934            // together with not translated default language records
935            $translatedOnlyTableAlias = $tableAlias . '_to';
936            $queryBuilderForSubselect = $this->queryBuilder->getConnection()->createQueryBuilder();
937            $queryBuilderForSubselect
938                ->select($translatedOnlyTableAlias . '.' . $transOrigPointerField)
939                ->from($tableName, $translatedOnlyTableAlias)
940                ->where(
941                    $queryBuilderForSubselect->expr()->andX(
942                        $queryBuilderForSubselect->expr()->gt($translatedOnlyTableAlias . '.' . $transOrigPointerField, 0),
943                        $queryBuilderForSubselect->expr()->eq($translatedOnlyTableAlias . '.' . $languageField, (int)$querySettings->getLanguageUid())
944                    )
945                );
946            // records in default language, which do not have a translation
947            $andConditions[] = $this->queryBuilder->expr()->andX(
948                $this->queryBuilder->expr()->eq($tableAlias . '.' . $languageField, 0),
949                $this->queryBuilder->expr()->notIn(
950                    $tableAlias . '.uid',
951                    $queryBuilderForSubselect->getSQL()
952                )
953            );
954        }
955
956        return $this->queryBuilder->expr()->orX(...$andConditions);
957    }
958
959    /**
960     * Builds the page ID checking statement
961     *
962     * @param string $tableName The database table name
963     * @param string $tableAlias The table alias used in the query.
964     * @param array $storagePageIds list of storage page ids
965     * @return string
966     * @throws InconsistentQuerySettingsException
967     */
968    protected function getPageIdStatement($tableName, $tableAlias, array $storagePageIds)
969    {
970        if (!is_array($GLOBALS['TCA'][$tableName]['ctrl'])) {
971            return '';
972        }
973
974        $rootLevel = (int)$GLOBALS['TCA'][$tableName]['ctrl']['rootLevel'];
975        switch ($rootLevel) {
976            // Only in pid 0
977            case 1:
978                $storagePageIds = [0];
979                break;
980            // Pid 0 and pagetree
981            case -1:
982                if (empty($storagePageIds)) {
983                    $storagePageIds = [0];
984                } else {
985                    $storagePageIds[] = 0;
986                }
987                break;
988            // Only pagetree or not set
989            case 0:
990                if (empty($storagePageIds)) {
991                    throw new InconsistentQuerySettingsException('Missing storage page ids.', 1365779762);
992                }
993                break;
994            // Invalid configuration
995            default:
996                return '';
997        }
998        $storagePageIds = array_map('intval', $storagePageIds);
999        if (count($storagePageIds) === 1) {
1000            return $this->queryBuilder->expr()->eq($tableAlias . '.pid', reset($storagePageIds));
1001        }
1002        return $this->queryBuilder->expr()->in($tableAlias . '.pid', $storagePageIds);
1003    }
1004
1005    /**
1006     * Transforms a Join into SQL and parameter arrays
1007     *
1008     * @param Qom\JoinInterface $join The join
1009     * @param string $leftTableAlias The alias from the table to main
1010     */
1011    protected function parseJoin(Qom\JoinInterface $join, $leftTableAlias)
1012    {
1013        $leftSource = $join->getLeft();
1014        $leftClassName = $leftSource->getNodeTypeName();
1015        $this->addRecordTypeConstraint($leftClassName);
1016        $rightSource = $join->getRight();
1017        if ($rightSource instanceof Qom\JoinInterface) {
1018            $left = $rightSource->getLeft();
1019            $rightClassName = $left->getNodeTypeName();
1020            $rightTableName = $left->getSelectorName();
1021        } else {
1022            $rightClassName = $rightSource->getNodeTypeName();
1023            $rightTableName = $rightSource->getSelectorName();
1024            $this->queryBuilder->addSelect($rightTableName . '.*');
1025        }
1026        $this->addRecordTypeConstraint($rightClassName);
1027        $rightTableAlias = $this->getUniqueAlias($rightTableName);
1028        $joinCondition = $join->getJoinCondition();
1029        $joinConditionExpression = null;
1030        if ($joinCondition instanceof Qom\EquiJoinCondition) {
1031            $column1Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty1Name(), $leftClassName);
1032            $column2Name = $this->dataMapper->convertPropertyNameToColumnName($joinCondition->getProperty2Name(), $rightClassName);
1033
1034            $joinConditionExpression = $this->queryBuilder->expr()->eq(
1035                $leftTableAlias . '.' . $column1Name,
1036                $this->queryBuilder->quoteIdentifier($rightTableAlias . '.' . $column2Name)
1037            );
1038        }
1039        $this->queryBuilder->leftJoin($leftTableAlias, $rightTableName, $rightTableAlias, $joinConditionExpression);
1040        if ($rightSource instanceof Qom\JoinInterface) {
1041            $this->parseJoin($rightSource, $rightTableAlias);
1042        }
1043    }
1044
1045    /**
1046     * Generates a unique alias for the given table and the given property path.
1047     * The property path will be mapped to the generated alias in the tablePropertyMap.
1048     *
1049     * @param string $tableName The name of the table for which the alias should be generated.
1050     * @param string $fullPropertyPath The full property path that is related to the given table.
1051     * @return string The generated table alias.
1052     */
1053    protected function getUniqueAlias($tableName, $fullPropertyPath = null)
1054    {
1055        if (isset($fullPropertyPath) && isset($this->tablePropertyMap[$fullPropertyPath])) {
1056            return $this->tablePropertyMap[$fullPropertyPath];
1057        }
1058
1059        $alias = $tableName;
1060        $i = 0;
1061        while (isset($this->tableAliasMap[$alias])) {
1062            $alias = $tableName . $i;
1063            $i++;
1064        }
1065
1066        $this->tableAliasMap[$alias] = $tableName;
1067
1068        if (isset($fullPropertyPath)) {
1069            $this->tablePropertyMap[$fullPropertyPath] = $alias;
1070        }
1071
1072        return $alias;
1073    }
1074
1075    /**
1076     * adds a union statement to the query, mostly for tables referenced in the where condition.
1077     * The property for which the union statement is generated will be appended.
1078     *
1079     * @param string &$className The name of the parent class, will be set to the child class after processing.
1080     * @param string &$tableName The name of the parent table, will be set to the table alias that is used in the union statement.
1081     * @param string &$propertyPath The remaining property path, will be cut of by one part during the process.
1082     * @param string $fullPropertyPath The full path the the current property, will be used to make table names unique.
1083     * @throws Exception
1084     * @throws InvalidRelationConfigurationException
1085     * @throws MissingColumnMapException
1086     */
1087    protected function addUnionStatement(&$className, &$tableName, &$propertyPath, &$fullPropertyPath)
1088    {
1089        $explodedPropertyPath = explode('.', $propertyPath, 2);
1090        $propertyName = $explodedPropertyPath[0];
1091        $columnName = $this->dataMapper->convertPropertyNameToColumnName($propertyName, $className);
1092        $realTableName = $this->dataMapper->convertClassNameToTableName($className);
1093        $tableName = $this->tablePropertyMap[$fullPropertyPath] ?? $realTableName;
1094        $columnMap = $this->dataMapper->getDataMap($className)->getColumnMap($propertyName);
1095
1096        if ($columnMap === null) {
1097            throw new MissingColumnMapException('The ColumnMap for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1355142232);
1098        }
1099
1100        $parentKeyFieldName = $columnMap->getParentKeyFieldName();
1101        $childTableName = $columnMap->getChildTableName();
1102
1103        if ($childTableName === null) {
1104            throw new InvalidRelationConfigurationException('The relation information for property "' . $propertyName . '" of class "' . $className . '" is missing.', 1353170925);
1105        }
1106
1107        $fullPropertyPath .= ($fullPropertyPath === '') ? $propertyName : '.' . $propertyName;
1108        $childTableAlias = $this->getUniqueAlias($childTableName, $fullPropertyPath);
1109
1110        // If there is already a union with the current identifier we do not need to build it again and exit early.
1111        if (in_array($childTableAlias, $this->unionTableAliasCache, true)) {
1112            $propertyPath = $explodedPropertyPath[1];
1113            $tableName = $childTableAlias;
1114            $className = $this->dataMapper->getType($className, $propertyName);
1115            return;
1116        }
1117
1118        if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_ONE) {
1119            if (isset($parentKeyFieldName)) {
1120                // @todo: no test for this part yet
1121                $joinConditionExpression = $this->queryBuilder->expr()->eq(
1122                    $tableName . '.uid',
1123                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.' . $parentKeyFieldName)
1124                );
1125            } else {
1126                $joinConditionExpression = $this->queryBuilder->expr()->eq(
1127                    $tableName . '.' . $columnName,
1128                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid')
1129                );
1130            }
1131            $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
1132            $this->unionTableAliasCache[] = $childTableAlias;
1133            $this->queryBuilder->andWhere(
1134                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
1135            );
1136        } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
1137            // @todo: no tests for this part yet
1138            if (isset($parentKeyFieldName)) {
1139                $joinConditionExpression = $this->queryBuilder->expr()->eq(
1140                    $tableName . '.uid',
1141                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.' . $parentKeyFieldName)
1142                );
1143            } else {
1144                $joinConditionExpression = $this->queryBuilder->expr()->inSet(
1145                    $tableName . '.' . $columnName,
1146                    $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid'),
1147                    true
1148                );
1149            }
1150            $this->queryBuilder->leftJoin($tableName, $childTableName, $childTableAlias, $joinConditionExpression);
1151            $this->unionTableAliasCache[] = $childTableAlias;
1152            $this->queryBuilder->andWhere(
1153                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $childTableAlias, $realTableName)
1154            );
1155            $this->suggestDistinctQuery = true;
1156        } elseif ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_AND_BELONGS_TO_MANY) {
1157            $relationTableName = $columnMap->getRelationTableName();
1158            $relationTableAlias = $this->getUniqueAlias($relationTableName, $fullPropertyPath . '_mm');
1159
1160            $joinConditionExpression = $this->queryBuilder->expr()->andX(
1161                $this->queryBuilder->expr()->eq(
1162                    $tableName . '.uid',
1163                    $this->queryBuilder->quoteIdentifier(
1164                        $relationTableAlias . '.' . $columnMap->getParentKeyFieldName()
1165                    )
1166                ),
1167                $this->getAdditionalMatchFieldsStatement($this->queryBuilder->expr(), $columnMap, $relationTableAlias, $realTableName)
1168            );
1169            $this->queryBuilder->leftJoin($tableName, $relationTableName, $relationTableAlias, $joinConditionExpression);
1170            $joinConditionExpression = $this->queryBuilder->expr()->eq(
1171                $relationTableAlias . '.' . $columnMap->getChildKeyFieldName(),
1172                $this->queryBuilder->quoteIdentifier($childTableAlias . '.uid')
1173            );
1174            $this->queryBuilder->leftJoin($relationTableAlias, $childTableName, $childTableAlias, $joinConditionExpression);
1175            $this->unionTableAliasCache[] = $childTableAlias;
1176            $this->suggestDistinctQuery = true;
1177        } else {
1178            throw new Exception('Could not determine type of relation.', 1252502725);
1179        }
1180        $propertyPath = $explodedPropertyPath[1];
1181        $tableName = $childTableAlias;
1182        $className = $this->dataMapper->getType($className, $propertyName);
1183    }
1184
1185    /**
1186     * If the table name does not match the table alias all occurrences of
1187     * "tableName." are replaced with "tableAlias." in the given SQL statement.
1188     *
1189     * @param string $statement The SQL statement in which the values are replaced.
1190     * @param string $tableName The table name that is replaced.
1191     * @param string $tableAlias The table alias that replaced the table name.
1192     * @return string The modified SQL statement.
1193     */
1194    protected function replaceTableNameWithAlias($statement, $tableName, $tableAlias)
1195    {
1196        if ($tableAlias !== $tableName) {
1197            /** @var Connection $connection */
1198            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName);
1199            $quotedTableName = $connection->quoteIdentifier($tableName);
1200            $quotedTableAlias = $connection->quoteIdentifier($tableAlias);
1201            $statement = str_replace(
1202                [$tableName . '.', $quotedTableName . '.'],
1203                [$tableAlias . '.', $quotedTableAlias . '.'],
1204                $statement
1205            );
1206        }
1207
1208        return $statement;
1209    }
1210
1211    /**
1212     * @return PageRepository
1213     */
1214    protected function getPageRepository()
1215    {
1216        if (!$this->pageRepository instanceof PageRepository) {
1217            $this->pageRepository = GeneralUtility::makeInstance(PageRepository::class);
1218        }
1219        return $this->pageRepository;
1220    }
1221}
1222