Skip to content

Commit

Permalink
rebase
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Pyzhikov <5071@mail.ru>
  • Loading branch information
iRedds committed May 25, 2022
1 parent 3711b49 commit a3ccae4
Show file tree
Hide file tree
Showing 8 changed files with 295 additions and 3 deletions.
65 changes: 63 additions & 2 deletions system/Database/BaseBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ class BaseBuilder
*/
public $QBOrderBy = [];

/**
* QB UNION data
*
* @var array<string>
*/
protected array $QBUnion = [];

/**
* QB NO ESCAPE data
*
Expand Down Expand Up @@ -1138,6 +1145,48 @@ protected function _like_statement(?string $prefix, string $column, ?string $not
return "{$prefix} {$column} {$not} LIKE :{$bind}:";
}

/**
* Add UNION statement
*
* @param BaseBuilder|Closure $union
*
* @return $this
*/
public function union($union)
{
return $this->addUnionStatement($union);
}

/**
* Add UNION ALL statement
*
* @param BaseBuilder|Closure $union
*
* @return $this
*/
public function unionAll($union)
{
return $this->addUnionStatement($union, true);
}

/**
* @used-by union()
* @used-by unionAll()
*
* @param BaseBuilder|Closure $union
*
* @return $this
*/
protected function addUnionStatement($union, bool $all = false)
{
$this->QBUnion[] = "\n" . 'UNION '
. ($all ? 'ALL ' : '')
. 'SELECT * FROM '
. $this->buildSubquery($union, true, 'uwrp' . (count($this->QBUnion) + 1));

return $this;
}

/**
* Starts a query group.
*
Expand Down Expand Up @@ -2427,10 +2476,10 @@ protected function compileSelect($selectOverride = false): string
. $this->compileOrderBy();

if ($this->QBLimit) {
return $this->_limit($sql . "\n");
$sql = $this->_limit($sql . "\n");
}

return $sql;
return $this->unionInjection($sql);
}

/**
Expand Down Expand Up @@ -2585,6 +2634,17 @@ protected function compileOrderBy(): string
return '';
}

protected function unionInjection(string $sql): string
{
if ($this->QBUnion === []) {
return $sql;
}

return 'SELECT * FROM (' . $sql . ') '
. ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers('uwrp0') : 'uwrp0')
. implode("\n", $this->QBUnion);
}

/**
* Takes an object as input and converts the class variables to array key/vals
*
Expand Down Expand Up @@ -2704,6 +2764,7 @@ protected function resetSelect()
'QBDistinct' => false,
'QBLimit' => false,
'QBOffset' => false,
'QBUnion' => [],
]);

if (! empty($this->db)) {
Expand Down
2 changes: 1 addition & 1 deletion system/Database/SQLSRV/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ protected function compileSelect($selectOverride = false): string
$sql = $this->_limit($sql . "\n");
}

return $sql;
return $this->unionInjection($sql);
}

/**
Expand Down
92 changes: 92 additions & 0 deletions tests/system/Database/Builder/UnionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Database\Builder;

use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\SQLSRV\Connection as SQLSRVConnection;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockConnection;

/**
* @internal
*/
final class UnionTest extends CIUnitTestCase
{
/**
* @var MockConnection
*/
protected $db;

protected function setUp(): void
{
parent::setUp();

$this->db = new MockConnection([]);
}

public function testUnion(): void
{
$expected = 'SELECT * FROM (SELECT * FROM "test") "uwrp0" UNION SELECT * FROM (SELECT * FROM "test") "uwrp1"';
$builder = $this->db->table('test');

$builder->union($this->db->table('test'));
$this->assertSame($expected, $this->buildSelect($builder));

$builder = $this->db->table('test');

$builder->union(static fn ($builder) => $builder->from('test'));
$this->assertSame($expected, $this->buildSelect($builder));
}

public function testUnionAll(): void
{
$expected = 'SELECT * FROM (SELECT * FROM "test") "uwrp0"'
. ' UNION ALL SELECT * FROM (SELECT * FROM "test") "uwrp1"';
$builder = $this->db->table('test');

$builder->unionAll($this->db->table('test'));
$this->assertSame($expected, $this->buildSelect($builder));
}

public function testOrderLimit(): void
{
$expected = 'SELECT * FROM (SELECT * FROM "test" ORDER BY "id" DESC LIMIT 10) "uwrp0"'
. ' UNION SELECT * FROM (SELECT * FROM "test") "uwrp1"';
$builder = $this->db->table('test');

$builder->union($this->db->table('test'))->limit(10)->orderBy('id', 'DESC');
$this->assertSame($expected, $this->buildSelect($builder));
}

public function testUnionSQLSRV(): void
{
$expected = 'SELECT * FROM (SELECT * FROM "test"."dbo"."users") "uwrp0"'
. ' UNION SELECT * FROM (SELECT * FROM "test"."dbo"."users") "uwrp1"';

$db = new SQLSRVConnection(['DBDriver' => 'SQLSRV', 'database' => 'test', 'schema' => 'dbo']);

$builder = $db->table('users');

$builder->union($db->table('users'));
$this->assertSame($expected, $this->buildSelect($builder));

$builder = $db->table('users');

$builder->union(static fn ($builder) => $builder->from('users'));
$this->assertSame($expected, $this->buildSelect($builder));
}

protected function buildSelect(BaseBuilder $builder): string
{
return str_replace("\n", ' ', $builder->getCompiledSelect());
}
}
62 changes: 62 additions & 0 deletions tests/system/Database/Live/UnionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\Database\Live;

use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use Tests\Support\Database\Seeds\CITestSeeder;

/**
* @group DatabaseLive
*
* @internal
*/
final class UnionTest extends CIUnitTestCase
{
use DatabaseTestTrait;

protected $refresh = true;
protected $seed = CITestSeeder::class;

public function testUnion(): void
{
$union = $this->db->table('user')
->limit(1)
->orderBy('id', 'ASC');
$builder = $this->db->table('user');

$builder->union($union)
->limit(1)
->orderBy('id', 'DESC');

$result = $this->db->newQuery()
->fromSubquery($builder, 'q')
->orderBy('id', 'DESC')
->get();

$this->assertSame(2, $result->getNumRows());

$rows = $result->getResult();
$this->assertSame(4, (int) $rows[0]->id);
$this->assertSame(1, (int) $rows[1]->id);
}

public function testUnionAll(): void
{
$union = $this->db->table('user');
$builder = $this->db->table('user');

$result = $builder->unionAll($union)->get();

$this->assertSame(8, $result->getNumRows());
}
}
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.2.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Database
- Added the class ``CodeIgniter\Database\RawSql`` which expresses raw SQL strings.
- :ref:`select() <query-builder-select-rawsql>`, :ref:`where() <query-builder-where-rawsql>`, :ref:`like() <query-builder-like-rawsql>`, :ref:`join() <query-builder-join-rawsql>` accept the ``CodeIgniter\Database\RawSql`` instance.
- ``DBForge::addField()`` default value raw SQL string support. See :ref:`forge-addfield-default-value-rawsql`.
- QueryBuilder. Union queries. See :ref:`query-builder-union`.

Others
======
Expand Down
51 changes: 51 additions & 0 deletions user_guide_src/source/database/query_builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,41 @@ As is in ``countAllResult()`` method, this method resets any field values that y
to ``select()`` as well. If you need to keep them, you can pass ``false`` as the
first parameter.

.. _query-builder-union:

*************
Union queries
*************

Union
=====

$builder->union()
-----------------

Is used to combine the result-set of two or more SELECT statements.

.. literalinclude:: query_builder/103.php

.. note:: For correct work with DBMS (such as MSSQL and Oracle) queries are wrapped in ``SELECT * FROM ( ... ) alias``
The main query will always have an alias of ``uwrp0``. Each subsequent query added via ``union()`` will have an
alias ``uwrpN+1``.

All union queries will be added after the main query, regardless of the order in which the ``union()`` method was
called. That is, the ``limit()`` or ``orderBy()`` methods will be relative to the main query, even if called after
``union()``.

In some cases, it may be necessary, for example, to sort or limit the number of records of the query result.
The solution is to use the wrapper created via ``$db->newQuery()``.
In the example below, we get the first 5 users + the last 5 users and sort the result by id:

.. literalinclude:: query_builder/104.php

$builder->unionAll()
--------------------

The behavior is the same as the ``union()`` method.

**************
Query grouping
**************
Expand Down Expand Up @@ -1495,6 +1530,22 @@ Class Reference

Adds an ``OFFSET`` clause to a query.

.. php:method:: union($union)
:param BaseBulder|Closure $union: Union query
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

Adds a ``UNION`` clause.

.. php:method:: unionAll($union)
:param BaseBulder|Closure $union: Union query
:returns: ``BaseBuilder`` instance (method chaining)
:rtype: ``BaseBuilder``

Adds a ``UNION ALL`` clause.

.. php:method:: set($key[, $value = ''[, $escape = null]])
:param mixed $key: Field name, or an array of field/value pairs
Expand Down
11 changes: 11 additions & 0 deletions user_guide_src/source/database/query_builder/103.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

$union = $this->db->table('users')->select('id', 'name');
$builder = $this->db->table('users')->select('id', 'name');

$builder->union($union)->limit(10)->get();
/*
* Produces:
* SELECT * FROM (SELECT `id`, `name` FROM `users` LIMIT 10) uwrp0
* UNION SELECT * FROM (SELECT `id`, `name` FROM `users`) uwrp1
*/
14 changes: 14 additions & 0 deletions user_guide_src/source/database/query_builder/104.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

$union = $this->db->table('users')->select('id', 'name')->orderBy('id', 'DESC')->limit(5);
$builder = $this->db->table('users')->select('id', 'name')->orderBy('id', 'ASC')->limit(5)->union($union);

$this->db->newQuery()->fromSubquery($builder, 'q')->orderBy('id', 'DESC')->get();
/*
* Produces:
* SELECT * FROM (
* SELECT * FROM (SELECT `id`, `name` FROM `users` ORDER BY `id` ASC LIMIT 5) uwrp0
* UNION
* SELECT * FROM (SELECT `id`, `name` FROM `users` ORDER BY `id` DESC LIMIT 5) uwrp1
* ) q ORDER BY `id` DESC
*/

0 comments on commit a3ccae4

Please sign in to comment.