Skip to content

Commit ae12df5

Browse files
committed
Subqueries in BaseBuilder
Signed-off-by: Andrey Pyzhikov <5071@mail.ru>
1 parent 45174f7 commit ae12df5

3 files changed

Lines changed: 204 additions & 62 deletions

File tree

system/Database/BaseBuilder.php

Lines changed: 47 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
@@ -701,10 +702,19 @@ protected function whereHaving(string $qb_key, $key, $value = null, string $type
701702
}
702703
else
703704
{
704-
$k .= $op;
705+
$k .= " $op";
705706
}
706707

707-
$v = " :$bind:";
708+
if ($v instanceof Closure)
709+
{
710+
$builder = clone $this;
711+
$builder->from([], true)->resetQuery();
712+
$v = '(' . str_replace("\n", ' ', $v($builder)->getCompiledSelect()) . ')';
713+
}
714+
else
715+
{
716+
$v = " :$bind:";
717+
}
708718
}
709719
elseif (! $this->hasOperator($k) && $qb_key !== 'QBHaving')
710720
{
@@ -733,13 +743,13 @@ protected function whereHaving(string $qb_key, $key, $value = null, string $type
733743
* Generates a WHERE field IN('item', 'item') SQL query,
734744
* joined with 'AND' if appropriate.
735745
*
736-
* @param string $key The field to search
737-
* @param array $values The values searched on
738-
* @param boolean $escape
746+
* @param string $key The field to search
747+
* @param array|Closure $values The values searched on, or anonymous function with subquery
748+
* @param boolean $escape
739749
*
740750
* @return BaseBuilder
741751
*/
742-
public function whereIn(string $key = null, array $values = null, bool $escape = null)
752+
public function whereIn(string $key = null, $values = null, bool $escape = null)
743753
{
744754
return $this->_whereIn($key, $values, false, 'AND ', $escape);
745755
}
@@ -752,13 +762,13 @@ public function whereIn(string $key = null, array $values = null, bool $escape =
752762
* Generates a WHERE field IN('item', 'item') SQL query,
753763
* joined with 'OR' if appropriate.
754764
*
755-
* @param string $key The field to search
756-
* @param array $values The values searched on
757-
* @param boolean $escape
765+
* @param string $key The field to search
766+
* @param array|Closure $values The values searched on, or anonymous function with subquery
767+
* @param boolean $escape
758768
*
759769
* @return BaseBuilder
760770
*/
761-
public function orWhereIn(string $key = null, array $values = null, bool $escape = null)
771+
public function orWhereIn(string $key = null, $values = null, bool $escape = null)
762772
{
763773
return $this->_whereIn($key, $values, false, 'OR ', $escape);
764774
}
@@ -771,13 +781,13 @@ public function orWhereIn(string $key = null, array $values = null, bool $escape
771781
* Generates a WHERE field NOT IN('item', 'item') SQL query,
772782
* joined with 'AND' if appropriate.
773783
*
774-
* @param string $key The field to search
775-
* @param array $values The values searched on
776-
* @param boolean $escape
784+
* @param string $key The field to search
785+
* @param array|Closure $values The values searched on, or anonymous function with subquery
786+
* @param boolean $escape
777787
*
778788
* @return BaseBuilder
779789
*/
780-
public function whereNotIn(string $key = null, array $values = null, bool $escape = null)
790+
public function whereNotIn(string $key = null, $values = null, bool $escape = null)
781791
{
782792
return $this->_whereIn($key, $values, true, 'AND ', $escape);
783793
}
@@ -790,13 +800,13 @@ public function whereNotIn(string $key = null, array $values = null, bool $escap
790800
* Generates a WHERE field NOT IN('item', 'item') SQL query,
791801
* joined with 'OR' if appropriate.
792802
*
793-
* @param string $key The field to search
794-
* @param array $values The values searched on
795-
* @param boolean $escape
803+
* @param string $key The field to search
804+
* @param array|Closure $values The values searched on, or anonymous function with subquery
805+
* @param boolean $escape
796806
*
797807
* @return BaseBuilder
798808
*/
799-
public function orWhereNotIn(string $key = null, array $values = null, bool $escape = null)
809+
public function orWhereNotIn(string $key = null, $values = null, bool $escape = null)
800810
{
801811
return $this->_whereIn($key, $values, true, 'OR ', $escape);
802812
}
@@ -811,17 +821,17 @@ public function orWhereNotIn(string $key = null, array $values = null, bool $esc
811821
* @used-by whereNotIn()
812822
* @used-by orWhereNotIn()
813823
*
814-
* @param string $key The field to search
815-
* @param array $values The values searched on
816-
* @param boolean $not If the statement would be IN or NOT IN
817-
* @param string $type
818-
* @param boolean $escape
824+
* @param string $key The field to search
825+
* @param array|Closure $values The values searched on, or anonymous function with subquery
826+
* @param boolean $not If the statement would be IN or NOT IN
827+
* @param string $type
828+
* @param boolean $escape
819829
*
820830
* @return BaseBuilder
821831
*/
822-
protected function _whereIn(string $key = null, array $values = null, bool $not = false, string $type = 'AND ', bool $escape = null)
832+
protected function _whereIn(string $key = null, $values = null, bool $not = false, string $type = 'AND ', bool $escape = null)
823833
{
824-
if ($key === null || $values === null)
834+
if ($key === null || $values === null || (! is_array($values) && ! ($values instanceof Closure)))
825835
{
826836
return $this;
827837
}
@@ -837,13 +847,21 @@ protected function _whereIn(string $key = null, array $values = null, bool $not
837847

838848
$not = ($not) ? ' NOT' : '';
839849

840-
$where_in = array_values($values);
841-
$ok = $this->setBind($ok, $where_in, $escape);
850+
if ($values instanceof Closure)
851+
{
852+
$builder = clone $this;
853+
$builder->from([], true)->resetQuery();
854+
$ok = str_replace("\n", ' ', $values($builder)->getCompiledSelect());
855+
}
856+
else
857+
{
858+
$ok = $this->setBind($ok, array_values($values), $escape);
859+
}
842860

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

845863
$where_in = [
846-
'condition' => $prefix . $key . $not . " IN :{$ok}:",
864+
'condition' => $prefix . $key . $not . ($values instanceof Closure ? " IN ($ok)" : " IN :{$ok}:"),
847865
'escape' => false,
848866
];
849867

@@ -2640,7 +2658,6 @@ protected function compileWhereHaving(string $qb_key): string
26402658
{
26412659
continue;
26422660
}
2643-
26442661
// $matches = array(
26452662
// 0 => '(test <= foo)', /* the whole thing */
26462663
// 1 => '(', /* optional */
@@ -2968,7 +2985,7 @@ protected function getOperator(string $str, bool $list = false)
29682985
];
29692986
}
29702987

2971-
return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][count($match[0]) - 1]) : false;
2988+
return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][0]) : false;
29722989
}
29732990

29742991
// --------------------------------------------------------------------

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)