diff --git a/docs/en/reference/query-builder.rst b/docs/en/reference/query-builder.rst
index b15a75f67ac..f9242b68d0f 100644
--- a/docs/en/reference/query-builder.rst
+++ b/docs/en/reference/query-builder.rst
@@ -315,6 +315,53 @@ user-input:
->setParameter(0, $userInputLastLogin)
;
+UNION-Clause
+~~~~~~~~~~~~
+
+To combine multiple ``SELECT`` queries into one result-set you can pass SQL Part strings
+or QueryBuilder instances to one of the following methods:
+
+* ``union('SELECT 1 as field', $partQueryBuilder)``
+* ``addUnion('SELECT 1 as field', $partQueryBuilder)``
+* ``unionAll('SELECT 1 as field', $partQueryBuilder)``
+* ``addUnionAll('SELECT 1 as field', $partQueryBuilder)``
+
+.. code-block:: php
+
+ union('SELECT 1 AS field', 'SELECT 2 AS field')
+ ->addUnion('SELECT 3 AS field', 'SELECT 3 as field')
+ ;
+
+ $queryBuilder
+ ->unionAll('SELECT 1 AS field', 'SELECT 2 AS field')
+ ->addUnionAll('SELECT 3 AS field', 'SELECT 3 as field')
+ ;
+
+ $subQueryBuilder1
+ ->select('id AS field')
+ ->from('a_table');
+ $subQueryBuilder2
+ ->select('id AS field')
+ ->from('a_table');
+ $queryBuilder
+ ->union($subQueryBuilder1)
+ ->addUnion($subQueryBuilder2)
+ ;
+
+ $subQueryBuilder1
+ ->select('id AS field')
+ ->from('a_table');
+ $subQueryBuilder2
+ ->select('id AS field')
+ ->from('a_table');
+ $queryBuilder
+ ->unionAll($subQueryBuilder1)
+ ->addUnionAll($subQueryBuilder2)
+ ;
+
Building Expressions
--------------------
diff --git a/src/Platforms/AbstractPlatform.php b/src/Platforms/AbstractPlatform.php
index 0beb37a0939..6e35ef8f146 100644
--- a/src/Platforms/AbstractPlatform.php
+++ b/src/Platforms/AbstractPlatform.php
@@ -27,7 +27,9 @@
use Doctrine\DBAL\Schema\TableDiff;
use Doctrine\DBAL\Schema\UniqueConstraint;
use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\DefaultUnionSQLBuilder;
use Doctrine\DBAL\SQL\Builder\SelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\UnionSQLBuilder;
use Doctrine\DBAL\SQL\Parser;
use Doctrine\DBAL\TransactionIsolationLevel;
use Doctrine\DBAL\Types;
@@ -770,6 +772,11 @@ public function createSelectSQLBuilder(): SelectSQLBuilder
return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED');
}
+ public function createUnionSQLBuilder(): UnionSQLBuilder
+ {
+ return new DefaultUnionSQLBuilder($this);
+ }
+
/**
* @internal
*
diff --git a/src/Query/QueryBuilder.php b/src/Query/QueryBuilder.php
index 1127c6b9e4a..e71243f3fde 100644
--- a/src/Query/QueryBuilder.php
+++ b/src/Query/QueryBuilder.php
@@ -153,6 +153,13 @@ class QueryBuilder
*/
private array $values = [];
+ /**
+ * The QueryBuilder for the union parts.
+ *
+ * @var string[]|QueryBuilder[]
+ */
+ private array $union = [];
+
/**
* The query cache profile used for caching results.
*/
@@ -336,6 +343,8 @@ public function getSQL(): string
QueryType::DELETE => $this->getSQLForDelete(),
QueryType::UPDATE => $this->getSQLForUpdate(),
QueryType::SELECT => $this->getSQLForSelect(),
+ QueryType::UNION_ALL,
+ QueryType::UNION_DISTINCT => $this->getSQLForUnion(),
};
}
@@ -501,6 +510,94 @@ public function forUpdate(ConflictResolutionMode $conflictResolutionMode = Confl
return $this;
}
+ /**
+ * Specifies union parts to be used to build a UNION query.
+ * Replaces any previously specified parts.
+ *
+ *
+ * $qb = $conn->createQueryBuilder()
+ * ->union('SELECT 1 AS field1', 'SELECT 2 AS field1');
+ *
+ *
+ * @return $this
+ */
+ public function union(string|QueryBuilder ...$parts): self
+ {
+ $this->type = QueryType::UNION_DISTINCT;
+
+ $this->union = $parts;
+
+ $this->sql = null;
+
+ return $this;
+ }
+
+ /**
+ * Add parts to be used to build a UNION query.
+ *
+ *
+ * $qb = $conn->createQueryBuilder()
+ * ->union('SELECT 1 AS field1')
+ * ->addUnion('SELECT 2 AS field1', 'SELECT 3 AS field1')
+ *
+ *
+ * @return $this
+ */
+ public function addUnion(string|QueryBuilder ...$parts): self
+ {
+ $this->type = QueryType::UNION_DISTINCT;
+
+ $this->union = array_merge($this->union, $parts);
+
+ $this->sql = null;
+
+ return $this;
+ }
+
+ /**
+ * Specifies UNION ALL parts to be used to build a UNION query.
+ * Replaces any previously specified parts.
+ *
+ *
+ * $qb = $conn->createQueryBuilder()
+ * ->unionAll('SELECT 1 AS field1', 'SELECT 2 AS field1');
+ *
+ *
+ * @return $this
+ */
+ public function unionAll(string|QueryBuilder ...$parts): self
+ {
+ $this->type = QueryType::UNION_ALL;
+
+ $this->union = $parts;
+
+ $this->sql = null;
+
+ return $this;
+ }
+
+ /**
+ * Add parts to be used to build a UNION ALL query.
+ *
+ *
+ * $qb = $conn->createQueryBuilder()
+ * ->unionAll('SELECT 1 AS field1')
+ * ->addUnionAll('SELECT 2 AS field1', 'SELECT 3 AS field1')
+ *
+ *
+ * @return $this
+ */
+ public function addUnionAll(string|QueryBuilder ...$parts): self
+ {
+ $this->type = QueryType::UNION_ALL;
+
+ $this->union = array_merge($this->union, $parts);
+
+ $this->sql = null;
+
+ return $this;
+ }
+
/**
* Specifies an item that is to be returned in the query result.
* Replaces any previously specified selections, if any.
@@ -1309,6 +1406,30 @@ private function getSQLForDelete(): string
return $query;
}
+ /**
+ * Converts this instance into a UNION string in SQL.
+ */
+ private function getSQLForUnion(): string
+ {
+ $countUnions = count($this->union);
+ if ($countUnions < 2) {
+ throw new QueryException($countUnions === 0
+ ? 'No UNION parts given. Please use union() or addUnion().'
+ : 'Insufficient UNION parts give, need at least 2. Please use addUnion() to add more.');
+ }
+
+ return $this->connection->getDatabasePlatform()
+ ->createUnionSQLBuilder()
+ ->buildSQL(
+ new UnionQuery(
+ $this->type === QueryType::UNION_DISTINCT,
+ $this->union,
+ $this->orderBy,
+ new Limit($this->maxResults, $this->firstResult),
+ ),
+ );
+ }
+
/**
* Gets a string representation of this QueryBuilder which corresponds to
* the final SQL query being constructed.
diff --git a/src/Query/QueryType.php b/src/Query/QueryType.php
index 632c4959fc9..4dc8ffc551e 100644
--- a/src/Query/QueryType.php
+++ b/src/Query/QueryType.php
@@ -11,4 +11,6 @@ enum QueryType
case DELETE;
case UPDATE;
case INSERT;
+ case UNION_ALL;
+ case UNION_DISTINCT;
}
diff --git a/src/Query/UnionQuery.php b/src/Query/UnionQuery.php
new file mode 100644
index 00000000000..93721ea3408
--- /dev/null
+++ b/src/Query/UnionQuery.php
@@ -0,0 +1,44 @@
+unionDistinct;
+ }
+
+ /** @return string[]|QueryBuilder[] */
+ public function getUnionParts(): array
+ {
+ return $this->unionParts;
+ }
+
+ /** @return string[] */
+ public function getOrderBy(): array
+ {
+ return $this->orderBy;
+ }
+
+ public function getLimit(): Limit
+ {
+ return $this->limit;
+ }
+}
diff --git a/src/SQL/Builder/DefaultUnionSQLBuilder.php b/src/SQL/Builder/DefaultUnionSQLBuilder.php
new file mode 100644
index 00000000000..1b1b090153d
--- /dev/null
+++ b/src/SQL/Builder/DefaultUnionSQLBuilder.php
@@ -0,0 +1,53 @@
+isUnionDistinct() ? ' UNION ' : ' UNION ALL ';
+ $unionParts = $this->prepareUnionParts($query);
+ $parts[] = implode($modifier, $unionParts);
+
+ $orderBy = $query->getOrderBy();
+ if (count($orderBy) > 0) {
+ $parts[] = 'ORDER BY ' . implode(', ', $orderBy);
+ }
+
+ $sql = implode(' ', $parts);
+ $limit = $query->getLimit();
+
+ if ($limit->isDefined()) {
+ $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult());
+ }
+
+ return $sql;
+ }
+
+ /** @return string[] */
+ private function prepareUnionParts(UnionQuery $query): array
+ {
+ $return = [];
+ $unionParts = $query->getUnionParts();
+ foreach ($unionParts as $part) {
+ $return[] = (string) $part;
+ }
+
+ return $return;
+ }
+}
diff --git a/src/SQL/Builder/UnionSQLBuilder.php b/src/SQL/Builder/UnionSQLBuilder.php
new file mode 100644
index 00000000000..271f035120d
--- /dev/null
+++ b/src/SQL/Builder/UnionSQLBuilder.php
@@ -0,0 +1,14 @@
+executeQuery();
}
+ public function testUnionAllReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 1], ['field_one' => 1], ['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->unionAll($plainSelect1, $plainSelect2, $plainSelect3)->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 1], ['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->union($plainSelect1, $plainSelect2, $plainSelect3)->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testOnlyAddUnionAllReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 1], ['field_one' => 1], ['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->addUnionAll($plainSelect1, $plainSelect2, $plainSelect3)->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testOnlyAddUnionReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 1], ['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->addUnion($plainSelect1, $plainSelect2, $plainSelect3)->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAllAndAddUnionAllReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 1], ['field_one' => 1], ['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->unionAll($plainSelect1)->addUnionAll($plainSelect2, $plainSelect3)->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAndAddUnionReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 1], ['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->union($plainSelect1)->addUnion($plainSelect2, $plainSelect3)->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAllWithDescOrderByReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 2], ['field_one' => 1], ['field_one' => 1]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->unionAll($plainSelect1, $plainSelect2, $plainSelect3)->orderBy('field_one', 'DESC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionWithDescOrderByReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 2], ['field_one' => 1]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->union($plainSelect1, $plainSelect2, $plainSelect3)->orderBy('field_one', 'DESC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAllWithLimitClauseReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->unionAll($plainSelect1, $plainSelect2, $plainSelect3)
+ ->setMaxResults(1)
+ ->setFirstResult(0)
+ ->orderBy('field_one', 'DESC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionWithLimitClauseReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->union($plainSelect1, $plainSelect2, $plainSelect3)
+ ->setMaxResults(1)
+ ->setFirstResult(0)
+ ->orderBy('field_one', 'DESC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAllWithLimitAndOffsetClauseReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 1]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->unionAll($plainSelect1, $plainSelect2, $plainSelect3)
+ ->setMaxResults(1)
+ ->setFirstResult(1)
+ ->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionWithLimitAndOffsetClauseReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['field_one' => 2]]);
+ $platform = $this->connection->getDatabasePlatform();
+ $plainSelect1 = $platform->getDummySelectSQL('1 as field_one');
+ $plainSelect2 = $platform->getDummySelectSQL('2 as field_one');
+ $plainSelect3 = $platform->getDummySelectSQL('1 as field_one');
+ $qb = $this->connection->createQueryBuilder();
+ $qb->union($plainSelect1, $plainSelect2, $plainSelect3)
+ ->setMaxResults(1)
+ ->setFirstResult(1)
+ ->orderBy('field_one', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAllAndAddUnionAllWorksWithQueryBuilderPartsAndOrderByDescAndReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['id' => 2], ['id' => 1], ['id' => 1]]);
+ $qb = $this->connection->createQueryBuilder();
+
+ $subQueryBuilder1 = $this->connection->createQueryBuilder();
+ $subQueryBuilder1->select('id')->from('for_update')->where($qb->expr()->eq('id', '1'));
+
+ $subQueryBuilder2 = $this->connection->createQueryBuilder();
+ $subQueryBuilder2->select('id')->from('for_update')->where($qb->expr()->eq('id', '2'));
+
+ $subQueryBuilder3 = $this->connection->createQueryBuilder();
+ $subQueryBuilder3->select('id')->from('for_update')->where($qb->expr()->eq('id', '1'));
+
+ $qb->unionAll($subQueryBuilder1)
+ ->addUnionAll($subQueryBuilder2, $subQueryBuilder3)
+ ->orderBy('id', 'DESC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAndAddUnionWithNamedParameterOnOuterInstanceAndOrderByDescWorks(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['id' => 2], ['id' => 1]]);
+ $qb = $this->connection->createQueryBuilder();
+
+ $subQueryBuilder1 = $this->connection->createQueryBuilder();
+ $subQueryBuilder1->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
+
+ $subQueryBuilder2 = $this->connection->createQueryBuilder();
+ $subQueryBuilder2->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(2, ParameterType::INTEGER)));
+
+ $subQueryBuilder3 = $this->connection->createQueryBuilder();
+ $subQueryBuilder3->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
+
+ $qb->union($subQueryBuilder1)->addUnion($subQueryBuilder2, $subQueryBuilder3)->orderBy('id', 'DESC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAllAndAddUnionAllWorksWithQueryBuilderPartsAndReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['id' => 1], ['id' => 1], ['id' => 2]]);
+ $qb = $this->connection->createQueryBuilder();
+
+ $subQueryBuilder1 = $this->connection->createQueryBuilder();
+ $subQueryBuilder1->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
+
+ $subQueryBuilder2 = $this->connection->createQueryBuilder();
+ $subQueryBuilder2->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(2, ParameterType::INTEGER)));
+
+ $subQueryBuilder3 = $this->connection->createQueryBuilder();
+ $subQueryBuilder3->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
+
+ $qb->unionAll($subQueryBuilder1)
+ ->addUnionAll($subQueryBuilder2, $subQueryBuilder3)
+ ->orderBy('id', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ public function testUnionAndAddUnionWorksWithQueryBuilderPartsAndReturnsExpectedResult(): void
+ {
+ $expectedRows = $this->prepareExpectedRows([['id' => 1], ['id' => 2]]);
+ $qb = $this->connection->createQueryBuilder();
+
+ $subQueryBuilder1 = $this->connection->createQueryBuilder();
+ $subQueryBuilder1->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
+
+ $subQueryBuilder2 = $this->connection->createQueryBuilder();
+ $subQueryBuilder2->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(2, ParameterType::INTEGER)));
+
+ $subQueryBuilder3 = $this->connection->createQueryBuilder();
+ $subQueryBuilder3->select('id')
+ ->from('for_update')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter(1, ParameterType::INTEGER)));
+
+ $qb->union($subQueryBuilder1)
+ ->addUnion($subQueryBuilder2, $subQueryBuilder3)
+ ->orderBy('id', 'ASC');
+
+ self::assertSame($expectedRows, $qb->executeQuery()->fetchAllAssociative());
+ }
+
+ /**
+ * @param array> $rows
+ *
+ * @return array>
+ */
+ private function prepareExpectedRows(array $rows): array
+ {
+ if (! TestUtil::isDriverOneOf('ibm_db2', 'pdo_oci', 'pdo_sqlsrv', 'oci8')) {
+ return $rows;
+ }
+
+ if (! TestUtil::isDriverOneOf('ibm_db2')) {
+ foreach ($rows as &$row) {
+ foreach ($row as &$value) {
+ $value = (string) $value;
+ }
+ }
+ }
+
+ if (! TestUtil::isDriverOneOf('ibm_db2', 'pdo_oci', 'oci8')) {
+ return $rows;
+ }
+
+ foreach ($rows as &$row) {
+ $row = array_change_key_case($row, CASE_UPPER);
+ }
+
+ return $rows;
+ }
+
private function platformSupportsSkipLocked(): bool
{
$platform = $this->connection->getDatabasePlatform();
diff --git a/tests/Query/QueryBuilderTest.php b/tests/Query/QueryBuilderTest.php
index e56417938c7..249c9a9ff30 100644
--- a/tests/Query/QueryBuilderTest.php
+++ b/tests/Query/QueryBuilderTest.php
@@ -14,6 +14,7 @@
use Doctrine\DBAL\Query\QueryException;
use Doctrine\DBAL\Result;
use Doctrine\DBAL\SQL\Builder\DefaultSelectSQLBuilder;
+use Doctrine\DBAL\SQL\Builder\DefaultUnionSQLBuilder;
use Doctrine\DBAL\Types\Types;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
@@ -38,6 +39,8 @@ protected function setUp(): void
$platform = $this->createMock(AbstractPlatform::class);
$platform->method('createSelectSQLBuilder')
->willReturn(new DefaultSelectSQLBuilder($platform, null, null));
+ $platform->method('createUnionSQLBuilder')
+ ->willReturn(new DefaultUnionSQLBuilder($platform));
$this->conn->method('getDatabasePlatform')
->willReturn($platform);
@@ -1409,4 +1412,116 @@ public function testExecuteStatement(): void
$results,
);
}
+
+ public function testUnionWithNoUnionPartsThrowException(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->union();
+
+ $this->expectException(QueryException::class);
+ $this->expectExceptionMessage('No UNION parts given. Please use union() or addUnion().');
+
+ $qb->getSQL();
+ }
+
+ public function testUnionWithOneUnionPartThrowException(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->union('SELECT 1 AS field_one');
+
+ $this->expectException(QueryException::class);
+ $this->expectExceptionMessage(
+ 'Insufficient UNION parts give, need at least 2. Please use addUnion() to add more.',
+ );
+
+ $qb->getSQL();
+ }
+
+ public function testUnionAllReturnsUnionAllQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->unionAll('SELECT 1 AS field_one', 'SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION ALL SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testUnionWAllAndLimitClauseReturnsUnionAllQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->unionAll('SELECT 1 AS field_one', 'SELECT 2 as field_one')->setMaxResults(10)->setFirstResult(10);
+
+ self::assertSame('SELECT 1 AS field_one UNION ALL SELECT 2 as field_one LIMIT 10 OFFSET 10', $qb->getSQL());
+ }
+
+ public function testOnlyAddUnionAllReturnsUnionAllQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->addUnionAll('SELECT 1 AS field_one', 'SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION ALL SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testUnionAllAndAddUnionAllReturnsUnionAllQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->unionAll('SELECT 1 AS field_one')->addUnionAll('SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION ALL SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testUnionAndAddUnionAllReturnsUnionAllQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->union('SELECT 1 AS field_one')->addUnionAll('SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION ALL SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testUnionAllAndAddUnionReturnsUnionQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->unionAll('SELECT 1 AS field_one')->addUnion('SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testUnionAllWithOrderByReturnsUnionAllQueryWithOrderBy(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->unionAll('SELECT 1 AS field_one', 'SELECT 2 as field_one')->orderBy('field_one', 'ASC');
+
+ self::assertSame('SELECT 1 AS field_one UNION ALL SELECT 2 as field_one ORDER BY field_one ASC', $qb->getSQL());
+ }
+
+ public function testUnionReturnsUnionQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->union('SELECT 1 AS field_one', 'SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testOnlyAddUnionReturnsUnionQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->addUnion('SELECT 1 AS field_one', 'SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testUnionAndAddUnionReturnsUnionQuery(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->union('SELECT 1 AS field_one')->addUnion('SELECT 2 as field_one');
+
+ self::assertSame('SELECT 1 AS field_one UNION SELECT 2 as field_one', $qb->getSQL());
+ }
+
+ public function testUnionAndOrderByReturnsUnionQueryWithOrderBy(): void
+ {
+ $qb = new QueryBuilder($this->conn);
+ $qb->union('SELECT 1 AS field_one', 'SELECT 2 as field_one')->orderBy('field_one', 'ASC');
+
+ self::assertSame('SELECT 1 AS field_one UNION SELECT 2 as field_one ORDER BY field_one ASC', $qb->getSQL());
+ }
}