Skip to content

Commit 6e060d7

Browse files
authored
Merge pull request #2001 from iRedds/feature/querybuiler-subqueries
Subqueries in BaseBuilder
2 parents 33835f1 + 083aa25 commit 6e060d7

3 files changed

Lines changed: 214 additions & 62 deletions

File tree

system/Database/BaseBuilder.php

Lines changed: 57 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
use CodeIgniter\Database\Exceptions\DatabaseException;
4242
use CodeIgniter\Database\Exceptions\DataException;
43+
use Closure;
4344

4445
/**
4546
* Class BaseBuilder
@@ -718,10 +719,18 @@ protected function whereHaving(string $qb_key, $key, $value = null, string $type
718719
}
719720
else
720721
{
721-
$k .= $op;
722+
$k .= " $op";
722723
}
723724

724-
$v = " :$bind:";
725+
if ($v instanceof Closure)
726+
{
727+
$builder = $this->cleanClone();
728+
$v = '(' . str_replace("\n", ' ', $v($builder)->getCompiledSelect()) . ')';
729+
}
730+
else
731+
{
732+
$v = " :$bind:";
733+
}
725734
}
726735
elseif (! $this->hasOperator($k) && $qb_key !== 'QBHaving')
727736
{
@@ -750,13 +759,13 @@ protected function whereHaving(string $qb_key, $key, $value = null, string $type
750759
* Generates a WHERE field IN('item', 'item') SQL query,
751760
* joined with 'AND' if appropriate.
752761
*
753-
* @param string $key The field to search
754-
* @param array $values The values searched on
755-
* @param boolean $escape
762+
* @param string $key The field to search
763+
* @param array|Closure $values The values searched on, or anonymous function with subquery
764+
* @param boolean $escape
756765
*
757766
* @return BaseBuilder
758767
*/
759-
public function whereIn(string $key = null, array $values = null, bool $escape = null)
768+
public function whereIn(string $key = null, $values = null, bool $escape = null)
760769
{
761770
return $this->_whereIn($key, $values, false, 'AND ', $escape);
762771
}
@@ -769,13 +778,13 @@ public function whereIn(string $key = null, array $values = null, bool $escape =
769778
* Generates a WHERE field IN('item', 'item') SQL query,
770779
* joined with 'OR' if appropriate.
771780
*
772-
* @param string $key The field to search
773-
* @param array $values The values searched on
774-
* @param boolean $escape
781+
* @param string $key The field to search
782+
* @param array|Closure $values The values searched on, or anonymous function with subquery
783+
* @param boolean $escape
775784
*
776785
* @return BaseBuilder
777786
*/
778-
public function orWhereIn(string $key = null, array $values = null, bool $escape = null)
787+
public function orWhereIn(string $key = null, $values = null, bool $escape = null)
779788
{
780789
return $this->_whereIn($key, $values, false, 'OR ', $escape);
781790
}
@@ -788,13 +797,13 @@ public function orWhereIn(string $key = null, array $values = null, bool $escape
788797
* Generates a WHERE field NOT IN('item', 'item') SQL query,
789798
* joined with 'AND' if appropriate.
790799
*
791-
* @param string $key The field to search
792-
* @param array $values The values searched on
793-
* @param boolean $escape
800+
* @param string $key The field to search
801+
* @param array|Closure $values The values searched on, or anonymous function with subquery
802+
* @param boolean $escape
794803
*
795804
* @return BaseBuilder
796805
*/
797-
public function whereNotIn(string $key = null, array $values = null, bool $escape = null)
806+
public function whereNotIn(string $key = null, $values = null, bool $escape = null)
798807
{
799808
return $this->_whereIn($key, $values, true, 'AND ', $escape);
800809
}
@@ -807,13 +816,13 @@ public function whereNotIn(string $key = null, array $values = null, bool $escap
807816
* Generates a WHERE field NOT IN('item', 'item') SQL query,
808817
* joined with 'OR' if appropriate.
809818
*
810-
* @param string $key The field to search
811-
* @param array $values The values searched on
812-
* @param boolean $escape
819+
* @param string $key The field to search
820+
* @param array|Closure $values The values searched on, or anonymous function with subquery
821+
* @param boolean $escape
813822
*
814823
* @return BaseBuilder
815824
*/
816-
public function orWhereNotIn(string $key = null, array $values = null, bool $escape = null)
825+
public function orWhereNotIn(string $key = null, $values = null, bool $escape = null)
817826
{
818827
return $this->_whereIn($key, $values, true, 'OR ', $escape);
819828
}
@@ -828,17 +837,17 @@ public function orWhereNotIn(string $key = null, array $values = null, bool $esc
828837
* @used-by whereNotIn()
829838
* @used-by orWhereNotIn()
830839
*
831-
* @param string $key The field to search
832-
* @param array $values The values searched on
833-
* @param boolean $not If the statement would be IN or NOT IN
834-
* @param string $type
835-
* @param boolean $escape
840+
* @param string $key The field to search
841+
* @param array|Closure $values The values searched on, or anonymous function with subquery
842+
* @param boolean $not If the statement would be IN or NOT IN
843+
* @param string $type
844+
* @param boolean $escape
836845
*
837846
* @return BaseBuilder
838847
*/
839-
protected function _whereIn(string $key = null, array $values = null, bool $not = false, string $type = 'AND ', bool $escape = null)
848+
protected function _whereIn(string $key = null, $values = null, bool $not = false, string $type = 'AND ', bool $escape = null)
840849
{
841-
if ($key === null || $values === null)
850+
if ($key === null || $values === null || (! is_array($values) && ! ($values instanceof Closure)))
842851
{
843852
return $this;
844853
}
@@ -854,13 +863,20 @@ protected function _whereIn(string $key = null, array $values = null, bool $not
854863

855864
$not = ($not) ? ' NOT' : '';
856865

857-
$where_in = array_values($values);
858-
$ok = $this->setBind($ok, $where_in, $escape);
866+
if ($values instanceof Closure)
867+
{
868+
$builder = $this->cleanClone();
869+
$ok = str_replace("\n", ' ', $values($builder)->getCompiledSelect());
870+
}
871+
else
872+
{
873+
$ok = $this->setBind($ok, array_values($values), $escape);
874+
}
859875

860876
$prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type);
861877

862878
$where_in = [
863-
'condition' => $prefix . $key . $not . " IN :{$ok}:",
879+
'condition' => $prefix . $key . $not . ($values instanceof Closure ? " IN ($ok)" : " IN :{$ok}:"),
864880
'escape' => false,
865881
];
866882

@@ -2676,7 +2692,6 @@ protected function compileWhereHaving(string $qb_key): string
26762692
{
26772693
continue;
26782694
}
2679-
26802695
// $matches = array(
26812696
// 0 => '(test <= foo)', /* the whole thing */
26822697
// 1 => '(', /* optional */
@@ -3004,7 +3019,7 @@ protected function getOperator(string $str, bool $list = false)
30043019
];
30053020
}
30063021

3007-
return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][count($match[0]) - 1]) : false;
3022+
return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][0]) : false;
30083023
}
30093024

30103025
// --------------------------------------------------------------------
@@ -3049,4 +3064,16 @@ protected function setBind(string $key, $value = null, bool $escape = true): str
30493064
}
30503065

30513066
//--------------------------------------------------------------------
3067+
3068+
/**
3069+
* Returns a clone of a Base Builder with reset query builder values.
3070+
*
3071+
* @return BaseBuilder
3072+
*/
3073+
protected function cleanClone()
3074+
{
3075+
return (clone $this)->from([], true)->resetQuery();
3076+
}
3077+
3078+
//--------------------------------------------------------------------
30523079
}

tests/system/Database/Builder/WhereTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php namespace Builder;
22

3+
use CodeIgniter\Database\BaseBuilder;
34
use Tests\Support\Database\MockConnection;
45

56
class WhereTest extends \CIUnitTestCase
@@ -118,6 +119,20 @@ public function testWhereCustomString()
118119

119120
//--------------------------------------------------------------------
120121

122+
public function testWhereValueClosure()
123+
{
124+
$builder = $this->db->table('neworder');
125+
126+
$builder->where('advance_amount <', function (BaseBuilder $builder) {
127+
return $builder->select('MAX(advance_amount)', false)->from('orders')->where('id >', 2);
128+
});
129+
$expectedSQL = 'SELECT * FROM "neworder" WHERE "advance_amount" < (SELECT MAX(advance_amount) FROM "orders" WHERE "id" > 2)';
130+
131+
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
132+
}
133+
134+
//--------------------------------------------------------------------
135+
121136
public function testOrWhere()
122137
{
123138
$builder = $this->db->table('jobs');
@@ -191,6 +206,21 @@ public function testWhereIn()
191206

192207
//--------------------------------------------------------------------
193208

209+
public function testWhereInClosure()
210+
{
211+
$builder = $this->db->table('jobs');
212+
213+
$builder->whereIn('id', function (BaseBuilder $builder) {
214+
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
215+
});
216+
217+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
218+
219+
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
220+
}
221+
222+
//--------------------------------------------------------------------
223+
194224
public function testWhereNotIn()
195225
{
196226
$builder = $this->db->table('jobs');
@@ -214,6 +244,21 @@ public function testWhereNotIn()
214244

215245
//--------------------------------------------------------------------
216246

247+
public function testWhereNotInClosure()
248+
{
249+
$builder = $this->db->table('jobs');
250+
251+
$builder->whereNotIn('id', function (BaseBuilder $builder) {
252+
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
253+
});
254+
255+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
256+
257+
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
258+
}
259+
260+
//--------------------------------------------------------------------
261+
217262
public function testOrWhereIn()
218263
{
219264
$builder = $this->db->table('jobs');
@@ -241,6 +286,21 @@ public function testOrWhereIn()
241286

242287
//--------------------------------------------------------------------
243288

289+
public function testOrWhereInClosure()
290+
{
291+
$builder = $this->db->table('jobs');
292+
293+
$builder->where('deleted_at', null)->orWhereIn('id', function (BaseBuilder $builder) {
294+
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
295+
});
296+
297+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
298+
299+
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
300+
}
301+
302+
//--------------------------------------------------------------------
303+
244304
public function testOrWhereNotIn()
245305
{
246306
$builder = $this->db->table('jobs');
@@ -267,4 +327,19 @@ public function testOrWhereNotIn()
267327
}
268328

269329
//--------------------------------------------------------------------
330+
331+
public function testOrWhereNotInClosure()
332+
{
333+
$builder = $this->db->table('jobs');
334+
335+
$builder->where('deleted_at', null)->orWhereNotIn('id', function (BaseBuilder $builder) {
336+
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
337+
});
338+
339+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
340+
341+
$this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
342+
}
343+
344+
//--------------------------------------------------------------------
270345
}

0 commit comments

Comments
 (0)