Skip to content

Commit c6ace5d

Browse files
authored
Merge pull request #6015 from iRedds/feature/query-builder-union
Feature. QueryBuilder. Query union.
2 parents 75d4ed4 + fd013cd commit c6ace5d

8 files changed

Lines changed: 295 additions & 3 deletions

File tree

system/Database/BaseBuilder.php

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ class BaseBuilder
109109
*/
110110
public $QBOrderBy = [];
111111

112+
/**
113+
* QB UNION data
114+
*
115+
* @var array<string>
116+
*/
117+
protected array $QBUnion = [];
118+
112119
/**
113120
* QB NO ESCAPE data
114121
*
@@ -1138,6 +1145,48 @@ protected function _like_statement(?string $prefix, string $column, ?string $not
11381145
return "{$prefix} {$column} {$not} LIKE :{$bind}:";
11391146
}
11401147

1148+
/**
1149+
* Add UNION statement
1150+
*
1151+
* @param BaseBuilder|Closure $union
1152+
*
1153+
* @return $this
1154+
*/
1155+
public function union($union)
1156+
{
1157+
return $this->addUnionStatement($union);
1158+
}
1159+
1160+
/**
1161+
* Add UNION ALL statement
1162+
*
1163+
* @param BaseBuilder|Closure $union
1164+
*
1165+
* @return $this
1166+
*/
1167+
public function unionAll($union)
1168+
{
1169+
return $this->addUnionStatement($union, true);
1170+
}
1171+
1172+
/**
1173+
* @used-by union()
1174+
* @used-by unionAll()
1175+
*
1176+
* @param BaseBuilder|Closure $union
1177+
*
1178+
* @return $this
1179+
*/
1180+
protected function addUnionStatement($union, bool $all = false)
1181+
{
1182+
$this->QBUnion[] = "\n" . 'UNION '
1183+
. ($all ? 'ALL ' : '')
1184+
. 'SELECT * FROM '
1185+
. $this->buildSubquery($union, true, 'uwrp' . (count($this->QBUnion) + 1));
1186+
1187+
return $this;
1188+
}
1189+
11411190
/**
11421191
* Starts a query group.
11431192
*
@@ -2427,10 +2476,10 @@ protected function compileSelect($selectOverride = false): string
24272476
. $this->compileOrderBy();
24282477

24292478
if ($this->QBLimit) {
2430-
return $this->_limit($sql . "\n");
2479+
$sql = $this->_limit($sql . "\n");
24312480
}
24322481

2433-
return $sql;
2482+
return $this->unionInjection($sql);
24342483
}
24352484

24362485
/**
@@ -2585,6 +2634,17 @@ protected function compileOrderBy(): string
25852634
return '';
25862635
}
25872636

2637+
protected function unionInjection(string $sql): string
2638+
{
2639+
if ($this->QBUnion === []) {
2640+
return $sql;
2641+
}
2642+
2643+
return 'SELECT * FROM (' . $sql . ') '
2644+
. ($this->db->protectIdentifiers ? $this->db->escapeIdentifiers('uwrp0') : 'uwrp0')
2645+
. implode("\n", $this->QBUnion);
2646+
}
2647+
25882648
/**
25892649
* Takes an object as input and converts the class variables to array key/vals
25902650
*
@@ -2704,6 +2764,7 @@ protected function resetSelect()
27042764
'QBDistinct' => false,
27052765
'QBLimit' => false,
27062766
'QBOffset' => false,
2767+
'QBUnion' => [],
27072768
]);
27082769

27092770
if (! empty($this->db)) {

system/Database/SQLSRV/Builder.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ protected function compileSelect($selectOverride = false): string
597597
$sql = $this->_limit($sql . "\n");
598598
}
599599

600-
return $sql;
600+
return $this->unionInjection($sql);
601601
}
602602

603603
/**
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Database\Builder;
13+
14+
use CodeIgniter\Database\BaseBuilder;
15+
use CodeIgniter\Database\SQLSRV\Connection as SQLSRVConnection;
16+
use CodeIgniter\Test\CIUnitTestCase;
17+
use CodeIgniter\Test\Mock\MockConnection;
18+
19+
/**
20+
* @internal
21+
*/
22+
final class UnionTest extends CIUnitTestCase
23+
{
24+
/**
25+
* @var MockConnection
26+
*/
27+
protected $db;
28+
29+
protected function setUp(): void
30+
{
31+
parent::setUp();
32+
33+
$this->db = new MockConnection([]);
34+
}
35+
36+
public function testUnion(): void
37+
{
38+
$expected = 'SELECT * FROM (SELECT * FROM "test") "uwrp0" UNION SELECT * FROM (SELECT * FROM "test") "uwrp1"';
39+
$builder = $this->db->table('test');
40+
41+
$builder->union($this->db->table('test'));
42+
$this->assertSame($expected, $this->buildSelect($builder));
43+
44+
$builder = $this->db->table('test');
45+
46+
$builder->union(static fn ($builder) => $builder->from('test'));
47+
$this->assertSame($expected, $this->buildSelect($builder));
48+
}
49+
50+
public function testUnionAll(): void
51+
{
52+
$expected = 'SELECT * FROM (SELECT * FROM "test") "uwrp0"'
53+
. ' UNION ALL SELECT * FROM (SELECT * FROM "test") "uwrp1"';
54+
$builder = $this->db->table('test');
55+
56+
$builder->unionAll($this->db->table('test'));
57+
$this->assertSame($expected, $this->buildSelect($builder));
58+
}
59+
60+
public function testOrderLimit(): void
61+
{
62+
$expected = 'SELECT * FROM (SELECT * FROM "test" ORDER BY "id" DESC LIMIT 10) "uwrp0"'
63+
. ' UNION SELECT * FROM (SELECT * FROM "test") "uwrp1"';
64+
$builder = $this->db->table('test');
65+
66+
$builder->union($this->db->table('test'))->limit(10)->orderBy('id', 'DESC');
67+
$this->assertSame($expected, $this->buildSelect($builder));
68+
}
69+
70+
public function testUnionSQLSRV(): void
71+
{
72+
$expected = 'SELECT * FROM (SELECT * FROM "test"."dbo"."users") "uwrp0"'
73+
. ' UNION SELECT * FROM (SELECT * FROM "test"."dbo"."users") "uwrp1"';
74+
75+
$db = new SQLSRVConnection(['DBDriver' => 'SQLSRV', 'database' => 'test', 'schema' => 'dbo']);
76+
77+
$builder = $db->table('users');
78+
79+
$builder->union($db->table('users'));
80+
$this->assertSame($expected, $this->buildSelect($builder));
81+
82+
$builder = $db->table('users');
83+
84+
$builder->union(static fn ($builder) => $builder->from('users'));
85+
$this->assertSame($expected, $this->buildSelect($builder));
86+
}
87+
88+
protected function buildSelect(BaseBuilder $builder): string
89+
{
90+
return str_replace("\n", ' ', $builder->getCompiledSelect());
91+
}
92+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
/**
4+
* This file is part of CodeIgniter 4 framework.
5+
*
6+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace CodeIgniter\Database\Live;
13+
14+
use CodeIgniter\Test\CIUnitTestCase;
15+
use CodeIgniter\Test\DatabaseTestTrait;
16+
use Tests\Support\Database\Seeds\CITestSeeder;
17+
18+
/**
19+
* @group DatabaseLive
20+
*
21+
* @internal
22+
*/
23+
final class UnionTest extends CIUnitTestCase
24+
{
25+
use DatabaseTestTrait;
26+
27+
protected $refresh = true;
28+
protected $seed = CITestSeeder::class;
29+
30+
public function testUnion(): void
31+
{
32+
$union = $this->db->table('user')
33+
->limit(1)
34+
->orderBy('id', 'ASC');
35+
$builder = $this->db->table('user');
36+
37+
$builder->union($union)
38+
->limit(1)
39+
->orderBy('id', 'DESC');
40+
41+
$result = $this->db->newQuery()
42+
->fromSubquery($builder, 'q')
43+
->orderBy('id', 'DESC')
44+
->get();
45+
46+
$this->assertSame(2, $result->getNumRows());
47+
48+
$rows = $result->getResult();
49+
$this->assertSame(4, (int) $rows[0]->id);
50+
$this->assertSame(1, (int) $rows[1]->id);
51+
}
52+
53+
public function testUnionAll(): void
54+
{
55+
$union = $this->db->table('user');
56+
$builder = $this->db->table('user');
57+
58+
$result = $builder->unionAll($union)->get();
59+
60+
$this->assertSame(8, $result->getNumRows());
61+
}
62+
}

user_guide_src/source/changelogs/v4.2.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Database
7474
- Added the class ``CodeIgniter\Database\RawSql`` which expresses raw SQL strings.
7575
- :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.
7676
- ``DBForge::addField()`` default value raw SQL string support. See :ref:`forge-addfield-default-value-rawsql`.
77+
- QueryBuilder. Union queries. See :ref:`query-builder-union`.
7778

7879
Others
7980
======

user_guide_src/source/database/query_builder.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,41 @@ As is in ``countAllResult()`` method, this method resets any field values that y
679679
to ``select()`` as well. If you need to keep them, you can pass ``false`` as the
680680
first parameter.
681681

682+
.. _query-builder-union:
683+
684+
*************
685+
Union queries
686+
*************
687+
688+
Union
689+
=====
690+
691+
$builder->union()
692+
-----------------
693+
694+
Is used to combine the result-set of two or more SELECT statements. It will return only the unique results.
695+
696+
.. literalinclude:: query_builder/103.php
697+
698+
.. note:: For correct work with DBMS (such as MSSQL and Oracle) queries are wrapped in ``SELECT * FROM ( ... ) alias``
699+
The main query will always have an alias of ``uwrp0``. Each subsequent query added via ``union()`` will have an
700+
alias ``uwrpN+1``.
701+
702+
All union queries will be added after the main query, regardless of the order in which the ``union()`` method was
703+
called. That is, the ``limit()`` or ``orderBy()`` methods will be relative to the main query, even if called after
704+
``union()``.
705+
706+
In some cases, it may be necessary, for example, to sort or limit the number of records of the query result.
707+
The solution is to use the wrapper created via ``$db->newQuery()``.
708+
In the example below, we get the first 5 users + the last 5 users and sort the result by id:
709+
710+
.. literalinclude:: query_builder/104.php
711+
712+
$builder->unionAll()
713+
--------------------
714+
715+
The behavior is the same as the ``union()`` method. However, all results will be returned, not just the unique ones.
716+
682717
**************
683718
Query grouping
684719
**************
@@ -1495,6 +1530,22 @@ Class Reference
14951530

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

1533+
.. php:method:: union($union)
1534+
1535+
:param BaseBulder|Closure $union: Union query
1536+
:returns: ``BaseBuilder`` instance (method chaining)
1537+
:rtype: ``BaseBuilder``
1538+
1539+
Adds a ``UNION`` clause.
1540+
1541+
.. php:method:: unionAll($union)
1542+
1543+
:param BaseBulder|Closure $union: Union query
1544+
:returns: ``BaseBuilder`` instance (method chaining)
1545+
:rtype: ``BaseBuilder``
1546+
1547+
Adds a ``UNION ALL`` clause.
1548+
14981549
.. php:method:: set($key[, $value = ''[, $escape = null]])
14991550
15001551
:param mixed $key: Field name, or an array of field/value pairs
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
$union = $this->db->table('users')->select('id', 'name');
4+
$builder = $this->db->table('users')->select('id', 'name');
5+
6+
$builder->union($union)->limit(10)->get();
7+
/*
8+
* Produces:
9+
* SELECT * FROM (SELECT `id`, `name` FROM `users` LIMIT 10) uwrp0
10+
* UNION SELECT * FROM (SELECT `id`, `name` FROM `users`) uwrp1
11+
*/
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
$union = $this->db->table('users')->select('id', 'name')->orderBy('id', 'DESC')->limit(5);
4+
$builder = $this->db->table('users')->select('id', 'name')->orderBy('id', 'ASC')->limit(5)->union($union);
5+
6+
$this->db->newQuery()->fromSubquery($builder, 'q')->orderBy('id', 'DESC')->get();
7+
/*
8+
* Produces:
9+
* SELECT * FROM (
10+
* SELECT * FROM (SELECT `id`, `name` FROM `users` ORDER BY `id` ASC LIMIT 5) uwrp0
11+
* UNION
12+
* SELECT * FROM (SELECT `id`, `name` FROM `users` ORDER BY `id` DESC LIMIT 5) uwrp1
13+
* ) q ORDER BY `id` DESC
14+
*/

0 commit comments

Comments
 (0)