Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic UNION support for the QueryBuilder #6368

Open
sbuerk opened this issue Apr 25, 2024 · 0 comments
Open

Basic UNION support for the QueryBuilder #6368

sbuerk opened this issue Apr 25, 2024 · 0 comments

Comments

@sbuerk
Copy link
Contributor

sbuerk commented Apr 25, 2024

Feature Request

Q A
New Feature yes
RFC no

Summary

Introduction / Description

The UNION operator is used to combine the result-set
of two or more SELECT statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Personally, I implemented helper, services
or builders on top/around the Doctrine DBAL
QueryBuilder in countless projects and companies.

Due to requirements for TYPO3 [1]
the need to implement something again raised,
and the whish raised to find a) a simple
way without additional helper or classes and
b) to add it in Doctrine DBAL directly.

Proposal

Following the current structure of the QueryBuilder,
two methods should be added:

  • add QueryType::UNION to distinguish the new
    query type from the others internally.
  • union(string|QueryBuilder ...$unionParts) as
    setter(reset).
  • addUnion(string|QueryBuilder ...$unionParts)
    to add/append additional parts without reseting
    already set parts.
  • Reuse distinct(bool $distinct) to differ between
    UNION (DISTINCT) (true) and UNION ALL (false).
  • add internal UnionQuery as DTO
  • getSQL() needs to be extended to dispatch a new
    method getSQLForUnion() for QueryType::UNION.
  • add getSQLForUnion() to dispatch the creation
    of the SQL string to a dedicated SQL Builder using
    the added DTO as transport
  • add DefaultUnionSQLBuilder as implementation of
    the added UnionSQLBuilder interface.
  • add AbstractPlatform::createUnionSQLBuilder() to
    retrieve the union SQL Builder instance.

Possible usages of the proposed implementation

// ---------------------------------------------------------------------------------------
// plain sql parts to build a `UNION ALL` query
// ---------------------------------------------------------------------------------------
$platform     = $this->connection->getDatabasePlatform();
$qb           = $this->connection->createQueryBuilder();
$select1      = $platform->getDummySelectSQL('2 as field_one');
$select2      = $platform->getDummySelectSQL('1 as field_one');
$select3      = $platform->getDummySelectSQL('4 as field_one');
$select4      = $platform->getDummySelectSQL('3 as field_one');
$qb->union($select1, $select2)
   ->addUnion(...[$select3, $select4])
   ->setMaxResults(1)->setFirstResult(1)->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
// ---------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------
// using QueryBuilder sub instances to build a `UNION ALL` query
// ---------------------------------------------------------------------------------------
$unionQueryBuilder = $this->connection->createQueryBuilder();
$namedParameter1 = $unionQueryBuilder->createNamedParameter(2, ParameterType::INTEGER);
$namedParameter2 = $unionQueryBuilder->createNamedParameter(1, ParameterType::INTEGER);

// sub query 1
$sub1 = $this->connection->createQueryBuilder();
$sub1->select('id')->from('a_table')
  ->where($unionQueryBuilder->expr()->eq('id', $namedParameter1));

// sub query 2
$sub2 = $this->connection->createQueryBuilder();
$sub2->select('id')->from('a_table')
    ->where($unionQueryBuilder->expr()->eq('id', $namedParameter2));

// execute union
$rows = $unionQueryBuilder
  ->union($sub1)
  ->addUnion($sub2)
  ->orderBy('id', 'ASC')
  ->executeQuery()
  ->fetchAllAssociative();
// ---------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------
// plain sql parts to build a `UNION <DISTINCT>` query
// ---------------------------------------------------------------------------------------
$platform     = $this->connection->getDatabasePlatform();
$qb           = $this->connection->createQueryBuilder();
$select1      = $platform->getDummySelectSQL('2 as field_one');
$select2      = $platform->getDummySelectSQL('1 as field_one');
$select3      = $platform->getDummySelectSQL('4 as field_one');
$select4      = $platform->getDummySelectSQL('3 as field_one');
$qb->union($select1, $select2)
   ->addUnion(...[$select3, $select4])
   ->distinct()
   ->setMaxResults(1)->setFirstResult(1)->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
// ---------------------------------------------------------------------------------------

Links

sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition by
adding `union(...)` and `addUnion(...)` methods following
the same logic as `select(...)` and `addSelect(...)` along
with introducing a new internal `QueryType::UNION`.

To avoid to much noise, the `distinct()` method and
property is reused to controll the distinctness of
the build union query. `UNION DISTICT` is the default
and the same `UNION`, which means that without using
`QueryBuilder::distinct()` duplicated rows may be
returned.

It has been consodered to reuse distinct over adding
a additional `union distict / unique rows) property
and method to controll that switch, albeit reverting
the default.

Additional consideration has been, to allow SQL strings
and QueryBuilder for `union()` and `addUnion()` and to
minimize the direct handling of miss-configuration to
the number (minimum of two union parts) and let other
issues like the field (order, count, naming) or not
allowed order by handling to the database itself. With
that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

This change ...

* adds internal `QueryType::UNION` enum case.
* adds `UnionSQLBuilder interface` and implement
  `DefaultUnionSQLBuilder`.
* adds `AbstractPlatform::createUnionSQLBuilder()`.
* adds public QueryBuilder methods `QueryBuilder::union()`,
  `QueryBuilder::addUnion()` and `QueryBuilder::uniqueRows()`.
* modifies `QueryBuilder::getSQL()` to build a UNION string,
  handled with private method `QueryBuilder::getUnionSQLString()`.
* adds unit and functional tests.
* modifies existing QueryBuilder documentation file.

Resolves doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition by
adding `union(...)` and `addUnion(...)` methods following
the same logic as `select(...)` and `addSelect(...)` along
with introducing a new internal `QueryType::UNION`.

To avoid to much noise, the `distinct()` method and
property is reused to controll the distinctness of
the build union query. `UNION DISTICT` is the default
and the same `UNION`, which means that without using
`QueryBuilder::distinct()` duplicated rows may be
returned.

It has been consodered to reuse distinct over adding
a additional `union distict / unique rows) property
and method to controll that switch, albeit reverting
the default.

Additional consideration has been, to allow SQL strings
and QueryBuilder for `union()` and `addUnion()` and to
minimize the direct handling of miss-configuration to
the number (minimum of two union parts) and let other
issues like the field (order, count, naming) or not
allowed order by handling to the database itself. With
that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

This change ...

* adds internal `QueryType::UNION` enum case.
* adds `UnionSQLBuilder interface` and implement
  `DefaultUnionSQLBuilder`.
* adds `AbstractPlatform::createUnionSQLBuilder()`.
* adds public QueryBuilder methods `QueryBuilder::union()`,
  `QueryBuilder::addUnion()` and `QueryBuilder::uniqueRows()`.
* modifies `QueryBuilder::getSQL()` to build a UNION string,
  handled with private method `QueryBuilder::getUnionSQLString()`.
* adds unit and functional tests.
* modifies existing QueryBuilder documentation file.

Resolves doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition by
adding `union(...)` and `addUnion(...)` methods following
the same logic as `select(...)` and `addSelect(...)` along
with introducing a new internal `QueryType::UNION`.

To avoid to much noise, the `distinct()` method and
property is reused to controll the distinctness of
the build union query. `UNION DISTICT` is the default
and the same `UNION`, which means that without using
`QueryBuilder::distinct()` duplicated rows may be
returned.

It has been consodered to reuse distinct over adding
a additional `union distict / unique rows) property
and method to controll that switch, albeit reverting
the default.

Additional consideration has been, to allow SQL strings
and QueryBuilder for `union()` and `addUnion()` and to
minimize the direct handling of miss-configuration to
the number (minimum of two union parts) and let other
issues like the field (order, count, naming) or not
allowed order by handling to the database itself. With
that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

This change ...

* adds internal `QueryType::UNION` enum case.
* adds `UnionSQLBuilder interface` and implement
  `DefaultUnionSQLBuilder`.
* adds `AbstractPlatform::createUnionSQLBuilder()`.
* adds public QueryBuilder methods `QueryBuilder::union()`,
  `QueryBuilder::addUnion()` and `QueryBuilder::uniqueRows()`.
* modifies `QueryBuilder::getSQL()` to build a UNION string,
  handled with private method `QueryBuilder::getUnionSQLString()`.
* adds unit and functional tests.
* modifies existing QueryBuilder documentation file.

Resolves doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition by
adding `union(...)` and `addUnion(...)` methods following
the same logic as `select(...)` and `addSelect(...)` along
with introducing a new internal `QueryType::UNION`.

To avoid to much noise, the `distinct()` method and
property is reused to controll the distinctness of
the build union query. `UNION DISTICT` is the default
and the same `UNION`, which means that without using
`QueryBuilder::distinct()` duplicated rows may be
returned.

It has been consodered to reuse distinct over adding
a additional `union distict / unique rows) property
and method to controll that switch, albeit reverting
the default.

Additional consideration has been, to allow SQL strings
and QueryBuilder for `union()` and `addUnion()` and to
minimize the direct handling of miss-configuration to
the number (minimum of two union parts) and let other
issues like the field (order, count, naming) or not
allowed order by handling to the database itself. With
that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

This change ...

* adds internal `QueryType::UNION` enum case.
* adds `UnionSQLBuilder interface` and implement
  `DefaultUnionSQLBuilder`.
* adds `AbstractPlatform::createUnionSQLBuilder()`.
* adds public QueryBuilder methods `QueryBuilder::union()`,
  `QueryBuilder::addUnion()` and `QueryBuilder::uniqueRows()`.
* modifies `QueryBuilder::getSQL()` to build a UNION string,
  handled with private method `QueryBuilder::getUnionSQLString()`.
* adds unit and functional tests.
* modifies existing QueryBuilder documentation file.

Resolves doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition now,
and following methods are added:

* `union(string|QueryBuilder ...$unionParts)` and
  `addUnion(string|QueryBuilder ...$unionParts)` to
   create a `UNION` query retrieving unique rows
* `unionAll(string|QueryBuilder ...$unionParts)` and
  `addUnionAll(string|QueryBuilder ...$unionParts) to
   create a `UNION ALL` query retrieving eventually
   duplicated rows.

This follows the generic logic as `select(...)` and
`addSelect(...)` along with introducing new internal
`QueryType::UNION_DISTINCT` and `QueryType::UNION_ALL`
enum cases.

Additional to the consideration to allow SQL strings
and QueryBuilder for `union()`, `unionAll(), `addUnion()`
and `addUnionAll()` and minimize the direct handling
of miss-configuration to the number of provided parts
and let other issues like the field (order, count, naming)
or not allowed order by handling to the database itself.
With that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

Unit and functional tests are added to demonstrate the
implementation and cover it for future changes.

Resolves: doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition now,
and following methods are added:

* `union(string|QueryBuilder ...$unionParts)` and
  `addUnion(string|QueryBuilder ...$unionParts)` to
   create a `UNION` query retrieving unique rows
* `unionAll(string|QueryBuilder ...$unionParts)` and
  `addUnionAll(string|QueryBuilder ...$unionParts) to
   create a `UNION ALL` query retrieving eventually
   duplicated rows.

This follows the generic logic as `select(...)` and
`addSelect(...)` along with introducing new internal
`QueryType::UNION_DISTINCT` and `QueryType::UNION_ALL`
enum cases.

Additional to the consideration to allow SQL strings
and QueryBuilder for `union()`, `unionAll(), `addUnion()`
and `addUnionAll()` and minimize the direct handling
of miss-configuration to the number of provided parts
and let other issues like the field (order, count, naming)
or not allowed order by handling to the database itself.
With that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

Unit and functional tests are added to demonstrate the
implementation and cover it for future changes.

Resolves: doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition now,
and following methods are added:

* `union(string|QueryBuilder ...$unionParts)` and
  `addUnion(string|QueryBuilder ...$unionParts)` to
   create a `UNION` query retrieving unique rows
* `unionAll(string|QueryBuilder ...$unionParts)` and
  `addUnionAll(string|QueryBuilder ...$unionParts) to
   create a `UNION ALL` query retrieving eventually
   duplicated rows.

This follows the generic logic as `select(...)` and
`addSelect(...)` along with introducing new internal
`QueryType::UNION_DISTINCT` and `QueryType::UNION_ALL`
enum cases.

Additional to the consideration to allow SQL strings
and QueryBuilder for `union()`, `unionAll(), `addUnion()`
and `addUnionAll()` and minimize the direct handling
of miss-configuration to the number of provided parts
and let other issues like the field (order, count, naming)
or not allowed order by handling to the database itself.
With that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

Unit and functional tests are added to demonstrate the
implementation and cover it for future changes.

Resolves: doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition now,
and following methods are added:

* `union(string|QueryBuilder ...$unionParts)` and
  `addUnion(string|QueryBuilder ...$unionParts)` to
   create a `UNION` query retrieving unique rows
* `unionAll(string|QueryBuilder ...$unionParts)` and
  `addUnionAll(string|QueryBuilder ...$unionParts) to
   create a `UNION ALL` query retrieving eventually
   duplicated rows.

This follows the generic logic as `select(...)` and
`addSelect(...)` along with introducing new internal
`QueryType::UNION_DISTINCT` and `QueryType::UNION_ALL`
enum cases.

Additional to the consideration to allow SQL strings
and QueryBuilder for `union()`, `unionAll(), `addUnion()`
and `addUnionAll()` and minimize the direct handling
of miss-configuration to the number of provided parts
and let other issues like the field (order, count, naming)
or not allowed order by handling to the database itself.
With that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

Unit and functional tests are added to demonstrate the
implementation and cover it for future changes.

Resolves: doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 25, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition now,
and following methods are added:

* `union(string|QueryBuilder ...$unionParts)` and
  `addUnion(string|QueryBuilder ...$unionParts)` to
   create a `UNION` query retrieving unique rows
* `unionAll(string|QueryBuilder ...$unionParts)` and
  `addUnionAll(string|QueryBuilder ...$unionParts) to
   create a `UNION ALL` query retrieving eventually
   duplicated rows.

This follows the generic logic as `select(...)` and
`addSelect(...)` along with introducing new internal
`QueryType::UNION_DISTINCT` and `QueryType::UNION_ALL`
enum cases.

Additional to the consideration to allow SQL strings
and QueryBuilder for `union()`, `unionAll(), `addUnion()`
and `addUnionAll()` and minimize the direct handling
of miss-configuration to the number of provided parts
and let other issues like the field (order, count, naming)
or not allowed order by handling to the database itself.
With that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

Unit and functional tests are added to demonstrate the
implementation and cover it for future changes.

Resolves: doctrine#6368
sbuerk added a commit to sbuerk/dbal that referenced this issue Apr 27, 2024
The `UNION` operator is used to combine the result-set
of two or more `SELECT` statements, which all database
vendors supports with usual specialities for each.

Still, there is a common shared subset which works for
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 common 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.

Taking the shared requirements and working behaviour,
it is possible to provide a generic support to the
QueryBuilder with a minimalistic surface addition now,
and following methods are added:

* `union(string|QueryBuilder ...$unionParts)` and
  `addUnion(string|QueryBuilder ...$unionParts)` to
   create a `UNION` query retrieving unique rows
* `unionAll(string|QueryBuilder ...$unionParts)` and
  `addUnionAll(string|QueryBuilder ...$unionParts) to
   create a `UNION ALL` query retrieving eventually
   duplicated rows.

This follows the generic logic as `select(...)` and
`addSelect(...)` along with introducing new internal
`QueryType::UNION_DISTINCT` and `QueryType::UNION_ALL`
enum cases.

Additional to the consideration to allow SQL strings
and QueryBuilder for `union()`, `unionAll(), `addUnion()`
and `addUnionAll()` and minimize the direct handling
of miss-configuration to the number of provided parts
and let other issues like the field (order, count, naming)
or not allowed order by handling to the database itself.
With that, vendor specific implementation can be done if
required.

Technically, the SQL build process is dispatched to a
`DefaultUnionSQLBuilder` along with an `UnionSQLBuilder`
interface, which also allows application to implement
custom behaviour if required.

Example:

```php
$platform = $connection->getDatabasePlatform();
$qb       = $>connection->createQueryBuilder();
$select10 = $platform->getDummySelectSQL('2 as field_one');
$select20 = $platform->getDummySelectSQL('1 as field_one');
$qb->union($select10,    /*$select11, $select12, ... */)
   ->addUnion($select20, /*$select21, $select22, ... */)
   ->setMaxResults(1)
   ->setFirstResult(1)
   ->orderBy('field_one', 'ASC');
$rows = $qb->executeQuery()->fetchAllAssociative();
```

Unit and functional tests are added to demonstrate the
implementation and cover it for future changes.

Resolves: doctrine#6368
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant