From 59fe0ee2bc13cd158945c0263197114433bf6dc0 Mon Sep 17 00:00:00 2001 From: "lina.wolf" Date: Sun, 16 Feb 2025 12:09:03 +0100 Subject: [PATCH 1/6] [FEATURE] Add UNION Clause support to the QueryBuilder Resolves: https://github.com/TYPO3-Documentation/Changelog-To-Doc/issues/994 Releases: main, 13.4 --- .../Database/QueryBuilder/Index.rst | 78 ++++++++++++++++++ .../Database/QueryBuilder/_UnionExample.php | 80 +++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php diff --git a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst index ea87eac043..ed4193a147 100644 --- a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst +++ b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst @@ -847,6 +847,84 @@ Remarks: * For more complex statements you can use the raw Doctrine QueryBuilder. See remarks for :ref:`orderBy() ` +.. _database-query-builder-union: + +union() and addUnion() +====================== + +Method `union()` provides a streamlined way to combine result sets from multiple +queries. + +:php:`union(string|QueryBuilder $part)` + Creates the initial :sql:`UNION` query part by accepting either a raw SQL + string or a `QueryBuilder` instance. + +:php:`addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT)` + Adds additional :sql:`UNION` parts to the query. The `$type` parameter accepts: + + `UnionType::DISTINCT` + Combines results while eliminating duplicates. + `UnionType::ALL` + Combines results and retains all duplicates. Not removing duplicates can + be a performance improvement. + +.. note:: + While technically possible, it is not recommended to send direct SQL queries + as strings to the `union()` and `addUnion()` methods. We recommend to use a + query builder. + + If you decide to do so you **must** take care of quoting, escaping, and + valid SQL Syntax for the database system in question. The `Default Restrictions `_ + are **not applied** on that part. + +Named placeholders, such as created by :php:`QueryBuilder::createNamedParameter()` +**must** be created on the outer most QueryBuilder See the example below. + +.. seealso:: + * `W3School: SQL UNION Operator `_ + * For technical details see the changelog entry `Feature: #104631 - Add + UNION Clause support to the QueryBuilder `_. + +.. _database-query-builder-union-db-support: + +Database provider support of union() and addUnion +------------------------------------------------- + +:php-short:`\TYPO3\CMS\Core\Database\Query\QueryBuilder` can be used create +:sql:`UNION` clause queries not compatible with all database providers, +for example using :sql:`LIMIT/OFFSET` in each part query or other stuff. + +When building functional tests, run them on all database types that should +be supported. + +.. _database-query-builder-union-example-querybuilder: + +Example using `union()` on two QueryBuilders +-------------------------------------------- + +.. literalinclude:: _UnionExample.php + :caption: packages/my_extension/classes/Service/MyService.php + +Line 18 + All query parts **must** share the same connection. +Line 19 + The outer most QueryBuilder is responsible for the union, it **must** be + used to create named parameters and build expressions within the sub queries. +Line 22-23 + We therefore pass the central QueryBuilder responsible for the :sql:`UNION` + to all subqueries. Same with the ExpressionBuilder. +Line 25-30 + We start building the `union()` on the first sub query, then add the + second sub query using `addUnion()` +Line 41 + Only use the ExpressionBuilder of the sql:`UNION` within the subqueries. +Line 50 + Named parameters must also be called on the outer most union query builder. + +The `Default Restrictions `_ +are applied to each subquery automatically. + +.. _database-query-builder-setMaxResults: setMaxResults() and setFirstResult() ==================================== diff --git a/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php b/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php new file mode 100644 index 0000000000..aaf07c1955 --- /dev/null +++ b/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php @@ -0,0 +1,80 @@ +connectionPool->getConnectionForTable('pages'); + $unionQueryBuilder = $connection->createQueryBuilder(); + + // Passing the outermost QueryBuilder to the subqueries + $firstPartQueryBuilder = $this->getUnionPart1QueryBuilder($connection, $unionQueryBuilder, $parentId); + $secondPartQueryBuilder = $this->getUnionPart2QueryBuilder($connection, $unionQueryBuilder, $parentId); + + return $unionQueryBuilder + ->union($firstPartQueryBuilder) + ->addUnion($secondPartQueryBuilder, UnionType::DISTINCT) + ->orderBy('uid', 'ASC') + ->executeQuery() + ->fetchAllAssociative(); + } + + private function getUnionPart1QueryBuilder( + Connection $connection, + QueryBuilder $unionQueryBuilder, + int $pageId, + ): QueryBuilder { + $queryBuilder = $connection->createQueryBuilder(); + // The union Expression Builder **must** be used on subqueries + $unionExpr = $unionQueryBuilder->expr(); + $queryBuilder + // The column names of the first query are used + // The column count of both subqueries must be the same + // The data types must be compatible across columns of the queries + ->select('title', 'subtitle') + ->from('pages') + ->where( + // The union Expression Builder **must** be used on subqueries + $unionExpr->eq( + 'pages.pid', + // Named parameters **must** be created on the outermost (union) query builder + $unionQueryBuilder->createNamedParameter($pageId), + ), + ); + return $queryBuilder; + } + + private function getUnionPart2QueryBuilder( + Connection $connection, + QueryBuilder $unionQueryBuilder, + int $pageId, + ): QueryBuilder { + $queryBuilder = $connection->createQueryBuilder(); + // The union Expression Builder **must** be used on subqueries + $unionExpr = $unionQueryBuilder->expr(); + $queryBuilder + // The column count of both subqueries must be the same + ->select('header', 'subheader') + ->from('tt_content') + ->where( + $unionExpr->eq( + 'tt_content.pid', + // Named parameters **must** be created on the outermost (union) query builder + $unionQueryBuilder->createNamedParameter($pageId), + ), + ); + return $queryBuilder; + } +} From 0b58155ee03993bf5ae709f237afb865df2b3989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Fr=C3=B6mken?= Date: Sun, 16 Feb 2025 13:40:32 +0100 Subject: [PATCH 2/6] Update Documentation/ApiOverview/Database/QueryBuilder/Index.rst --- Documentation/ApiOverview/Database/QueryBuilder/Index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst index ed4193a147..f6b983c776 100644 --- a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst +++ b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst @@ -874,7 +874,7 @@ queries. query builder. If you decide to do so you **must** take care of quoting, escaping, and - valid SQL Syntax for the database system in question. The `Default Restrictions `_ + valid SQL Syntax for the database system in question. The `Default Restrictions `_ are **not applied** on that part. Named placeholders, such as created by :php:`QueryBuilder::createNamedParameter()` From 49434a0efa88cdfbb3ede54e632d3a9a23b1eb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Fr=C3=B6mken?= Date: Sun, 16 Feb 2025 13:43:34 +0100 Subject: [PATCH 3/6] Update Documentation/ApiOverview/Database/QueryBuilder/Index.rst --- Documentation/ApiOverview/Database/QueryBuilder/Index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst index f6b983c776..6b3a87fdd7 100644 --- a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst +++ b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst @@ -887,8 +887,8 @@ Named placeholders, such as created by :php:`QueryBuilder::createNamedParameter( .. _database-query-builder-union-db-support: -Database provider support of union() and addUnion -------------------------------------------------- +Database provider support of union() and addUnion() +--------------------------------------------------- :php-short:`\TYPO3\CMS\Core\Database\Query\QueryBuilder` can be used create :sql:`UNION` clause queries not compatible with all database providers, From ea04ff50dd42634c2b86a2228228cf75bfefc642 Mon Sep 17 00:00:00 2001 From: Lina Wolf <48202465+linawolf@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:08:51 +0100 Subject: [PATCH 4/6] Update Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stefan Bürk --- .../ApiOverview/Database/QueryBuilder/_UnionExample.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php b/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php index aaf07c1955..49c3f85ff0 100644 --- a/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php +++ b/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php @@ -72,7 +72,7 @@ private function getUnionPart2QueryBuilder( $unionExpr->eq( 'tt_content.pid', // Named parameters **must** be created on the outermost (union) query builder - $unionQueryBuilder->createNamedParameter($pageId), + $unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT), ), ); return $queryBuilder; From 2b6bc45723595b3a1e431c52487e3f11afa6a480 Mon Sep 17 00:00:00 2001 From: Lina Wolf <48202465+linawolf@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:09:11 +0100 Subject: [PATCH 5/6] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stefan Bürk --- .../ApiOverview/Database/QueryBuilder/_UnionExample.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php b/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php index 49c3f85ff0..9f545806a5 100644 --- a/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php +++ b/Documentation/ApiOverview/Database/QueryBuilder/_UnionExample.php @@ -50,7 +50,7 @@ private function getUnionPart1QueryBuilder( $unionExpr->eq( 'pages.pid', // Named parameters **must** be created on the outermost (union) query builder - $unionQueryBuilder->createNamedParameter($pageId), + $unionQueryBuilder->createNamedParameter($pageId, Connection::PARAM_INT), ), ); return $queryBuilder; From eb0572ebc9123ce08c43aa95d43dba565b275c78 Mon Sep 17 00:00:00 2001 From: Lina Wolf <48202465+linawolf@users.noreply.github.com> Date: Mon, 17 Feb 2025 06:11:03 +0100 Subject: [PATCH 6/6] Update Index.rst --- Documentation/ApiOverview/Database/QueryBuilder/Index.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst index 6b3a87fdd7..b5ea0acdce 100644 --- a/Documentation/ApiOverview/Database/QueryBuilder/Index.rst +++ b/Documentation/ApiOverview/Database/QueryBuilder/Index.rst @@ -857,7 +857,9 @@ queries. :php:`union(string|QueryBuilder $part)` Creates the initial :sql:`UNION` query part by accepting either a raw SQL - string or a `QueryBuilder` instance. + string or a `QueryBuilder` instance. Calling `union()` resets all previous + union definitions, it should therefore only be called once, using `addUnion()` + to add subsequent union parts. :php:`addUnion(string|QueryBuilder $part, UnionType $type = UnionType::DISTINCT)` Adds additional :sql:`UNION` parts to the query. The `$type` parameter accepts: