Feature: #104631 - Add UNION Clause support to the QueryBuilder
See forge#104631
Description
The
UNION clause is used to combine the result sets of two or more
SELECT statements, which all database vendors support, each with their
own specific variations.
However, there is a commonly shared subset that works across all of them:
SELECT column_name(s) FROM table1
WHERE ...
UNION <ALL | DISTINCT>
SELECT column_name(s) FROM table2
WHERE ...
ORDER BY ...
LIMIT x OFFSET y
with shared requirements:
- Each SELECT must return the same fields in number, naming and order.
- Each SELECT must not have ORDER BY, expect MySQL allowing it to be used as sub query expression encapsulated in parentheses.
Generic
UNION clause support has been contributed to Doctrine DBAL and
is included since Release 4.1.0
which introduces two new API method on the
\Query:
unionto create first UNION query part(string |Query Builder $part) addto add additionalUnion (string |Query Builder $part, Union Type $type = Union Type:: DISTINCT) UNIONquery parts with the selected union query type.(ALL |DISTINCT)
TYPO3 decorates the Doctrine DBAL
\Query
to provide for most API methods automatic
quoting of identifiers and values and to apply database restrictions automatically
for
SELECT queries.
The Doctrine DBAL API has been adopted now to provide the same surface for the
TYPO3
\TYPO3\ and the intermediate
\TYPO3\ to make it easier to
create
UNION clause queries. The API on both methods allows to provide
dedicated
Query instances
or direct queries as strings in case it is needed.
Note
Providing
UNION parts as plain string requires the developer to take
care of proper quoting and escaping within the query part.
In queries containing subqueries, only named placeholders (such as :username)
can be used and must be registered on the outermost
Query object,
similar to advanced query creation with
SUB QUERIES.
Warning
Query can be used create
UNION clause queries not compatible with all database providers,
for example using
LIMIT/ in each part query or other stuff.
UnionType::DISTINCT and UnionType::ALL
Each subsequent part needs to be defined either as
UNION DISTINCT or
UNION ALL which could have not so obvious effects.
For example, using
UNION ALL for all parts in between except for the last
one would generate larger result sets first, but discards duplicates when adding
the last result set. On the other side, using
UNION ALL tells the query
optimizer not to scan for duplicates and remove them at all which can be a
performance improvement - if you can deal with duplicates it can be ensured that
each part does not produce same outputs.
Example: Compose a
UNION clause query
use Doctrine\DBAL\Query\UnionType;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
final readonly class MyService {
public function __construct(
private ConnectionPool $connectionPool,
) {}
public function executeUnionQuery(
int $pageIdOne,
int $pageIdTwo,
): ?array {
$connection = $this->connectionPool->getConnectionForTable('pages');
$unionQueryBuilder = $connection->createQueryBuilder();
$firstPartQueryBuilder = $connection->createQueryBuilder();
$secondPartQueryBuilder = $connection->createQueryBuilder();
// removing automatic TYPO3 restriction for the sake of the example
// to match the PLAIN SQL example when executed. Not removing them
// will generate corresponding restriction SQL code for each part.
$firstPartQueryBuilder->getRestrictions()->removeAll();
$secondPartQueryBuilder->getRestrictions()->removeAll();
$expr = $unionQueryBuilder->expr();
$firstPartQueryBuilder
// The query parts **must** have the same column counts, and these
// columns **must** have compatible types
->select('uid', 'pid', 'title')
->from('pages')
->where(
$expr->eq(
'pages.uid',
// !!! Ensure to use most outer / top / main QueryBuilder
// instance for creating parameters and the complete
// query can be executed in the end.
$unionQueryBuilder->createNamedParameter($pageIdOne, Connection::PARAM_INT),
)
);
$secondPartQueryBuilder
->select('uid', 'pid', 'title')
->from('pages')
->where(
$expr->eq(
'pages.uid',
// !!! Ensure to use most outer / top / main QueryBuilder instance
$unionQueryBuilder->createNamedParameter($pageIdTwo, Connection::PARAM_INT),
)
);
// Set first and second union part to the main (union)
// QueryBuilder and return the retrieved rows.
return $unionQueryBuilder
->union($firstPartQueryBuilder)
->addUnion($secondPartQueryBuilder, UnionType::DISTINCT)
->orderBy('uid', 'ASC')
->executeQuery()
->fetchAllAssociative();
}
}
This would create the following query for MySQL with
$page and
$page:
(SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 100)
UNION
(SELECT `uid`, `pid`, `title` FROM pages WHERE `pages`.`uid` = 10)
ORDER BY `uid` ASC
Impact
Extension authors can use the new
Query methods to build more
advanced queries.