From 7b29866e4b411b1f4c074175215e5c2b3d9e43dd Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Tue, 16 Feb 2021 06:12:19 +0800 Subject: [PATCH 1/5] New: BaseBuilder. Added methods that implement UNION. --- system/Database/BaseBuilder.php | 142 +++++++++++++-- tests/system/Database/Builder/UnionTest.php | 171 ++++++++++++++++++ .../source/database/query_builder.rst | 94 ++++++++++ 3 files changed, 396 insertions(+), 11 deletions(-) create mode 100644 tests/system/Database/Builder/UnionTest.php diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 610fe8e54528..53a7dcdb9f98 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -139,6 +139,20 @@ class BaseBuilder */ protected $QBWhereGroupCount = 0; + /** + * Collection of union queries + * + * @var string[] + */ + protected $QBUnion = []; + + /** + * UNION ORDER BY data + * + * @var array + */ + protected $QBUnionOrderBy = []; + /** * Ignore data that cause certain * exceptions, for example in case of @@ -3022,7 +3036,14 @@ protected function compileSelect($selectOverride = false): string // LIMIT if ($this->QBLimit) { - return $this->_limit($sql . "\n"); + $sql = $this->_limit($sql . "\n"); + } + + if ($this->QBUnion) + { + $sql = '(' . $sql . ')' + . $this->compileUnion() + . $this->compileUnionOrderBy(); } return $sql; @@ -3358,16 +3379,18 @@ protected function resetRun(array $qbResetItems) protected function resetSelect() { $this->resetRun([ - 'QBSelect' => [], - 'QBJoin' => [], - 'QBWhere' => [], - 'QBGroupBy' => [], - 'QBHaving' => [], - 'QBOrderBy' => [], - 'QBNoEscape' => [], - 'QBDistinct' => false, - 'QBLimit' => false, - 'QBOffset' => false, + 'QBSelect' => [], + 'QBJoin' => [], + 'QBWhere' => [], + 'QBGroupBy' => [], + 'QBHaving' => [], + 'QBOrderBy' => [], + 'QBNoEscape' => [], + 'QBUnion' => [], + 'QBUnionOrderBy' => [], + 'QBDistinct' => false, + 'QBLimit' => false, + 'QBOffset' => false, ]); if (! empty($this->db)) @@ -3492,6 +3515,103 @@ protected function setBind(string $key, $value = null, bool $escape = true): str return $key . $count; } + /** + * Union queries + * + * @param Closure $closure Contains a query to combine + * + * @return static + */ + public function union(Closure $closure) + { + return $this->unionBuilder($closure); + } + + /** + * Union queries + * + * @param Closure $closure Contains a query to combine + * + * @return static + */ + public function unionAll(Closure $closure) + { + return $this->unionBuilder($closure, true); + } + + /** + * Union query builder + * + * @param Closure $closure Contains a query to combine + * @param boolean $all Using UNION ALL construction + * + * @return static + */ + protected function unionBuilder(Closure $closure, bool $all = false) + { + $builder = $closure($this->cleanClone()); + + if (! ($builder instanceof BaseBuilder)) + { + throw new DatabaseException( + 'BaseBuilder::union(). The closure must return an instance of the BaseBuilder class' + ); + } + $sql = $builder->getCompiledSelect(); + + $all = $all ? 'ALL ' : ''; + + $this->QBUnion[] = ' UNION ' . $all . '(' . $sql . ')'; + + return $this; + } + + /** + * Compile UNION queries + * + * @return string + */ + protected function compileUnion() : string + { + return count($this->QBUnion) ? join('', $this->QBUnion) : ''; + } + + /** + * ORDER BY + * + * @param string $orderBy Field + * @param string $direction ASC, DESC or RANDOM + * @param boolean $escape Escape + * + * @return static + */ + public function unionOrderBy(string $orderBy, string $direction = '', bool $escape = null) + { + $this->QBUnionOrderBy[] = [ + $orderBy, + $direction, + $escape, + ]; + + return $this; + } + + /** + * Compile UNION ORDER BY data + * + * @return string + */ + protected function compileUnionOrderBy() : string + { + $builder = $this->cleanClone(); + foreach ($this->QBUnionOrderBy as $order) + { + $builder->orderBy(...$order); + } + + return $builder->compileOrderBy(); + } + //-------------------------------------------------------------------- /** diff --git a/tests/system/Database/Builder/UnionTest.php b/tests/system/Database/Builder/UnionTest.php new file mode 100644 index 000000000000..52c97104cf97 --- /dev/null +++ b/tests/system/Database/Builder/UnionTest.php @@ -0,0 +1,171 @@ +db = new MockConnection([]); + } + + public function testUnion() + { + $builder = $this->db->table('movies'); + + $builder->select('title, year') + ->union(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }); + + $sql = '(SELECT "title", "year" FROM "movies") UNION (SELECT "title", "year" FROM "top_movies")'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testUnionExceptions() + { + $builder = $this->db->table('movies'); + $builder->select('title, year'); + + $this->expectException(\TypeError::class); + + $builder->union(1); + + $this->expectException(DatabaseException::class); + $this->expectExceptionMessage( + 'BaseBuilder::union(). The closure must return an instance of the BaseBuilder class' + ); + + $builder->union(function (BaseBuilder $builder) { + $builder->select('title, year')->from('top_movies'); + }); + } + + public function testUnionAll() + { + $builder = $this->db->table('movies'); + + $builder->select('title, year') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }); + + $sql = '(SELECT "title", "year" FROM "movies") UNION ALL (SELECT "title", "year" FROM "top_movies")'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testMultiQueryUnion() + { + $builder = $this->db->table('movies'); + + $builder->select('title, year') + ->union(function (BaseBuilder $builder) { + return $builder->select('title, year') + ->from('top_movies') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('tomato_movies'); + }); + }); + + $sql = '(SELECT "title", "year" FROM "movies") ' + . 'UNION ((SELECT "title", "year" FROM "top_movies") ' + . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies"))'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + + $builder->resetQuery(); + + $builder->select('title, year') + ->union(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + })->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('tomato_movies'); + }); + + $sql = '(SELECT "title", "year" FROM "movies") ' + . 'UNION (SELECT "title", "year" FROM "top_movies") ' + . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies")'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + + $builder->resetQuery(); + + $year1 = 2000; + $year2 = 2010; + $year3 = 2020; + + $builder->select('title, year') + ->where('year', $year1) + ->union(function (BaseBuilder $builder) use ($year2, $year3) { + return $builder->select('title, year') + ->from('top_movies') + ->where('year', $year2) + ->unionAll(function (BaseBuilder $builder) use ($year3) { + return $builder->select('title, year') + ->from('tomato_movies') + ->where('year', $year3); + }); + }); + + $sql = '(SELECT "title", "year" FROM "movies" WHERE "year" = 2000) ' + . 'UNION ((SELECT "title", "year" FROM "top_movies" WHERE "year" = 2010) ' + . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies" WHERE "year" = 2020))'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testUnionOrderBy() + { + $builder = $this->db->table('movies'); + + $builder->select('title, year') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }) + ->unionOrderBy('title', 'DESC') + ->unionOrderBy('year', 'ASC'); + + $sql = '(SELECT "title", "year" FROM "movies") ' + . 'UNION ALL (SELECT "title", "year" FROM "top_movies") ORDER BY "title" DESC, "year" ASC'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + + $builder->resetQuery(); + + $builder->select('title, year') + ->union(function (BaseBuilder $builder) { + return $builder->select('title, year') + ->from('top_movies') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('tomato_movies'); + })->unionOrderBy('title', 'DESC'); + }); + + $sql = '(SELECT "title", "year" FROM "movies") ' + . 'UNION ((SELECT "title", "year" FROM "top_movies") ' + . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies") ORDER BY "title" DESC)'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testIgnoreUnionOrderByWithoutUnionQuery() + { + $builder = $this->db->table('movies'); + + $builder->select('title, year')->unionOrderBy('year', 'ASC'); + + $sql = 'SELECT "title", "year" FROM "movies"'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + } +} diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index e0fb96c8d68f..df28955fb1ac 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -839,6 +839,100 @@ Starts a new group by adding an opening parenthesis to the HAVING clause of the Ends the current group by adding a closing parenthesis to the HAVING clause of the query. +************************* +Combining queries (UNION) +************************* + +.. note:: To understand how UNION works, it is recommended to study the documentation for the used DBMS. + +**$builder->union()** + +The method combines the results of several queries into one common one and takes a Closure as an argument. +The Closure must return an instance of the BaseBuilder class:: + + //Add the following line after the namespace keyword + use CodeIgniter\Database\BaseBuilder; + + $builder = $db->table('movies'); + + $builder->select('title, year') + ->union(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }) + ->get(); + + // (SELECT title, year FROM movies) UNION (SELECT title, year FROM top_movies) + +Nested UNION and variable passing:: + + //Add the following line after the namespace keyword + use CodeIgniter\Database\BaseBuilder; + + $year = 2000; + $yearTop = 2010; + $yearBad = 2020; + + $builder = $db->table('movies'); + + $builder->select('title, year') + ->where('year', $year) + ->union(function (BaseBuilder $builder) use ($yearTop, $yearBad){ + return $builder->select('title, year') + ->from('top_movies') + ->where('year', $yearTop) + ->union(function (BaseBuilder $builder) use ($yearBad){ + return $builder->select('title, year') + ->from('bad_movies') + ->where('year', $yearBad) + }); + }) + ->get(); + + // (SELECT title, year FROM movies WHERE year = 2000) + // UNION ( + // (SELECT title, year FROM top_movies WHERE year = 2010) + // UNION (SELECT title, year FROM bad_movies WHERE year = 2020) + // ) + + +**$builder->unionAll()** + +The method adds ALL to UNION:: + + //Add the following line after the namespace keyword + use CodeIgniter\Database\BaseBuilder; + + $builder = $db->table('movies'); + + $builder->select('title, year') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }) + ->get(); + + // (SELECT title, year FROM movies) UNION ALL (SELECT title, year FROM top_movies) + + +**$builder->unionOrderBy()** + +The method adds a sort for the final query result after combining queries using UNION [ALL]. +The principle is the same as for ``orderBy()``:: + + //Add the following line after the namespace keyword + use CodeIgniter\Database\BaseBuilder; + + $builder = $db->table('movies'); + + $builder->select('title, year') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }) + ->unionOrderBy('title', 'DESC') + ->get(); + + // (SELECT title, year FROM movies) UNION ALL (SELECT title, year FROM top_movies) ORDER BY title DESC + + ************** Inserting Data ************** From 4ddf1d800afb4e211217ea055ac8ad8f32edc387 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 17 Feb 2021 12:57:58 +0800 Subject: [PATCH 2/5] New: BaseBuilder. Added methods that implement UNION. LiveTest --- system/Database/BaseBuilder.php | 22 ++++- system/Database/SQLSRV/Builder.php | 12 ++- tests/system/Database/Builder/UnionTest.php | 96 ++++++------------- tests/system/Database/Live/UnionTest.php | 39 ++++++++ .../source/database/query_builder.rst | 45 ++------- 5 files changed, 106 insertions(+), 108 deletions(-) create mode 100644 tests/system/Database/Live/UnionTest.php diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 53a7dcdb9f98..463c4b5fc404 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -3041,9 +3041,12 @@ protected function compileSelect($selectOverride = false): string if ($this->QBUnion) { - $sql = '(' . $sql . ')' - . $this->compileUnion() - . $this->compileUnionOrderBy(); + if ($this->QBOrderBy || $this->QBLimit) + { + $sql = 'SELECT * FROM (' . $sql . ') as wrapper_alias'; + } + + $sql .= $this->compileUnion() . $this->compileUnionOrderBy(); } return $sql; @@ -3557,11 +3560,20 @@ protected function unionBuilder(Closure $closure, bool $all = false) 'BaseBuilder::union(). The closure must return an instance of the BaseBuilder class' ); } - $sql = $builder->getCompiledSelect(); + + $sql = $builder->getCompiledSelect(false); $all = $all ? 'ALL ' : ''; - $this->QBUnion[] = ' UNION ' . $all . '(' . $sql . ')'; + if (($builder->QBOrderBy || $builder->QBLimit || $builder->QBUnionOrderBy) + && strpos($sql, 'wrapper_alias UNION') === false) + { + $sql = 'SELECT * FROM (' . $sql . ') as wrapper_alias'; + } + + $builder->resetSelect(); + + $this->QBUnion[] = ' UNION ' . $all . $sql; return $this; } diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index af914bf26f59..6fdf7e7ea856 100755 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -651,7 +651,17 @@ protected function compileSelect($selectOverride = false): string // LIMIT if ($this->QBLimit) { - return $sql = $this->_limit($sql . "\n"); + $sql = $this->_limit($sql . "\n"); + } + + if ($this->QBUnion) + { + if ($this->QBOrderBy || $this->QBLimit) + { + $sql = 'SELECT * FROM (' . $sql . ') as wrapper_alias'; + } + + $sql .= $this->compileUnion() . $this->compileUnionOrderBy(); } return $sql; diff --git a/tests/system/Database/Builder/UnionTest.php b/tests/system/Database/Builder/UnionTest.php index 52c97104cf97..fabc677b48a7 100644 --- a/tests/system/Database/Builder/UnionTest.php +++ b/tests/system/Database/Builder/UnionTest.php @@ -22,11 +22,13 @@ public function testUnion() $builder = $this->db->table('movies'); $builder->select('title, year') + ->orderBy('title') ->union(function (BaseBuilder $builder) { return $builder->select('title, year')->from('top_movies'); }); - $sql = '(SELECT "title", "year" FROM "movies") UNION (SELECT "title", "year" FROM "top_movies")'; + $sql = 'SELECT * FROM (SELECT "title", "year" FROM "movies" ORDER BY "title") as wrapper_alias ' + . 'UNION SELECT "title", "year" FROM "top_movies"'; $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); } @@ -55,71 +57,15 @@ public function testUnionAll() $builder = $this->db->table('movies'); $builder->select('title, year') + ->orderBy('title') ->unionAll(function (BaseBuilder $builder) { - return $builder->select('title, year')->from('top_movies'); - }); - - $sql = '(SELECT "title", "year" FROM "movies") UNION ALL (SELECT "title", "year" FROM "top_movies")'; - - $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); - } - - public function testMultiQueryUnion() - { - $builder = $this->db->table('movies'); - - $builder->select('title, year') - ->union(function (BaseBuilder $builder) { return $builder->select('title, year') ->from('top_movies') - ->unionAll(function (BaseBuilder $builder) { - return $builder->select('title, year')->from('tomato_movies'); - }); - }); - - $sql = '(SELECT "title", "year" FROM "movies") ' - . 'UNION ((SELECT "title", "year" FROM "top_movies") ' - . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies"))'; - - $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); - - $builder->resetQuery(); - - $builder->select('title, year') - ->union(function (BaseBuilder $builder) { - return $builder->select('title, year')->from('top_movies'); - })->unionAll(function (BaseBuilder $builder) { - return $builder->select('title, year')->from('tomato_movies'); + ->orderBy('title'); }); - $sql = '(SELECT "title", "year" FROM "movies") ' - . 'UNION (SELECT "title", "year" FROM "top_movies") ' - . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies")'; - - $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); - - $builder->resetQuery(); - - $year1 = 2000; - $year2 = 2010; - $year3 = 2020; - - $builder->select('title, year') - ->where('year', $year1) - ->union(function (BaseBuilder $builder) use ($year2, $year3) { - return $builder->select('title, year') - ->from('top_movies') - ->where('year', $year2) - ->unionAll(function (BaseBuilder $builder) use ($year3) { - return $builder->select('title, year') - ->from('tomato_movies') - ->where('year', $year3); - }); - }); - - $sql = '(SELECT "title", "year" FROM "movies" WHERE "year" = 2000) ' - . 'UNION ((SELECT "title", "year" FROM "top_movies" WHERE "year" = 2010) ' - . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies" WHERE "year" = 2020))'; + $sql = 'SELECT * FROM (SELECT "title", "year" FROM "movies" ORDER BY "title") as wrapper_alias ' + . 'UNION ALL SELECT * FROM (SELECT "title", "year" FROM "top_movies" ORDER BY "title") as wrapper_alias'; $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); } @@ -135,8 +81,8 @@ public function testUnionOrderBy() ->unionOrderBy('title', 'DESC') ->unionOrderBy('year', 'ASC'); - $sql = '(SELECT "title", "year" FROM "movies") ' - . 'UNION ALL (SELECT "title", "year" FROM "top_movies") ORDER BY "title" DESC, "year" ASC'; + $sql = 'SELECT "title", "year" FROM "movies" ' + . 'UNION ALL SELECT "title", "year" FROM "top_movies" ORDER BY "title" DESC, "year" ASC'; $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); @@ -149,11 +95,27 @@ public function testUnionOrderBy() ->unionAll(function (BaseBuilder $builder) { return $builder->select('title, year')->from('tomato_movies'); })->unionOrderBy('title', 'DESC'); - }); + })->unionOrderBy('title'); + + $sql = 'SELECT "title", "year" FROM "movies" ' + . 'UNION SELECT * FROM (SELECT "title", "year" FROM "top_movies" ' + . 'UNION ALL SELECT "title", "year" FROM "tomato_movies" ORDER BY "title" DESC) as wrapper_alias ORDER BY "title"'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + + $builder->select('title, year') + ->union(function (BaseBuilder $builder) { + return $builder->select('title, year') + ->from('top_movies') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('tomato_movies'); + })->limit(1); + }) + ->unionOrderBy('title'); - $sql = '(SELECT "title", "year" FROM "movies") ' - . 'UNION ((SELECT "title", "year" FROM "top_movies") ' - . 'UNION ALL (SELECT "title", "year" FROM "tomato_movies") ORDER BY "title" DESC)'; + $sql = 'SELECT "title", "year" FROM "movies" ' + . 'UNION SELECT * FROM (SELECT "title", "year" FROM "top_movies" LIMIT 1) as wrapper_alias ' + . 'UNION ALL SELECT "title", "year" FROM "tomato_movies" ORDER BY "title"'; $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); } diff --git a/tests/system/Database/Live/UnionTest.php b/tests/system/Database/Live/UnionTest.php new file mode 100644 index 000000000000..7d3588a423b0 --- /dev/null +++ b/tests/system/Database/Live/UnionTest.php @@ -0,0 +1,39 @@ +db->table('user') + ->select('name') + ->where('country', 'US') + ->limit(1) + ->orderBy('name', 'DESC') + ->union(function (BaseBuilder $builder) { + return $builder->select('name') + ->from('job') + ->where('id >', 1) + ->limit(2); + }) + ->unionOrderBy('name') + ->get() + ->getResult(); + + $this->assertEquals(3, count($rows)); + $this->assertEquals('Accountant', $rows[0]->name); + $this->assertEquals('Politician', $rows[1]->name); + $this->assertEquals('Richard A Causey', $rows[2]->name); + } +} diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index df28955fb1ac..4f6d32297205 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -844,6 +844,8 @@ Combining queries (UNION) ************************* .. note:: To understand how UNION works, it is recommended to study the documentation for the used DBMS. +.. note:: Each separate query containing LIMIT and ORDER BY will be wrapped in a +``SELECT * FROM (...) as wrapper_alias`` so that the DBMS will process queries correctly **$builder->union()** @@ -857,42 +859,14 @@ The Closure must return an instance of the BaseBuilder class:: $builder->select('title, year') ->union(function (BaseBuilder $builder) { - return $builder->select('title, year')->from('top_movies'); - }) - ->get(); - - // (SELECT title, year FROM movies) UNION (SELECT title, year FROM top_movies) - -Nested UNION and variable passing:: - - //Add the following line after the namespace keyword - use CodeIgniter\Database\BaseBuilder; - - $year = 2000; - $yearTop = 2010; - $yearBad = 2020; - - $builder = $db->table('movies'); - - $builder->select('title, year') - ->where('year', $year) - ->union(function (BaseBuilder $builder) use ($yearTop, $yearBad){ return $builder->select('title, year') - ->from('top_movies') - ->where('year', $yearTop) - ->union(function (BaseBuilder $builder) use ($yearBad){ - return $builder->select('title, year') - ->from('bad_movies') - ->where('year', $yearBad) - }); + ->from('top_movies') + ->orderBy('title'); }) ->get(); - // (SELECT title, year FROM movies WHERE year = 2000) - // UNION ( - // (SELECT title, year FROM top_movies WHERE year = 2010) - // UNION (SELECT title, year FROM bad_movies WHERE year = 2020) - // ) + // SELECT title, year FROM movies + // UNION SELECT * FROM (SELECT title, year FROM top_movies ORDER BY title) as wrapper_alias **$builder->unionAll()** @@ -905,12 +879,14 @@ The method adds ALL to UNION:: $builder = $db->table('movies'); $builder->select('title, year') + ->limit(1) ->unionAll(function (BaseBuilder $builder) { return $builder->select('title, year')->from('top_movies'); }) ->get(); - // (SELECT title, year FROM movies) UNION ALL (SELECT title, year FROM top_movies) + // SELECT * FROM (SELECT title, year FROM movies LIMIT 1) as wrapper_alias + // UNION ALL SELECT title, year FROM top_movies **$builder->unionOrderBy()** @@ -930,8 +906,7 @@ The principle is the same as for ``orderBy()``:: ->unionOrderBy('title', 'DESC') ->get(); - // (SELECT title, year FROM movies) UNION ALL (SELECT title, year FROM top_movies) ORDER BY title DESC - + // SELECT title, year FROM movies UNION ALL SELECT title, year FROM top_movies ORDER BY title DESC ************** Inserting Data From 12e99d4d056c33c864b663d4b7fb4b1e3f660a56 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Wed, 17 Feb 2021 14:35:59 +0800 Subject: [PATCH 3/5] UG Explicit markup ends without a blank line --- user_guide_src/source/database/query_builder.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 4f6d32297205..906d5f4918c6 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -845,7 +845,8 @@ Combining queries (UNION) .. note:: To understand how UNION works, it is recommended to study the documentation for the used DBMS. .. note:: Each separate query containing LIMIT and ORDER BY will be wrapped in a -``SELECT * FROM (...) as wrapper_alias`` so that the DBMS will process queries correctly +``SELECT * FROM (...) as wrapper_alias`` so that the DBMS will process queries correctly. + **$builder->union()** From b55b8a67a8b2f8076df51087c142b57c659c578f Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Tue, 23 Feb 2021 15:27:54 +0800 Subject: [PATCH 4/5] Change: Changed behavior when using union and limit or orderBy methods --- system/Database/BaseBuilder.php | 89 +++++++++++++++++-- system/Database/SQLSRV/Builder.php | 2 +- tests/system/Database/Builder/UnionTest.php | 44 ++++++--- .../source/database/query_builder.rst | 22 +---- 4 files changed, 119 insertions(+), 38 deletions(-) diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 463c4b5fc404..c9fba948eb3c 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -153,6 +153,20 @@ class BaseBuilder */ protected $QBUnionOrderBy = []; + /** + * QB UNION LIMIT data + * + * @var integer|boolean + */ + protected $QBUnionLimit = false; + + /** + * QB UNION OFFSET data + * + * @var integer|boolean + */ + protected $QBUnionOffset = false; + /** * Ignore data that cause certain * exceptions, for example in case of @@ -1634,6 +1648,11 @@ public function orHaving($key, $value = null, bool $escape = null) */ public function orderBy(string $orderBy, string $direction = '', bool $escape = null) { + if (count($this->QBUnion)) + { + return $this->unionOrderBy($orderBy, $direction, $escape); + } + $direction = strtoupper(trim($direction)); if ($direction === 'RANDOM') @@ -1700,6 +1719,11 @@ public function orderBy(string $orderBy, string $direction = '', bool $escape = */ public function limit(?int $value = null, ?int $offset = 0) { + if (count($this->QBUnion)) + { + return $this->unionLimit($value, $offset); + } + if (! is_null($value)) { $this->QBLimit = $value; @@ -3039,14 +3063,14 @@ protected function compileSelect($selectOverride = false): string $sql = $this->_limit($sql . "\n"); } - if ($this->QBUnion) + if (count($this->QBUnion)) { if ($this->QBOrderBy || $this->QBLimit) { $sql = 'SELECT * FROM (' . $sql . ') as wrapper_alias'; } - $sql .= $this->compileUnion() . $this->compileUnionOrderBy(); + $sql .= $this->compileUnion() . $this->compileUnionFilter(); } return $sql; @@ -3394,6 +3418,8 @@ protected function resetSelect() 'QBDistinct' => false, 'QBLimit' => false, 'QBOffset' => false, + 'QBUnionLimit' => false, + 'QBUnionOffset' => false, ]); if (! empty($this->db)) @@ -3565,7 +3591,7 @@ protected function unionBuilder(Closure $closure, bool $all = false) $all = $all ? 'ALL ' : ''; - if (($builder->QBOrderBy || $builder->QBLimit || $builder->QBUnionOrderBy) + if (($builder->QBOrderBy || $builder->QBLimit || $builder->QBUnionOrderBy || $builder->QBUnionLimit) && strpos($sql, 'wrapper_alias UNION') === false) { $sql = 'SELECT * FROM (' . $sql . ') as wrapper_alias'; @@ -3589,15 +3615,15 @@ protected function compileUnion() : string } /** - * ORDER BY + * UNION ORDER BY global * * @param string $orderBy Field * @param string $direction ASC, DESC or RANDOM * @param boolean $escape Escape * - * @return static + * @return $this */ - public function unionOrderBy(string $orderBy, string $direction = '', bool $escape = null) + protected function unionOrderBy(string $orderBy, string $direction = '', bool $escape = null) { $this->QBUnionOrderBy[] = [ $orderBy, @@ -3609,19 +3635,64 @@ public function unionOrderBy(string $orderBy, string $direction = '', bool $esca } /** - * Compile UNION ORDER BY data + * UNION LIMIT global + * + * @param integer|null $value LIMIT value + * @param integer|null $offset OFFSET value + * + * @return $this + */ + protected function unionLimit(?int $value = null, ?int $offset = 0) + { + if (! is_null($value)) + { + $this->QBUnionLimit = $value; + } + + return $this->unionOffset($offset); + } + + /** + * Sets the UNION OFFSET value + * + * @param integer $offset OFFSET value + * + * @return $this + */ + protected function unionOffset(int $offset) + { + if (! empty($offset)) + { + $this->QBUnionOffset = (int) $offset; + } + + return $this; + } + + /** + * Compile UNION Filter as ORDER BY and LIMIT * * @return string */ - protected function compileUnionOrderBy() : string + protected function compileUnionFilter() : string { $builder = $this->cleanClone(); + foreach ($this->QBUnionOrderBy as $order) { $builder->orderBy(...$order); } - return $builder->compileOrderBy(); + $filter = $builder->compileOrderBy(); + + $builder->limit($this->QBUnionLimit, $this->QBUnionOffset); + + if ($this->QBUnionLimit) + { + $filter = $builder->_limit($filter . "\n"); + } + + return $filter; } //-------------------------------------------------------------------- diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php index 6fdf7e7ea856..5cc80c6bdb55 100755 --- a/system/Database/SQLSRV/Builder.php +++ b/system/Database/SQLSRV/Builder.php @@ -661,7 +661,7 @@ protected function compileSelect($selectOverride = false): string $sql = 'SELECT * FROM (' . $sql . ') as wrapper_alias'; } - $sql .= $this->compileUnion() . $this->compileUnionOrderBy(); + $sql .= $this->compileUnion() . $this->compileUnionFilter(); } return $sql; diff --git a/tests/system/Database/Builder/UnionTest.php b/tests/system/Database/Builder/UnionTest.php index fabc677b48a7..429c90f2d779 100644 --- a/tests/system/Database/Builder/UnionTest.php +++ b/tests/system/Database/Builder/UnionTest.php @@ -2,6 +2,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\SQLSRV\Builder as BuilderSQLSRV; use CodeIgniter\Test\Mock\MockConnection; class UnionTest extends \CodeIgniter\Test\CIUnitTestCase @@ -78,8 +79,8 @@ public function testUnionOrderBy() ->unionAll(function (BaseBuilder $builder) { return $builder->select('title, year')->from('top_movies'); }) - ->unionOrderBy('title', 'DESC') - ->unionOrderBy('year', 'ASC'); + ->orderBy('title', 'DESC') + ->orderBy('year', 'ASC'); $sql = 'SELECT "title", "year" FROM "movies" ' . 'UNION ALL SELECT "title", "year" FROM "top_movies" ORDER BY "title" DESC, "year" ASC'; @@ -94,8 +95,8 @@ public function testUnionOrderBy() ->from('top_movies') ->unionAll(function (BaseBuilder $builder) { return $builder->select('title, year')->from('tomato_movies'); - })->unionOrderBy('title', 'DESC'); - })->unionOrderBy('title'); + })->orderBy('title', 'DESC'); + })->orderBy('title'); $sql = 'SELECT "title", "year" FROM "movies" ' . 'UNION SELECT * FROM (SELECT "title", "year" FROM "top_movies" ' @@ -111,22 +112,45 @@ public function testUnionOrderBy() return $builder->select('title, year')->from('tomato_movies'); })->limit(1); }) - ->unionOrderBy('title'); + ->orderBy('title'); $sql = 'SELECT "title", "year" FROM "movies" ' - . 'UNION SELECT * FROM (SELECT "title", "year" FROM "top_movies" LIMIT 1) as wrapper_alias ' - . 'UNION ALL SELECT "title", "year" FROM "tomato_movies" ORDER BY "title"'; + . 'UNION SELECT * FROM (SELECT "title", "year" FROM "top_movies" ' + . 'UNION ALL SELECT "title", "year" FROM "tomato_movies" LIMIT 1) as wrapper_alias ORDER BY "title"'; $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); } - public function testIgnoreUnionOrderByWithoutUnionQuery() + public function testUnionLimit() { $builder = $this->db->table('movies'); - $builder->select('title, year')->unionOrderBy('year', 'ASC'); + $builder->select('title, year') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }) + ->limit(5, 10); + + $sql = 'SELECT "title", "year" FROM "movies" ' + . 'UNION ALL SELECT "title", "year" FROM "top_movies" LIMIT 10, 5'; + + $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + public function testUnionSQLSRV() + { + $dbc = new MockConnection(['schema' => 'dbo', 'database' => 'test']); + $builder = new BuilderSQLSRV('movies', $dbc); + + $builder->select('title, year') + ->unionAll(function (BaseBuilder $builder) { + return $builder->select('title, year')->from('top_movies'); + }) + ->limit(5, 10); - $sql = 'SELECT "title", "year" FROM "movies"'; + $sql = 'SELECT "title", "year" FROM "test"."dbo"."movies" ' + . 'UNION ALL SELECT "title", "year" FROM "test"."dbo"."top_movies" ' + . ' ORDER BY (SELECT NULL) OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY '; $this->assertEquals($sql, str_replace("\n", ' ', $builder->getCompiledSelect())); } diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 906d5f4918c6..474efd089015 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -727,6 +727,9 @@ be ignored, unless you specify a numeric seed value. .. note:: Random ordering is not currently supported in Oracle and will default to ASC instead. +.. note:: The orderBy() method called after the union()/unionAll() method will be applied to the final result + + **************************** Limiting or Counting Results **************************** @@ -745,6 +748,7 @@ The second parameter lets you set a result offset. $builder->limit(10, 20); // Produces: LIMIT 20, 10 (in MySQL. Other databases have slightly different syntax) +.. note:: The limit() method called after the union()/unionAll() method will be applied to the final result **$builder->countAllResults()** @@ -890,24 +894,6 @@ The method adds ALL to UNION:: // UNION ALL SELECT title, year FROM top_movies -**$builder->unionOrderBy()** - -The method adds a sort for the final query result after combining queries using UNION [ALL]. -The principle is the same as for ``orderBy()``:: - - //Add the following line after the namespace keyword - use CodeIgniter\Database\BaseBuilder; - - $builder = $db->table('movies'); - - $builder->select('title, year') - ->unionAll(function (BaseBuilder $builder) { - return $builder->select('title, year')->from('top_movies'); - }) - ->unionOrderBy('title', 'DESC') - ->get(); - - // SELECT title, year FROM movies UNION ALL SELECT title, year FROM top_movies ORDER BY title DESC ************** Inserting Data From b31cf02573857a69ab52370496ea780ce2cd4e81 Mon Sep 17 00:00:00 2001 From: Andrey Pyzhikov <5071@mail.ru> Date: Tue, 23 Feb 2021 15:56:21 +0800 Subject: [PATCH 5/5] Fix live test --- tests/system/Database/Live/UnionTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Database/Live/UnionTest.php b/tests/system/Database/Live/UnionTest.php index 7d3588a423b0..1bd382fa477a 100644 --- a/tests/system/Database/Live/UnionTest.php +++ b/tests/system/Database/Live/UnionTest.php @@ -27,7 +27,7 @@ public function testUnion() ->where('id >', 1) ->limit(2); }) - ->unionOrderBy('name') + ->orderBy('name') ->get() ->getResult();