Skip to content

Commit 09e5c08

Browse files
committed
do not ignore parameters from query builder union parts
1 parent 69d5e34 commit 09e5c08

File tree

3 files changed

+193
-7
lines changed

3 files changed

+193
-7
lines changed

docs/en/reference/query-builder.rst

+9-2
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ or QueryBuilder instances to one of the following methods:
324324
* ``union(string|QueryBuilder $part)``
325325
* ``addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT)``
326326

327+
If you pass a QueryBuilder instance, you can set parameters on it.
328+
But you cannot use same parameter names in different QueryBuilder instances.
329+
327330
.. code-block:: php
328331
329332
<?php
@@ -358,10 +361,14 @@ or QueryBuilder instances to one of the following methods:
358361
359362
$subQueryBuilder1
360363
->select('id AS field')
361-
->from('a_table');
364+
->from('a_table')
365+
->where('id > :minId')
366+
->setParameter('minId', 12);
362367
$subQueryBuilder2
363368
->select('id AS field')
364-
->from('a_table');
369+
->from('a_table')
370+
->where('id < :maxId')
371+
->setParameter('maxId', 133);
365372
$queryBuilder
366373
->union($subQueryBuilder1)
367374
->addUnion($subQueryBuilder2,UnionType::ALL)

src/Query/QueryBuilder.php

+68-2
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818
use Doctrine\DBAL\Statement;
1919
use Doctrine\DBAL\Types\Type;
2020

21+
use function array_filter;
22+
use function array_intersect;
2123
use function array_key_exists;
2224
use function array_keys;
2325
use function array_merge;
2426
use function array_unshift;
2527
use function count;
2628
use function implode;
2729
use function is_object;
30+
use function sprintf;
2831
use function substr;
2932

3033
/**
@@ -302,14 +305,77 @@ public function fetchFirstColumn(): array
302305
*/
303306
public function executeQuery(): Result
304307
{
308+
[$params, $types] = $this->buildParametersAndTypes();
309+
305310
return $this->connection->executeQuery(
306311
$this->getSQL(),
307-
$this->params,
308-
$this->types,
312+
$params,
313+
$types,
309314
$this->resultCacheProfile,
310315
);
311316
}
312317

318+
/**
319+
* Build then return parameters and types for the query.
320+
*
321+
* @return array{
322+
* list<mixed>|array<string, mixed>,
323+
* WrapperParameterTypeArray,
324+
* } The parameters and types for the query.
325+
*/
326+
private function buildParametersAndTypes(): array
327+
{
328+
$partParams = $partParamTypes = [];
329+
330+
foreach ($this->unionParts as $part) {
331+
if (! $part->query instanceof self || count($part->query->params) === 0) {
332+
continue;
333+
}
334+
335+
$this->guardDuplicatedParameterNames($partParams, $part->query->params);
336+
337+
$partParams = array_merge($partParams, $part->query->params);
338+
$partParamTypes = array_merge($partParamTypes, $part->query->types);
339+
}
340+
341+
if (count($partParams) === 0) {
342+
return [$this->params, $this->types];
343+
}
344+
345+
$this->guardDuplicatedParameterNames($partParams, $this->params);
346+
347+
return [
348+
array_merge($partParams, $this->params),
349+
array_merge($partParamTypes, $this->types),
350+
];
351+
}
352+
353+
/**
354+
* Guards against duplicated parameter names.
355+
*
356+
* @param list<mixed>|array<string, mixed> $params
357+
* @param list<mixed>|array<string, mixed> $paramsToMerge
358+
*
359+
* @throws QueryException
360+
*/
361+
private function guardDuplicatedParameterNames(array $params, array $paramsToMerge): void
362+
{
363+
if (count($params) === 0 || count($paramsToMerge) === 0) {
364+
return;
365+
}
366+
367+
$paramsKeys = array_filter(array_keys($params), 'is_string');
368+
$paramsToMergeKeys = array_filter(array_keys($paramsToMerge), 'is_string');
369+
370+
$duplicates = array_intersect($paramsKeys, $paramsToMergeKeys);
371+
if (count($duplicates) > 0) {
372+
throw new QueryException(sprintf(
373+
'Found duplicated parameter in query. The duplicated parameter names are: "%s".',
374+
implode(', ', $duplicates),
375+
));
376+
}
377+
}
378+
313379
/**
314380
* Executes an SQL statement and returns the number of affected rows.
315381
*

tests/Functional/Query/QueryBuilderTest.php

+116-3
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,6 @@ public function testUnionWithLimitAndOffsetClauseReturnsExpectedResult(): void
212212
{
213213
$expectedRows = $this->prepareExpectedRows([['field_one' => 2]]);
214214
$platform = $this->connection->getDatabasePlatform();
215-
$plainSelect1 = $platform->getDummySelectSQL('1 as field_one');
216-
$plainSelect2 = $platform->getDummySelectSQL('2 as field_one');
217-
$plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
218215
$qb = $this->connection->createQueryBuilder();
219216
$qb->union($platform->getDummySelectSQL('1 as field_one'))
220217
->addUnion($platform->getDummySelectSQL('2 as field_one'), UnionType::DISTINCT)
@@ -332,6 +329,122 @@ public function testUnionAndAddUnionWorksWithQueryBuilderPartsAndReturnsExpected
332329
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
333330
}
334331

332+
public function testUnionAndAddUnionWorksWithBindingNamedParametersToQueryBuilderParts(): void
333+
{
334+
$expectedRows = $this->prepareExpectedRows([['id' => 2], ['id' => 1], ['id' => 1]]);
335+
$qb = $this->connection->createQueryBuilder();
336+
337+
$subQueryBuilder1 = $this->connection->createQueryBuilder();
338+
$subQueryBuilder1->select('id')
339+
->from('for_update')
340+
->where('id = :id1')
341+
->setParameter('id1', 1, ParameterType::INTEGER);
342+
343+
$subQueryBuilder2 = $this->connection->createQueryBuilder();
344+
$subQueryBuilder2->select('id')
345+
->from('for_update')
346+
->where('id = :id2')
347+
->setParameter('id2', 2, ParameterType::INTEGER);
348+
349+
$subQueryBuilder3 = $this->connection->createQueryBuilder();
350+
$subQueryBuilder3->select('id')
351+
->from('for_update')
352+
->where('id = :id3')
353+
->setParameter('id3', 1, ParameterType::INTEGER);
354+
355+
$qb->union($subQueryBuilder1)
356+
->addUnion($subQueryBuilder2)
357+
->addUnion($subQueryBuilder3, UnionType::ALL)
358+
->orderBy('id', 'DESC');
359+
360+
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
361+
}
362+
363+
public function testUnionAndAddUnionWorksWithBindingPositionalParametersToQueryBuilderParts(): void
364+
{
365+
$expectedRows = $this->prepareExpectedRows([['id' => 1], ['id' => 1], ['id' => 2]]);
366+
$qb = $this->connection->createQueryBuilder();
367+
368+
$subQueryBuilder1 = $this->connection->createQueryBuilder();
369+
$subQueryBuilder1->select('id')
370+
->from('for_update')
371+
->where('id = ?')
372+
->setParameter(0, 1, ParameterType::INTEGER);
373+
374+
$subQueryBuilder2 = $this->connection->createQueryBuilder();
375+
$subQueryBuilder2->select('id')
376+
->from('for_update')
377+
->where($subQueryBuilder2->expr()->eq(
378+
'id',
379+
$subQueryBuilder2->createPositionalParameter(2, ParameterType::INTEGER),
380+
));
381+
382+
$subQueryBuilder3 = $this->connection->createQueryBuilder();
383+
$subQueryBuilder3->select('id')
384+
->from('for_update')
385+
->where('id = ?')
386+
->setParameter(0, 1, ParameterType::INTEGER);
387+
388+
$qb->union($subQueryBuilder1)
389+
->addUnion($subQueryBuilder2)
390+
->addUnion($subQueryBuilder3, UnionType::ALL)
391+
->orderBy('id', 'ASC');
392+
393+
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
394+
}
395+
396+
public function testUnionAndAddUnionThrowsExceptionWithDuplicatedParametersNames(): void
397+
{
398+
$qb = $this->connection->createQueryBuilder();
399+
400+
$subQueryBuilder1 = $this->connection->createQueryBuilder();
401+
$subQueryBuilder1->select('id')
402+
->from('for_update')
403+
->where('id = :id')
404+
->setParameter('id', 1, ParameterType::INTEGER);
405+
406+
$subQueryBuilder2 = $this->connection->createQueryBuilder();
407+
$subQueryBuilder2->select('id')
408+
->from('for_update')
409+
->where('id = :id')
410+
->setParameter('id', 2, ParameterType::INTEGER);
411+
412+
$qb->union($subQueryBuilder1)
413+
->addUnion($subQueryBuilder2);
414+
415+
self::expectExceptionMessage('Found duplicated parameter in query. The duplicated parameter names are: "id".');
416+
$qb->executeQuery();
417+
}
418+
419+
public function testUnionAndAddUnionThrowsExceptionWithDuplicatedCreatedParametersNames(): void
420+
{
421+
$qb = $this->connection->createQueryBuilder();
422+
423+
$subQueryBuilder1 = $this->connection->createQueryBuilder();
424+
$subQueryBuilder1->select('id')
425+
->from('for_update')
426+
->where($subQueryBuilder1->expr()->eq(
427+
'id',
428+
$subQueryBuilder1->createNamedParameter(1, ParameterType::INTEGER),
429+
));
430+
431+
$subQueryBuilder2 = $this->connection->createQueryBuilder();
432+
$subQueryBuilder2->select('id')
433+
->from('for_update')
434+
->where($subQueryBuilder2->expr()->eq(
435+
'id',
436+
$subQueryBuilder2->createNamedParameter(2, ParameterType::INTEGER),
437+
));
438+
439+
$qb->union($subQueryBuilder1)
440+
->addUnion($subQueryBuilder2);
441+
442+
self::expectExceptionMessage(
443+
'Found duplicated parameter in query. The duplicated parameter names are: "dcValue1".',
444+
);
445+
$qb->executeQuery();
446+
}
447+
335448
/**
336449
* @param array<array<string, int>> $rows
337450
*

0 commit comments

Comments
 (0)