Skip to content

Commit

Permalink
Add CTE support to select in QueryBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
nio-dtp committed Nov 23, 2024
1 parent 052545f commit 686bf02
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 1 deletion.
48 changes: 47 additions & 1 deletion src/Query/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use function count;
use function implode;
use function is_object;
use function sprintf;
use function substr;

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

/**
* The common table expression parts.
*
* @var array<string, self>
*/
private array $with = [];

/**
* The query cache profile used for caching results.
*/
Expand Down Expand Up @@ -342,7 +350,7 @@ public function getSQL(): string
QueryType::INSERT => $this->getSQLForInsert(),
QueryType::DELETE => $this->getSQLForDelete(),
QueryType::UPDATE => $this->getSQLForUpdate(),
QueryType::SELECT => $this->getSQLForSelect(),
QueryType::SELECT => $this->getCommonTableExpressions() . $this->getSQLForSelect(),
QueryType::UNION => $this->getSQLForUnion(),
};
}
Expand Down Expand Up @@ -1259,6 +1267,30 @@ public function resetOrderBy(): self
return $this;
}

/**
* Adds a CTE to a select query.
*
* @param string $name The name of the CTE.
* @param self $queryBuilder The query builder to use within the CTE.
*
* @return $this This QueryBuilder instance.
*/
public function with(string $name, self $queryBuilder): self
{
if ($queryBuilder->hasCommonTableExpressions()) {
throw new QueryException('CTEs cannot be nested.');
}

$this->with[$name] = $queryBuilder;

return $this;
}

private function hasCommonTableExpressions(): bool
{
return 0 < count($this->with);
}

/** @throws Exception */
private function getSQLForSelect(): string
{
Expand All @@ -1283,6 +1315,20 @@ private function getSQLForSelect(): string
);
}

private function getCommonTableExpressions(): string
{
if (! $this->hasCommonTableExpressions()) {
return '';
}

$cte = [];
foreach ($this->with as $name => $queryBuilder) {
$cte[] = sprintf('%s AS (%s)', $name, $queryBuilder->getSQL());
}

return 'WITH ' . implode(', ', $cte) . ' ';
}

/**
* @return array<string, string>
*
Expand Down
40 changes: 40 additions & 0 deletions tests/Query/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,46 @@ public function testSelectAllWithoutTableAlias(): void
self::assertEquals('SELECT * FROM users', (string) $qb);
}

public function testSelectWithCTE(): void
{
$qbWith = new QueryBuilder($this->conn);
$qbWith->select('id', 'name')
->from('users')
->where('name LIKE :name');

$qb = new QueryBuilder($this->conn);
$qb->with('filtered_users', $qbWith)
->select('*')
->from('filtered_users', 'fu');

self::assertEquals(
'WITH filtered_users AS (SELECT id, name FROM users WHERE name LIKE :name)'
. ' SELECT * FROM filtered_users fu',
$qb->getSQL(),
);
}

public function testSelectNestedCTE(): void
{
$qbNested = new QueryBuilder($this->conn);
$qbNested->select('*')
->from('country');

$qbWith = new QueryBuilder($this->conn);
$qbWith->select('id', 'name')
->with('countries', $qbNested)
->from('users')
->where('name LIKE :name');

self::expectException(QueryException::class);
self::expectExceptionMessage('CTEs cannot be nested.');

$qb = new QueryBuilder($this->conn);
$qb->with('filtered_users', $qbWith)
->select('*')
->from('filtered_users', 'fu');
}

public function testGetParameterType(): void
{
$qb = new QueryBuilder($this->conn);
Expand Down

0 comments on commit 686bf02

Please sign in to comment.