Skip to content

Commit d9fbaf2

Browse files
authored
IBX-10219: Fixed handling multiple relationship comparison (#31)
1 parent d4b39e7 commit d9fbaf2

15 files changed

+762
-56
lines changed

src/contracts/Gateway/AbstractDoctrineDatabase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Doctrine\DBAL\Connection;
1313
use Doctrine\DBAL\Query\QueryBuilder;
1414
use Ibexa\CorePersistence\Gateway\ExpressionVisitor;
15+
use Ibexa\CorePersistence\Gateway\RelationshipTypeStrategyRegistry;
1516
use InvalidArgumentException;
1617

1718
/**
@@ -308,6 +309,7 @@ protected function buildExpressionVisitor(QueryBuilder $qb): ExpressionVisitor
308309
$this->registry,
309310
$this->getTableName(),
310311
$this->getTableAlias(),
312+
new RelationshipTypeStrategyRegistry()
311313
);
312314
}
313315

src/lib/Gateway/ExpressionVisitor.php

Lines changed: 108 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
1717
use Doctrine\DBAL\Query\QueryBuilder;
1818
use Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingException;
19-
use Ibexa\Contracts\CorePersistence\Gateway\DoctrineOneToManyRelationship;
2019
use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationship;
2120
use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationshipInterface;
2221
use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataInterface;
2322
use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataRegistryInterface;
23+
use LogicException;
2424
use RuntimeException;
2525

2626
/**
@@ -48,19 +48,23 @@ final class ExpressionVisitor extends BaseExpressionVisitor
4848

4949
private DoctrineSchemaMetadataRegistryInterface $registry;
5050

51+
private RelationshipTypeStrategyRegistryInterface $relationshipTypeStrategyRegistry;
52+
5153
/**
5254
* @param non-empty-string $tableName
5355
*/
5456
public function __construct(
5557
QueryBuilder $queryBuilder,
5658
DoctrineSchemaMetadataRegistryInterface $registry,
5759
string $tableName,
58-
string $tableAlias
60+
string $tableAlias,
61+
RelationshipTypeStrategyRegistryInterface $relationshipTypeStrategyRegistry
5962
) {
6063
$this->queryBuilder = $queryBuilder;
6164
$this->schemaMetadata = $registry->getMetadataForTable($tableName);
6265
$this->tableAlias = $tableAlias;
6366
$this->registry = $registry;
67+
$this->relationshipTypeStrategyRegistry = $relationshipTypeStrategyRegistry;
6468
}
6569

6670
/**
@@ -102,7 +106,7 @@ public function walkComparison(Comparison $comparison)
102106
}
103107

104108
$parameterName = $column . '_' . count($this->parameters);
105-
$placeholder = ':' . $parameterName;
109+
$placeholder = $this->getPlaceholder($parameterName);
106110
$value = $this->walkValue($comparison->getValue());
107111
$type = $this->schemaMetadata->getBindingTypeForColumn($column);
108112
if (is_array($value)) {
@@ -162,6 +166,10 @@ private function containsRelationshipDelimiter(string $column): bool
162166
private function handleRelationshipComparison(string $column, Comparison $comparison): string
163167
{
164168
$metadata = $this->schemaMetadata;
169+
$fromTable = $this->tableAlias;
170+
$toTable = null;
171+
$subQuery = null;
172+
165173
do {
166174
[
167175
$foreignProperty,
@@ -170,6 +178,26 @@ private function handleRelationshipComparison(string $column, Comparison $compar
170178

171179
$relationship = $metadata->getRelationshipByForeignProperty($foreignProperty);
172180
$metadata = $this->registry->getMetadata($relationship->getRelationshipClass());
181+
$parentMetadata = $metadata->getParentMetadata();
182+
if (null !== $parentMetadata) {
183+
$fromTable = $parentMetadata->getTableName();
184+
} else {
185+
$fromTable = $toTable ?? $fromTable;
186+
}
187+
188+
$toTable = $metadata->getTableName();
189+
190+
if (DoctrineRelationship::JOIN_TYPE_SUB_SELECT === $relationship->getJoinType()) {
191+
$subQuery ??= $this->queryBuilder->getConnection()->createQueryBuilder();
192+
}
193+
194+
$this->relationshipTypeStrategyRegistry->handleRelationshipType(
195+
$subQuery ?? $this->queryBuilder,
196+
$relationship,
197+
$this->tableAlias,
198+
$fromTable,
199+
$toTable
200+
);
173201
} while ($this->containsRelationshipDelimiter($column));
174202

175203
if (!$metadata->hasColumn($column)) {
@@ -186,32 +214,7 @@ private function handleRelationshipComparison(string $column, Comparison $compar
186214
));
187215
}
188216

189-
$relationshipType = get_class($relationship);
190-
191-
switch (true) {
192-
case $relationship->getJoinType() === DoctrineRelationship::JOIN_TYPE_SUB_SELECT:
193-
return $this->handleSubSelectQuery(
194-
$metadata,
195-
$relationship->getForeignKeyColumn(),
196-
$column,
197-
$comparison,
198-
);
199-
case $relationship->getJoinType() === DoctrineRelationship::JOIN_TYPE_JOINED:
200-
return $this->handleJoinQuery(
201-
$metadata,
202-
$column,
203-
$comparison,
204-
);
205-
default:
206-
throw new RuntimeMappingException(sprintf(
207-
'Unhandled relationship metadata. Expected one of "%s". Received "%s".',
208-
implode('", "', [
209-
DoctrineRelationship::class,
210-
DoctrineOneToManyRelationship::class,
211-
]),
212-
$relationshipType,
213-
));
214-
}
217+
return $this->handleRelationshipQuery($metadata, $relationship, $column, $comparison, $subQuery);
215218
}
216219

217220
private function escapeSpecialSQLValues(Parameter $parameter): string
@@ -226,63 +229,106 @@ private function expr(): ExpressionBuilder
226229
return $this->queryBuilder->expr();
227230
}
228231

232+
private function handleRelationshipQuery(
233+
DoctrineSchemaMetadataInterface $metadata,
234+
DoctrineRelationshipInterface $relationship,
235+
string $column,
236+
Comparison $comparison,
237+
?QueryBuilder $subQuery
238+
): string {
239+
$isSubSelectJoin = $relationship->getJoinType() === DoctrineRelationship::JOIN_TYPE_SUB_SELECT;
240+
if ($isSubSelectJoin) {
241+
if ($subQuery === null) {
242+
throw new LogicException(
243+
'Sub-select query is not initialized.',
244+
);
245+
}
246+
}
247+
248+
$parameterName = $column . '_' . count($this->parameters);
249+
$fullColumnName = $metadata->getTableName() . '.' . $column;
250+
$relationshipQuery = $this->relationshipTypeStrategyRegistry->handleRelationshipTypeQuery(
251+
$relationship,
252+
$subQuery ?? $this->queryBuilder,
253+
$fullColumnName,
254+
$this->getPlaceholder($parameterName)
255+
);
256+
257+
if ($isSubSelectJoin) {
258+
return $this->handleSubSelectQuery(
259+
$relationship,
260+
$metadata,
261+
$comparison,
262+
$column,
263+
$parameterName,
264+
$relationshipQuery
265+
);
266+
}
267+
268+
return $this->handleJoinQuery(
269+
$comparison,
270+
$metadata,
271+
$column,
272+
$parameterName,
273+
$fullColumnName,
274+
$relationshipQuery
275+
);
276+
}
277+
229278
private function handleJoinQuery(
279+
Comparison $comparison,
230280
DoctrineSchemaMetadataInterface $relationshipMetadata,
231281
string $field,
232-
Comparison $comparison
282+
string $parameterName,
283+
string $fullColumnName,
284+
QueryBuilder $relationshipQuery
233285
): string {
234-
$tableName = $relationshipMetadata->getTableName();
235-
$parameterName = $field . '_' . count($this->parameters);
236-
$placeholder = ':' . $parameterName;
237-
238286
$value = $this->walkValue($comparison->getValue());
239287
$type = $relationshipMetadata->getBindingTypeForColumn($field);
288+
240289
if (is_array($value)) {
241290
$type += Connection::ARRAY_PARAM_OFFSET;
242291
}
243292

244293
$parameter = new Parameter($parameterName, $value, $type);
294+
$placeholder = $this->getPlaceholder($parameterName);
245295

246296
if (is_array($value)) {
247297
$this->parameters[] = $parameter;
248298

249-
return $this->expr()->in($tableName . '.' . $field, $placeholder);
299+
return $relationshipQuery->expr()->in(
300+
$fullColumnName,
301+
$placeholder
302+
);
250303
}
251304

252-
return $this->handleComparison($comparison, $parameter, $tableName . '.' . $field, $placeholder);
305+
return $this->handleComparison(
306+
$comparison,
307+
$parameter,
308+
$fullColumnName,
309+
$placeholder
310+
);
253311
}
254312

255313
private function handleSubSelectQuery(
314+
DoctrineRelationshipInterface $relationship,
256315
DoctrineSchemaMetadataInterface $relationshipMetadata,
257-
string $foreignField,
316+
Comparison $comparison,
258317
string $field,
259-
Comparison $comparison
318+
string $parameterName,
319+
QueryBuilder $relationshipQuery
260320
): string {
261-
$tableName = $relationshipMetadata->getTableName();
262-
$parameterName = $field . '_' . count($this->parameters);
263-
$placeholder = ':' . $parameterName;
264-
265-
$subquery = $this->queryBuilder->getConnection()->createQueryBuilder();
266-
$subquery->from($tableName);
267-
$subquery->select($tableName . '.' . $relationshipMetadata->getIdentifierColumn());
268-
$subquery->where(
269-
$subquery->expr()->in(
270-
$tableName . '.' . $field,
271-
$placeholder,
272-
),
273-
);
274-
275321
$value = $this->walkValue($comparison->getValue());
276322
$type = $relationshipMetadata->getBindingTypeForColumn($field);
277323
if (is_array($value)) {
278324
$type += Connection::ARRAY_PARAM_OFFSET;
279325
}
280-
$parameter = new Parameter($parameterName, $value, $type);
281-
$this->parameters[] = $parameter;
326+
327+
$this->parameters[] = new Parameter($parameterName, $value, $type);
282328

283329
return $this->expr()->in(
284-
$this->tableAlias . '.' . $foreignField,
285-
$subquery->getSQL(),
330+
$this->tableAlias . '.' . $relationship->getForeignKeyColumn(),
331+
$relationshipQuery->getSQL(),
286332
);
287333
}
288334

@@ -304,6 +350,7 @@ private function handleTranslation(Comparison $comparison): string
304350
$this->registry,
305351
$translationMetadata->getTableName(),
306352
self::TRANSLATION_TABLE_ALIAS,
353+
$this->relationshipTypeStrategyRegistry
307354
);
308355
$innerCondition = $innerExpressionVisitor->walkComparison($comparison);
309356
$subquery->andWhere($innerCondition);
@@ -317,6 +364,11 @@ private function handleTranslation(Comparison $comparison): string
317364
);
318365
}
319366

367+
private function getPlaceholder(string $parameterName): string
368+
{
369+
return ':' . $parameterName;
370+
}
371+
320372
private function isInheritedColumn(string $column): bool
321373
{
322374
return $this->schemaMetadata->getIdentifierColumn() !== $column
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\CorePersistence\Gateway;
10+
11+
use Doctrine\DBAL\Query\QueryBuilder;
12+
use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationshipInterface;
13+
14+
/**
15+
* @internal
16+
*/
17+
final class JoinedRelationshipTypeStrategy implements RelationshipTypeStrategyInterface
18+
{
19+
public function handleRelationshipType(
20+
QueryBuilder $queryBuilder,
21+
DoctrineRelationshipInterface $relationship,
22+
string $rootTableAlias,
23+
string $fromTable,
24+
string $toTable
25+
): void {
26+
if ($this->isTableAlreadyJoined($queryBuilder, $toTable)) {
27+
return;
28+
}
29+
30+
$queryBuilder->leftJoin(
31+
$fromTable,
32+
$toTable,
33+
$toTable,
34+
$queryBuilder->expr()->eq(
35+
$fromTable . '.' . $relationship->getForeignKeyColumn(),
36+
$toTable . '.' . $relationship->getRelatedClassIdColumn()
37+
)
38+
);
39+
}
40+
41+
public function handleRelationshipTypeQuery(
42+
QueryBuilder $queryBuilder,
43+
string $fullColumnName,
44+
string $placeholder
45+
): QueryBuilder {
46+
return $queryBuilder;
47+
}
48+
49+
private function isTableAlreadyJoined(
50+
QueryBuilder $queryBuilder,
51+
string $tableToJoin
52+
): bool {
53+
$joinQueryPart = $queryBuilder->getQueryPart('join');
54+
55+
foreach ($joinQueryPart as $joins) {
56+
foreach ($joins as $join) {
57+
$joinAlias = $join['joinAlias'] ?? $join['joinTable'];
58+
59+
if ($joinAlias === $tableToJoin) {
60+
return true;
61+
}
62+
}
63+
}
64+
65+
return false;
66+
}
67+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/**
4+
* @copyright Copyright (C) Ibexa AS. All rights reserved.
5+
* @license For full copyright and license information view LICENSE file distributed with this source code.
6+
*/
7+
declare(strict_types=1);
8+
9+
namespace Ibexa\CorePersistence\Gateway;
10+
11+
use Doctrine\DBAL\Query\QueryBuilder;
12+
use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationshipInterface;
13+
14+
/**
15+
* @internal
16+
*/
17+
interface RelationshipTypeStrategyInterface
18+
{
19+
public function handleRelationshipType(
20+
QueryBuilder $queryBuilder,
21+
DoctrineRelationshipInterface $relationship,
22+
string $rootTableAlias,
23+
string $fromTable,
24+
string $toTable
25+
): void;
26+
27+
public function handleRelationshipTypeQuery(
28+
QueryBuilder $queryBuilder,
29+
string $fullColumnName,
30+
string $placeholder
31+
): QueryBuilder;
32+
}

0 commit comments

Comments
 (0)