Skip to content

Commit 7807496

Browse files
authored
Merge pull request #5488 from iRedds/subquery-buider
Feature: BaseBuilder instance as subquery.
2 parents 60f1367 + 864d366 commit 7807496

4 files changed

Lines changed: 179 additions & 52 deletions

File tree

system/Database/BaseBuilder.php

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -693,9 +693,8 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type
693693
$op = ' =';
694694
}
695695

696-
if ($v instanceof Closure) {
697-
$builder = $this->cleanClone();
698-
$v = ' (' . strtr($v($builder)->getCompiledSelect(), "\n", ' ') . ')';
696+
if ($this->isSubquery($v)) {
697+
$v = $this->buildSubquery($v, true);
699698
} else {
700699
$bind = $this->setBind($k, $v, $escape);
701700
$v = " :{$bind}:";
@@ -723,7 +722,7 @@ protected function whereHaving(string $qbKey, $key, $value = null, string $type
723722
* Generates a WHERE field IN('item', 'item') SQL query,
724723
* joined with 'AND' if appropriate.
725724
*
726-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
725+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
727726
*
728727
* @return $this
729728
*/
@@ -736,7 +735,7 @@ public function whereIn(?string $key = null, $values = null, ?bool $escape = nul
736735
* Generates a WHERE field IN('item', 'item') SQL query,
737736
* joined with 'OR' if appropriate.
738737
*
739-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
738+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
740739
*
741740
* @return $this
742741
*/
@@ -749,7 +748,7 @@ public function orWhereIn(?string $key = null, $values = null, ?bool $escape = n
749748
* Generates a WHERE field NOT IN('item', 'item') SQL query,
750749
* joined with 'AND' if appropriate.
751750
*
752-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
751+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
753752
*
754753
* @return $this
755754
*/
@@ -762,7 +761,7 @@ public function whereNotIn(?string $key = null, $values = null, ?bool $escape =
762761
* Generates a WHERE field NOT IN('item', 'item') SQL query,
763762
* joined with 'OR' if appropriate.
764763
*
765-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
764+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
766765
*
767766
* @return $this
768767
*/
@@ -775,7 +774,7 @@ public function orWhereNotIn(?string $key = null, $values = null, ?bool $escape
775774
* Generates a HAVING field IN('item', 'item') SQL query,
776775
* joined with 'AND' if appropriate.
777776
*
778-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
777+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
779778
*
780779
* @return $this
781780
*/
@@ -788,7 +787,7 @@ public function havingIn(?string $key = null, $values = null, ?bool $escape = nu
788787
* Generates a HAVING field IN('item', 'item') SQL query,
789788
* joined with 'OR' if appropriate.
790789
*
791-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
790+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
792791
*
793792
* @return $this
794793
*/
@@ -801,7 +800,7 @@ public function orHavingIn(?string $key = null, $values = null, ?bool $escape =
801800
* Generates a HAVING field NOT IN('item', 'item') SQL query,
802801
* joined with 'AND' if appropriate.
803802
*
804-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
803+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
805804
*
806805
* @return $this
807806
*/
@@ -814,7 +813,7 @@ public function havingNotIn(?string $key = null, $values = null, ?bool $escape =
814813
* Generates a HAVING field NOT IN('item', 'item') SQL query,
815814
* joined with 'OR' if appropriate.
816815
*
817-
* @param array|Closure|string $values The values searched on, or anonymous function with subquery
816+
* @param array|BaseBuilder|Closure|string $values The values searched on, or anonymous function with subquery
818817
*
819818
* @return $this
820819
*/
@@ -829,7 +828,7 @@ public function orHavingNotIn(?string $key = null, $values = null, ?bool $escape
829828
* @used-by whereNotIn()
830829
* @used-by orWhereNotIn()
831830
*
832-
* @param array|Closure|null $values The values searched on, or anonymous function with subquery
831+
* @param array|BaseBuilder|Closure|null $values The values searched on, or anonymous function with subquery
833832
*
834833
* @throws InvalidArgumentException
835834
*
@@ -845,7 +844,7 @@ protected function _whereIn(?string $key = null, $values = null, bool $not = fal
845844
return $this; // @codeCoverageIgnore
846845
}
847846

848-
if ($values === null || (! is_array($values) && ! ($values instanceof Closure))) {
847+
if ($values === null || (! is_array($values) && ! $this->isSubquery($values))) {
849848
if (CI_DEBUG) {
850849
throw new InvalidArgumentException(sprintf('%s() expects $values to be of type array or closure', debug_backtrace(0, 2)[1]['function']));
851850
}
@@ -865,18 +864,19 @@ protected function _whereIn(?string $key = null, $values = null, bool $not = fal
865864

866865
$not = ($not) ? ' NOT' : '';
867866

868-
if ($values instanceof Closure) {
869-
$builder = $this->cleanClone();
870-
$ok = strtr($values($builder)->getCompiledSelect(), "\n", ' ');
867+
if ($this->isSubquery($values)) {
868+
$whereIn = $this->buildSubquery($values, true);
869+
$escape = false;
871870
} else {
872871
$whereIn = array_values($values);
873-
$ok = $this->setBind($ok, $whereIn, $escape);
874872
}
875873

874+
$ok = $this->setBind($ok, $whereIn, $escape);
875+
876876
$prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type);
877877

878878
$whereIn = [
879-
'condition' => $prefix . $key . $not . ($values instanceof Closure ? " IN ({$ok})" : " IN :{$ok}:"),
879+
'condition' => "{$prefix}{$key}{$not} IN :{$ok}:",
880880
'escape' => false,
881881
];
882882

@@ -2724,9 +2724,35 @@ protected function setBind(string $key, $value = null, bool $escape = true): str
27242724
* Returns a clone of a Base Builder with reset query builder values.
27252725
*
27262726
* @return $this
2727+
*
2728+
* @deprecated
27272729
*/
27282730
protected function cleanClone()
27292731
{
27302732
return (clone $this)->from([], true)->resetQuery();
27312733
}
2734+
2735+
/**
2736+
* @param mixed $value
2737+
*/
2738+
protected function isSubquery($value): bool
2739+
{
2740+
return $value instanceof BaseBuilder || $value instanceof Closure;
2741+
}
2742+
2743+
/**
2744+
* @param BaseBuilder|Closure $builder
2745+
* @param bool $wrapped Wrap the subquery in brackets
2746+
*/
2747+
protected function buildSubquery($builder, bool $wrapped = false): string
2748+
{
2749+
if ($builder instanceof Closure) {
2750+
$instance = (clone $this)->from([], true)->resetQuery();
2751+
$builder = $builder($instance);
2752+
}
2753+
2754+
$subquery = strtr($builder->getCompiledSelect(), "\n", ' ');
2755+
2756+
return $wrapped ? '(' . $subquery . ')' : $subquery;
2757+
}
27322758
}

tests/system/Database/Builder/WhereTest.php

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,27 @@ public function testWhereCustomString()
141141
$this->assertSame($expectedBinds, $builder->getBinds());
142142
}
143143

144-
public function testWhereValueClosure()
144+
public function testWhereValueSubQuery()
145145
{
146+
$expectedSQL = 'SELECT * FROM "neworder" WHERE "advance_amount" < (SELECT MAX(advance_amount) FROM "orders" WHERE "id" > 2)';
147+
148+
// Closure
146149
$builder = $this->db->table('neworder');
147150

148151
$builder->where('advance_amount <', static function (BaseBuilder $builder) {
149152
return $builder->select('MAX(advance_amount)', false)->from('orders')->where('id >', 2);
150153
});
151-
$expectedSQL = 'SELECT * FROM "neworder" WHERE "advance_amount" < (SELECT MAX(advance_amount) FROM "orders" WHERE "id" > 2)';
154+
155+
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
156+
157+
// Builder
158+
$builder = $this->db->table('neworder');
159+
160+
$subQuery = $this->db->table('orders')
161+
->select('MAX(advance_amount)', false)
162+
->where('id >', 2);
163+
164+
$builder->where('advance_amount <', $subQuery);
152165

153166
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
154167
}
@@ -218,15 +231,27 @@ public function testWhereIn()
218231
$this->assertSame($expectedBinds, $builder->getBinds());
219232
}
220233

221-
public function testWhereInClosure()
234+
public function testWhereInSubQuery()
222235
{
236+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
237+
238+
// Closure
223239
$builder = $this->db->table('jobs');
224240

225241
$builder->whereIn('id', static function (BaseBuilder $builder) {
226242
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
227243
});
228244

229-
$expectedSQL = 'SELECT * FROM "jobs" WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
245+
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
246+
247+
// Builder
248+
$builder = $this->db->table('jobs');
249+
250+
$subQuery = $this->db->table('users_jobs')
251+
->select('job_id')
252+
->where('user_id', 3);
253+
254+
$builder->whereIn('id', $subQuery);
230255

231256
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
232257
}
@@ -295,15 +320,27 @@ public function testWhereNotIn()
295320
$this->assertSame($expectedBinds, $builder->getBinds());
296321
}
297322

298-
public function testWhereNotInClosure()
323+
public function testWhereNotInSubQuery()
299324
{
325+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
326+
327+
// Closure
300328
$builder = $this->db->table('jobs');
301329

302330
$builder->whereNotIn('id', static function (BaseBuilder $builder) {
303331
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
304332
});
305333

306-
$expectedSQL = 'SELECT * FROM "jobs" WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
334+
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
335+
336+
// Builder
337+
$builder = $this->db->table('jobs');
338+
339+
$subQuery = $this->db->table('users_jobs')
340+
->select('job_id')
341+
->where('user_id', 3);
342+
343+
$builder->whereNotIn('id', $subQuery);
307344

308345
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
309346
}
@@ -333,15 +370,27 @@ public function testOrWhereIn()
333370
$this->assertSame($expectedBinds, $builder->getBinds());
334371
}
335372

336-
public function testOrWhereInClosure()
373+
public function testOrWhereInSubQuery()
337374
{
375+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
376+
377+
// Closure
338378
$builder = $this->db->table('jobs');
339379

340380
$builder->where('deleted_at', null)->orWhereIn('id', static function (BaseBuilder $builder) {
341381
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
342382
});
343383

344-
$expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
384+
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
385+
386+
// Builder
387+
$builder = $this->db->table('jobs');
388+
389+
$subQuery = $this->db->table('users_jobs')
390+
->select('job_id')
391+
->where('user_id', 3);
392+
393+
$builder->where('deleted_at', null)->orWhereIn('id', $subQuery);
345394

346395
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
347396
}
@@ -371,15 +420,27 @@ public function testOrWhereNotIn()
371420
$this->assertSame($expectedBinds, $builder->getBinds());
372421
}
373422

374-
public function testOrWhereNotInClosure()
423+
public function testOrWhereNotInSubQuery()
375424
{
425+
$expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
426+
427+
// Closure
376428
$builder = $this->db->table('jobs');
377429

378430
$builder->where('deleted_at', null)->orWhereNotIn('id', static function (BaseBuilder $builder) {
379431
return $builder->select('job_id')->from('users_jobs')->where('user_id', 3);
380432
});
381433

382-
$expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)';
434+
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
435+
436+
// Builder
437+
$builder = $this->db->table('jobs');
438+
439+
$subQuery = $this->db->table('users_jobs')
440+
->select('job_id')
441+
->where('user_id', 3);
442+
443+
$builder->where('deleted_at', null)->orWhereNotIn('id', $subQuery);
383444

384445
$this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
385446
}

user_guide_src/source/changelogs/v4.1.6.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Enhancements
3333
************
3434

3535
- Database pane on debug toolbar now displays location where Query was called from. Also displays full backtrace.
36+
- :ref:`Subqueries <query-builder-where-subquery>` in QueryBuilder can now be an instance of the BaseBuilder class.
3637

3738
Changes
3839
*******
@@ -43,6 +44,7 @@ Deprecations
4344
************
4445

4546
- ``Seeder::faker()`` and ``Seeder::$faker`` are deprecated.
47+
- ``BaseBuilder::cleanClone()`` is deprecated.
4648

4749
Sending Cookies
4850
===============

0 commit comments

Comments
 (0)