Skip to content

Commit 639db83

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

File tree

3 files changed

+191
-7
lines changed

3 files changed

+191
-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

+114-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,120 @@ public function testUnionAndAddUnionWorksWithQueryBuilderPartsAndReturnsExpected
332329
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
333330
}
334331

332+
public function testUnionAndAddUnionWorksWithBindingNamedParametersToQueryBuilderParts(): void
333+
{
334+
$expectedRows = $this->prepareExpectedRows([['id' => 1], ['id' => 2], ['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+
359+
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
360+
}
361+
362+
public function testUnionAndAddUnionWorksWithBindingPositionalParametersToQueryBuilderParts(): void
363+
{
364+
$expectedRows = $this->prepareExpectedRows([['id' => 1], ['id' => 2], ['id' => 1]]);
365+
$qb = $this->connection->createQueryBuilder();
366+
367+
$subQueryBuilder1 = $this->connection->createQueryBuilder();
368+
$subQueryBuilder1->select('id')
369+
->from('for_update')
370+
->where('id = ?')
371+
->setParameter(0, 1, ParameterType::INTEGER);
372+
373+
$subQueryBuilder2 = $this->connection->createQueryBuilder();
374+
$subQueryBuilder2->select('id')
375+
->from('for_update')
376+
->where($subQueryBuilder2->expr()->eq(
377+
'id',
378+
$subQueryBuilder2->createPositionalParameter(2, ParameterType::INTEGER),
379+
));
380+
381+
$subQueryBuilder3 = $this->connection->createQueryBuilder();
382+
$subQueryBuilder3->select('id')
383+
->from('for_update')
384+
->where('id = ?')
385+
->setParameter(0, 1, ParameterType::INTEGER);
386+
387+
$qb->union($subQueryBuilder1)
388+
->addUnion($subQueryBuilder2)
389+
->addUnion($subQueryBuilder3, UnionType::ALL);
390+
391+
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
392+
}
393+
394+
public function testUnionAndAddUnionThrowsExceptionWithDuplicatedParametersNames(): void
395+
{
396+
$qb = $this->connection->createQueryBuilder();
397+
398+
$subQueryBuilder1 = $this->connection->createQueryBuilder();
399+
$subQueryBuilder1->select('id')
400+
->from('for_update')
401+
->where('id = :id')
402+
->setParameter('id', 1, ParameterType::INTEGER);
403+
404+
$subQueryBuilder2 = $this->connection->createQueryBuilder();
405+
$subQueryBuilder2->select('id')
406+
->from('for_update')
407+
->where('id = :id')
408+
->setParameter('id', 2, ParameterType::INTEGER);
409+
410+
$qb->union($subQueryBuilder1)
411+
->addUnion($subQueryBuilder2);
412+
413+
self::expectExceptionMessage('Found duplicated parameter in query. The duplicated parameter names are: "id".');
414+
$qb->executeQuery();
415+
}
416+
417+
public function testUnionAndAddUnionThrowsExceptionWithDuplicatedCreatedParametersNames(): void
418+
{
419+
$qb = $this->connection->createQueryBuilder();
420+
421+
$subQueryBuilder1 = $this->connection->createQueryBuilder();
422+
$subQueryBuilder1->select('id')
423+
->from('for_update')
424+
->where($subQueryBuilder1->expr()->eq(
425+
'id',
426+
$subQueryBuilder1->createNamedParameter(1, ParameterType::INTEGER),
427+
));
428+
429+
$subQueryBuilder2 = $this->connection->createQueryBuilder();
430+
$subQueryBuilder2->select('id')
431+
->from('for_update')
432+
->where($subQueryBuilder2->expr()->eq(
433+
'id',
434+
$subQueryBuilder2->createNamedParameter(2, ParameterType::INTEGER),
435+
));
436+
437+
$qb->union($subQueryBuilder1)
438+
->addUnion($subQueryBuilder2);
439+
440+
self::expectExceptionMessage(
441+
'Found duplicated parameter in query. The duplicated parameter names are: "dcValue1".',
442+
);
443+
$qb->executeQuery();
444+
}
445+
335446
/**
336447
* @param array<array<string, int>> $rows
337448
*

0 commit comments

Comments
 (0)