Make it possible to create union main part for a CTE (#7326)

<!-- Fill in the relevant information below to help triage your pull
request. -->

|      Q       |   A
|------------- | -----------
| Type         | bug
| Fixed issues | #7318

#### Summary

<!-- Provide a summary of your change. -->

Technically this change adopts the solution for adding 
the CTE with parts to a SELECT query for UNION query
handling using the `WithSQLBuilder` in case with parts
has been set to allow creating queries like

```sql
WITH
-- CTE with parts
  cte_a AS (SELECT * FROM a_table)

-- CTEmain part
      (SELECT cte_a.*, 'lit1' from cte_a)
UNION (SELECT cte_a.*, 'lit2' from cte_a)
```

using the `union` and `with` api on the same level:

```php
$qb = $connection->createQueryBuilder();

$cte = $qb->sub()
  ->select('*')
  ->from('a_table');

$union1 = $qb->sub()
  ->select('cte_a.*', $qb->expr()->literal('lit1'))
  ->from('cte_a');

$union1 = $qb->sub()
  ->select('cte_a.*', $qb->expr()->literal('lit2'))
  ->from('cte_a');

$qb->with('cte_a', $cte)
  ->union($union1)
  ->addUnion($union2);
```

This is a valid use-case and supported by databases supporting
common table expressions albeit I could not find that documented
in any documentation and real world use-cases exists and is the
reason why this change has been considered as bugfix.
This commit is contained in:
Stefan Bürk
2026-03-03 13:12:17 +01:00
committed by GitHub
parent 131535f7c0
commit 3ccea71a7b
3 changed files with 81 additions and 3 deletions

View File

@@ -369,7 +369,10 @@ or QueryBuilder instances to one of the following methods:
->setMaxResults(100);
Common Table Expressions
~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~
`SELECT` main query
^^^^^^^^^^^^^^^^^^^
To define Common Table Expressions (CTEs) that can be used in select query.
@@ -400,6 +403,39 @@ 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.
`UNION` main query part
^^^^^^^^^^^^^^^^^^^^^^^
To define Common Table Expressions (CTEs) that can be used in union query the union
api needs to be used instead of using `select()`:
.. code-block:: php
<?php
$qb = $connection->createQueryBuilder();
$baseQueryBuilder = $qb->sub()
->select('id')
->from('table_a');
$unionPart1 = $qb->sub()
->select('id', $qb->expr()->literal('first') . ' AS value', '1 AS sort')
->from('cte_base')
->where($qb->expr()->eq('id', ':id1'));
$unionPart2 = $qb->sub()
->select('id', $qb->expr()->literal('second') . ' AS value', '2 AS sort')
->from('cte_base')
->where($qb->expr()->eq('id', ':id2'));
$qb->with('cte_base', $baseQueryBuilder)
->union($unionPart1)
->addUnion($unionPart2)
->orderBy('sort')
->setParameter('id1', 2)
->setParameter('id2', 1);
Building Expressions
--------------------

View File

@@ -1445,7 +1445,15 @@ class QueryBuilder
);
}
return $this->connection->getDatabasePlatform()
$databasePlatform = $this->connection->getDatabasePlatform();
$unionParts = [];
if (count($this->commonTableExpressions) > 0) {
$unionParts[] = $databasePlatform
->createWithSQLBuilder()
->buildSQL(...$this->commonTableExpressions);
}
$unionParts[] = $databasePlatform
->createUnionSQLBuilder()
->buildSQL(
new UnionQuery(
@@ -1454,6 +1462,8 @@ class QueryBuilder
new Limit($this->maxResults, $this->firstResult),
),
);
return implode(' ', $unionParts);
}
/**

View File

@@ -528,6 +528,38 @@ final class QueryBuilderTest extends FunctionalTestCase
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
}
public function testCTEUnionMainQuery(): void
{
if (! $this->platformSupportsCTEs()) {
self::markTestSkipped('The database platform does not support CTE.');
}
$expectedRows = [['id' => 2, 'value' => 'first', 'sort' => 1], ['id' => 1, 'value' => 'second', 'sort' => 2]];
$expectedRows = $this->prepareExpectedRows($expectedRows);
$qb = $this->connection->createQueryBuilder();
$baseQueryBuilder = $qb->sub()
->select('id')
->from('for_update');
$unionPart1 = $qb->sub()
->select('id', $qb->expr()->literal('first') . ' AS value', '1 AS sort')
->from('cte_base')
->where($qb->expr()->eq('id', '2'));
$unionPart2 = $qb->sub()
->select('id', $qb->expr()->literal('second') . ' AS value', '2 AS sort')
->from('cte_base')
->where($qb->expr()->eq('id', '1'));
$qb->with('cte_base', $baseQueryBuilder)
->union($unionPart1)
->addUnion($unionPart2)
->orderBy('sort');
self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
}
public function testPlatformDoesNotSupportCTE(): void
{
if ($this->platformSupportsCTEs()) {
@@ -549,7 +581,7 @@ final class QueryBuilderTest extends FunctionalTestCase
}
/**
* @param array<array<string, int>> $rows
* @param array<array<string, int|string>> $rows
*
* @return array<array<string, int|string>>
*/