Skip to content

Commit

Permalink
query builders aggregator
Browse files Browse the repository at this point in the history
  • Loading branch information
nio-dtp committed Jan 2, 2025
1 parent 69d5e34 commit 0ef19b9
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 17 deletions.
10 changes: 10 additions & 0 deletions src/Connection.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Query\QueryBuildersAggregator;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
use Doctrine\DBAL\Schema\SchemaManagerFactory;
Expand Down Expand Up @@ -104,6 +105,8 @@ class Connection implements ServerVersionProvider

private SchemaManagerFactory $schemaManagerFactory;

private ?QueryBuildersAggregator $queryBuildersAggregator = null;

/**
* Initializes a new instance of the Connection class.
*
Expand All @@ -126,6 +129,13 @@ public function __construct(

$this->schemaManagerFactory = $this->_config->getSchemaManagerFactory()
?? new DefaultSchemaManagerFactory();

$this->queryBuildersAggregator = QueryBuildersAggregator::create();
}

public function getQueryBuildersAggregator(): QueryBuildersAggregator
{
return $this->queryBuildersAggregator;
}

/**
Expand Down
28 changes: 14 additions & 14 deletions src/Query/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,6 @@ class QueryBuilder
*/
private ?int $maxResults = null;

/**
* The counter of bound parameters used with {@see bindValue).
*
* @var int<0, max>
*/
private int $boundCounter = 0;

/**
* The SELECT parts of the query.
*
Expand Down Expand Up @@ -172,6 +165,7 @@ class QueryBuilder
*/
public function __construct(private readonly Connection $connection)
{
$this->connection->getQueryBuildersAggregator()->register($this);
}

/**
Expand Down Expand Up @@ -302,12 +296,18 @@ public function fetchFirstColumn(): array
*/
public function executeQuery(): Result
{
return $this->connection->executeQuery(
$queryBuildersAggregate = $this->connection->getQueryBuildersAggregator();
[$params, $types] = $queryBuildersAggregate->buildParametersAndTypes();

$result = $this->connection->executeQuery(
$this->getSQL(),
$this->params,
$this->types,
$params,
$types,
$this->resultCacheProfile,
);
$queryBuildersAggregate->toReset();

return $result;
}

/**
Expand Down Expand Up @@ -1430,8 +1430,8 @@ public function createNamedParameter(
?string $placeHolder = null,
): string {
if ($placeHolder === null) {
$this->boundCounter++;
$placeHolder = ':dcValue' . $this->boundCounter;
$this->connection->getQueryBuildersAggregator()->incrementBoundCounter();
$placeHolder = ':dcValue' . $this->connection->getQueryBuildersAggregator()->getBoundCounter();
}

$this->setParameter(substr($placeHolder, 1), $value, $type);
Expand Down Expand Up @@ -1460,8 +1460,8 @@ public function createPositionalParameter(
mixed $value,
string|ParameterType|Type|ArrayParameterType $type = ParameterType::STRING,
): string {
$this->setParameter($this->boundCounter, $value, $type);
$this->boundCounter++;
$this->setParameter($this->connection->getQueryBuildersAggregator()->getBoundCounter(), $value, $type);
$this->connection->getQueryBuildersAggregator()->incrementBoundCounter();

return '?';
}
Expand Down
120 changes: 120 additions & 0 deletions src/Query/QueryBuildersAggregator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Query;

use function array_filter;
use function array_intersect;
use function array_keys;
use function array_merge;
use function count;
use function implode;
use function sprintf;

class QueryBuildersAggregator
{
private static self|null $instance = null;

/**
* Set to true each time a query is executed,
* resetting the collection when a new QueryBuilder is created.
*/
private bool $toReset = false;

/**
* Aggregate of QueryBuilder used for the query.
*
* @var QueryBuilder[] $queryBuilders
*/
private array $queryBuilders = [];

/**
* The counter of bound parameters used by the query builders.
*
* @var int<0, max>
*/
private int $boundCounter = 0;

public static function create(): self
{
if (self::$instance === null) {
self::$instance = new QueryBuildersAggregator();
}

return self::$instance;
}

private function __construct()
{
}

public function toReset(): void
{
$this->toReset = true;
}

public function register(QueryBuilder $queryBuilder): void
{
if ($this->toReset) {
$this->queryBuilders = [];
$this->boundCounter = 0;
$this->toReset = false;
}

$this->queryBuilders[] = $queryBuilder;
}

/**
* @return list<mixed>|array<string, mixed>
*
* @throws QueryException
*/
public function buildParametersAndTypes(): array
{
$parameters = [];
$types = [];
foreach ($this->queryBuilders as $queryBuilder) {
$this->rejectDuplicatedParameterNames($parameters, $queryBuilder->getParameters());
$parameters = array_merge($parameters, $queryBuilder->getParameters());
$types = array_merge($types, $queryBuilder->getParameterTypes());
}

return [$parameters, $types];
}

/**
* Guards against duplicated parameter names.
*
* @param list<mixed>|array<string, mixed> $params
* @param list<mixed>|array<string, mixed> $paramsToMerge
*
* @throws QueryException
*/
private function rejectDuplicatedParameterNames(array $params, array $paramsToMerge): void
{
if (count($params) === 0 || count($paramsToMerge) === 0) {
return;
}

$paramKeys = array_filter(array_keys($params), 'is_string');
$cteParamKeys = array_filter(array_keys($paramsToMerge), 'is_string');
$duplicated = array_intersect($paramKeys, $cteParamKeys);
if (count($duplicated) > 0) {
throw new QueryException(sprintf(
'Found duplicated parameter in query. The duplicated parameter names are: "%s".',
implode(', ', $duplicated),
));
}
}

public function getBoundCounter(): int
{
return $this->boundCounter;
}

public function incrementBoundCounter(): void
{
$this->boundCounter++;
}
}
41 changes: 38 additions & 3 deletions tests/Functional/Query/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,17 +256,26 @@ public function testUnionAndAddUnionWithNamedParameterOnOuterInstanceAndOrderByD
$subQueryBuilder1 = $this->connection->createQueryBuilder();
$subQueryBuilder1->select('id')
->from('for_update')
->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
->where($subQueryBuilder1->expr()->eq(
'id',
$subQueryBuilder1->createNamedParameter(1, ParameterType::INTEGER),
));

$subQueryBuilder2 = $this->connection->createQueryBuilder();
$subQueryBuilder2->select('id')
->from('for_update')
->where($qb->expr()->eq('id', $qb->createNamedParameter(2, ParameterType::INTEGER)));
->where($subQueryBuilder2->expr()->eq(
'id',
$subQueryBuilder2->createNamedParameter(2, ParameterType::INTEGER),
));

$subQueryBuilder3 = $this->connection->createQueryBuilder();
$subQueryBuilder3->select('id')
->from('for_update')
->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
->where($subQueryBuilder3->expr()->eq(
'id',
$subQueryBuilder3->createNamedParameter(1, ParameterType::INTEGER),
));

$qb->union($subQueryBuilder1)
->addUnion($subQueryBuilder2, UnionType::DISTINCT)
Expand Down Expand Up @@ -332,6 +341,32 @@ public function testUnionAndAddUnionWorksWithQueryBuilderPartsAndReturnsExpected
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
}

public function testSelectWithParametersBindOnDifferentQueryBuilders(): void
{
$expectedRows = $this->prepareExpectedRows([['id' => 1], ['id' => 2]]);
$qb = $this->connection->createQueryBuilder();

$queryBuilder1 = $this->connection->createQueryBuilder();
$queryBuilder1->select('id')
->from('for_update')
->where('id = :id1')
->setParameter('id1', 1);

$queryBuilder2 = $this->connection->createQueryBuilder();
$queryBuilder2->select('id')
->from('for_update')
->where('id = :id2')
->setParameter('id2', 2);

$qb->select('id')
->from('for_update')
->where($qb->expr()->in('id', $queryBuilder1->getSQL()))
->orWhere($qb->expr()->in('id', $queryBuilder2->getSQL()))
->orderBy('id', 'ASC');

self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
}

/**
* @param array<array<string, int>> $rows
*
Expand Down
6 changes: 6 additions & 0 deletions tests/Query/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Query\QueryBuildersAggregator;
use Doctrine\DBAL\Query\QueryException;
use Doctrine\DBAL\Query\UnionType;
use Doctrine\DBAL\Result;
Expand Down Expand Up @@ -51,6 +52,11 @@ protected function setUp(): void

$this->conn->method('getDatabasePlatform')
->willReturn($platform);

$queryBuildersAggregate = QueryBuildersAggregator::create();
$this->conn->method('getQueryBuildersAggregator')
->willReturn($queryBuildersAggregate);
$queryBuildersAggregate->toReset();
}

public function testSimpleSelectWithoutFrom(): void
Expand Down

0 comments on commit 0ef19b9

Please sign in to comment.