Skip to content

Commit

Permalink
Merge pull request doctrine#6621 from nio-dtp/cte-support
Browse files Browse the repository at this point in the history
Add CTE support to select in QueryBuilder
  • Loading branch information
greg0ire authored Jan 5, 2025
2 parents 69d5e34 + b20f5e6 commit 4d8badd
Show file tree
Hide file tree
Showing 9 changed files with 417 additions and 1 deletion.
32 changes: 32 additions & 0 deletions docs/en/reference/query-builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,38 @@ or QueryBuilder instances to one of the following methods:
->orderBy('field', 'DESC')
->setMaxResults(100);
Common Table Expressions
~~~~~~~~~~~

To define Common Table Expressions (CTEs) that can be used in select query.

* ``with(string $name, string|QueryBuilder $queryBuilder, ?array $columns = null)``

.. code-block:: php
<?php
$cteQueryBuilder1
->select('id')
->from('table_a')
->where('id = :id');
$cteQueryBuilder2
->select('id')
->from('table_b');
$queryBuilder
->with('cte_a', $cteQueryBuilder1)
->with('cte_b', $cteQueryBuilder2)
->select('id')
->from('cte_b', 'b')
->join('b', 'cte_a', 'a', 'a.id = b.id')
->setParameter('id', 1);
Multiple CTEs can be defined by calling the with method multiple times.

Values of parameters used in a CTE should be defined in the main QueryBuilder.

Building Expressions
--------------------

Expand Down
6 changes: 6 additions & 0 deletions src/Platforms/AbstractPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Doctrine\DBAL\SQL\Builder\DefaultUnionSQLBuilder;
use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\SQL\Builder\UnionSQLBuilder;
use Doctrine\DBAL\SQL\Builder\WithSQLBuilder;
use Doctrine\DBAL\SQL\Parser;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
Expand Down Expand Up @@ -807,6 +808,11 @@ public function createUnionSQLBuilder(): UnionSQLBuilder
return new DefaultUnionSQLBuilder($this);
}

public function createWithSQLBuilder(): WithSQLBuilder
{
return new WithSQLBuilder();
}

/**
* @internal
*
Expand Down
6 changes: 6 additions & 0 deletions src/Platforms/MySQL80Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Platforms\Keywords\MySQL80Keywords;
use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
use Doctrine\DBAL\SQL\Builder\WithSQLBuilder;
use Doctrine\Deprecations\Deprecation;

/**
Expand All @@ -32,4 +33,9 @@ public function createSelectSQLBuilder(): SelectSQLBuilder
{
return AbstractPlatform::createSelectSQLBuilder();
}

public function createWithSQLBuilder(): WithSQLBuilder
{
return AbstractPlatform::createWithSQLBuilder();
}
}
7 changes: 7 additions & 0 deletions src/Platforms/MySQLPlatform.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace Doctrine\DBAL\Platforms;

use Doctrine\DBAL\Platforms\Exception\NotSupported;
use Doctrine\DBAL\Platforms\Keywords\KeywordList;
use Doctrine\DBAL\Platforms\Keywords\MySQLKeywords;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\SQL\Builder\WithSQLBuilder;
use Doctrine\DBAL\Types\BlobType;
use Doctrine\DBAL\Types\TextType;
use Doctrine\Deprecations\Deprecation;
Expand Down Expand Up @@ -35,6 +37,11 @@ public function getDefaultValueDeclarationSQL(array $column): string
return parent::getDefaultValueDeclarationSQL($column);
}

public function createWithSQLBuilder(): WithSQLBuilder
{
throw NotSupported::new(__METHOD__);
}

/**
* {@inheritDoc}
*/
Expand Down
23 changes: 23 additions & 0 deletions src/Query/CommonTableExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\Query;

use function count;
use function sprintf;

/** @internal */
final class CommonTableExpression
{
/** @param string[]|null $columns */
public function __construct(
public readonly string $name,
public readonly string|QueryBuilder $query,
public readonly ?array $columns,
) {
if ($columns !== null && count($columns) === 0) {
throw new QueryException(sprintf('Columns defined in CTE "%s" should not be an empty array.', $name));
}
}
}
52 changes: 51 additions & 1 deletion src/Query/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
use Doctrine\DBAL\Statement;
use Doctrine\DBAL\Types\Type;

use function array_filter;
use function array_intersect;
use function array_key_exists;
use function array_keys;
use function array_merge;
use function array_unshift;
use function count;
use function implode;
use function is_object;
use function sprintf;
use function substr;

/**
Expand Down Expand Up @@ -160,6 +163,13 @@ class QueryBuilder
*/
private array $unionParts = [];

/**
* The common table expression parts.
*
* @var CommonTableExpression[]
*/
private array $commonTableExpressions = [];

/**
* The query cache profile used for caching results.
*/
Expand Down Expand Up @@ -557,6 +567,36 @@ public function addUnion(string|QueryBuilder $part, UnionType $type = UnionType:
return $this;
}

/**
* Add a Common Table Expression to be used for a select query.
*
* <code>
* // WITH cte_name AS (SELECT id AS column1 FROM table_a)
* $qb = $conn->createQueryBuilder()
* ->with('cte_name', 'SELECT id AS column1 FROM table_a');
*
* // WITH cte_name(column1) AS (SELECT id AS column1 FROM table_a)
* $qb = $conn->createQueryBuilder()
* ->with('cte_name', 'SELECT id AS column1 FROM table_a', ['column1']);
* </code>
*
* @param string $name The name of the CTE
* @param string[]|null $columns The optional columns list to select in the CTE.
* If not provided, the columns are inferred from the CTE.
*
* @return $this This QueryBuilder instance.
*
* @throws QueryException Setting an empty array as columns is not allowed.
*/
public function with(string $name, string|QueryBuilder $part, ?array $columns = null): self
{
$this->commonTableExpressions[] = new CommonTableExpression($name, $part, $columns);

$this->sql = null;

return $this;
}

/**
* Specifies an item that is to be returned in the query result.
* Replaces any previously specified selections, if any.
Expand Down Expand Up @@ -1266,7 +1306,15 @@ private function getSQLForSelect(): string
throw new QueryException('No SELECT expressions given. Please use select() or addSelect().');
}

return $this->connection->getDatabasePlatform()
$databasePlatform = $this->connection->getDatabasePlatform();
$selectParts = [];
if (count($this->commonTableExpressions) > 0) {
$selectParts[] = $databasePlatform
->createWithSQLBuilder()
->buildSQL(...$this->commonTableExpressions);
}

$selectParts[] = $databasePlatform
->createSelectSQLBuilder()
->buildSQL(
new SelectQuery(
Expand All @@ -1281,6 +1329,8 @@ private function getSQLForSelect(): string
$this->forUpdate,
),
);

return implode(' ', $selectParts);
}

/**
Expand Down
31 changes: 31 additions & 0 deletions src/SQL/Builder/WithSQLBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Doctrine\DBAL\SQL\Builder;

use Doctrine\DBAL\Query\CommonTableExpression;

use function array_merge;
use function count;
use function implode;

final class WithSQLBuilder
{
public function buildSQL(CommonTableExpression $firstExpression, CommonTableExpression ...$otherExpressions): string
{
$cteParts = [];

foreach (array_merge([$firstExpression], $otherExpressions) as $part) {
$ctePart = [$part->name];
if ($part->columns !== null && count($part->columns) > 0) {
$ctePart[] = ' (' . implode(', ', $part->columns) . ')';
}

$ctePart[] = ' AS (' . $part->query . ')';
$cteParts[] = implode('', $ctePart);
}

return 'WITH ' . implode(', ', $cteParts);
}
}
Loading

0 comments on commit 4d8badd

Please sign in to comment.