diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 3bab4f1569d5..4cc692ebc148 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -40,6 +40,7 @@ use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; +use Closure; /** * Class BaseBuilder @@ -701,10 +702,18 @@ protected function whereHaving(string $qb_key, $key, $value = null, string $type } else { - $k .= $op; + $k .= " $op"; } - $v = " :$bind:"; + if ($v instanceof Closure) + { + $builder = $this->cleanClone(); + $v = '(' . str_replace("\n", ' ', $v($builder)->getCompiledSelect()) . ')'; + } + else + { + $v = " :$bind:"; + } } elseif (! $this->hasOperator($k) && $qb_key !== 'QBHaving') { @@ -733,13 +742,13 @@ protected function whereHaving(string $qb_key, $key, $value = null, string $type * Generates a WHERE field IN('item', 'item') SQL query, * joined with 'AND' if appropriate. * - * @param string $key The field to search - * @param array $values The values searched on - * @param boolean $escape + * @param string $key The field to search + * @param array|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape * * @return BaseBuilder */ - public function whereIn(string $key = null, array $values = null, bool $escape = null) + public function whereIn(string $key = null, $values = null, bool $escape = null) { return $this->_whereIn($key, $values, false, 'AND ', $escape); } @@ -752,13 +761,13 @@ public function whereIn(string $key = null, array $values = null, bool $escape = * Generates a WHERE field IN('item', 'item') SQL query, * joined with 'OR' if appropriate. * - * @param string $key The field to search - * @param array $values The values searched on - * @param boolean $escape + * @param string $key The field to search + * @param array|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape * * @return BaseBuilder */ - public function orWhereIn(string $key = null, array $values = null, bool $escape = null) + public function orWhereIn(string $key = null, $values = null, bool $escape = null) { return $this->_whereIn($key, $values, false, 'OR ', $escape); } @@ -771,13 +780,13 @@ public function orWhereIn(string $key = null, array $values = null, bool $escape * Generates a WHERE field NOT IN('item', 'item') SQL query, * joined with 'AND' if appropriate. * - * @param string $key The field to search - * @param array $values The values searched on - * @param boolean $escape + * @param string $key The field to search + * @param array|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape * * @return BaseBuilder */ - public function whereNotIn(string $key = null, array $values = null, bool $escape = null) + public function whereNotIn(string $key = null, $values = null, bool $escape = null) { return $this->_whereIn($key, $values, true, 'AND ', $escape); } @@ -790,13 +799,13 @@ public function whereNotIn(string $key = null, array $values = null, bool $escap * Generates a WHERE field NOT IN('item', 'item') SQL query, * joined with 'OR' if appropriate. * - * @param string $key The field to search - * @param array $values The values searched on - * @param boolean $escape + * @param string $key The field to search + * @param array|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $escape * * @return BaseBuilder */ - public function orWhereNotIn(string $key = null, array $values = null, bool $escape = null) + public function orWhereNotIn(string $key = null, $values = null, bool $escape = null) { return $this->_whereIn($key, $values, true, 'OR ', $escape); } @@ -811,17 +820,17 @@ public function orWhereNotIn(string $key = null, array $values = null, bool $esc * @used-by whereNotIn() * @used-by orWhereNotIn() * - * @param string $key The field to search - * @param array $values The values searched on - * @param boolean $not If the statement would be IN or NOT IN - * @param string $type - * @param boolean $escape + * @param string $key The field to search + * @param array|Closure $values The values searched on, or anonymous function with subquery + * @param boolean $not If the statement would be IN or NOT IN + * @param string $type + * @param boolean $escape * * @return BaseBuilder */ - protected function _whereIn(string $key = null, array $values = null, bool $not = false, string $type = 'AND ', bool $escape = null) + protected function _whereIn(string $key = null, $values = null, bool $not = false, string $type = 'AND ', bool $escape = null) { - if ($key === null || $values === null) + if ($key === null || $values === null || (! is_array($values) && ! ($values instanceof Closure))) { return $this; } @@ -837,13 +846,20 @@ protected function _whereIn(string $key = null, array $values = null, bool $not $not = ($not) ? ' NOT' : ''; - $where_in = array_values($values); - $ok = $this->setBind($ok, $where_in, $escape); + if ($values instanceof Closure) + { + $builder = $this->cleanClone(); + $ok = str_replace("\n", ' ', $values($builder)->getCompiledSelect()); + } + else + { + $ok = $this->setBind($ok, array_values($values), $escape); + } $prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type); $where_in = [ - 'condition' => $prefix . $key . $not . " IN :{$ok}:", + 'condition' => $prefix . $key . $not . ($values instanceof Closure ? " IN ($ok)" : " IN :{$ok}:"), 'escape' => false, ]; @@ -2640,7 +2656,6 @@ protected function compileWhereHaving(string $qb_key): string { continue; } - // $matches = array( // 0 => '(test <= foo)', /* the whole thing */ // 1 => '(', /* optional */ @@ -2968,7 +2983,7 @@ protected function getOperator(string $str, bool $list = false) ]; } - return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][count($match[0]) - 1]) : false; + return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][0]) : false; } // -------------------------------------------------------------------- @@ -3013,4 +3028,16 @@ protected function setBind(string $key, $value = null, bool $escape = true): str } //-------------------------------------------------------------------- + + /** + * Returns a clone of a Base Builder with reset query builder values. + * + * @return BaseBuilder + */ + protected function cleanClone() + { + return (clone $this)->from([], true)->resetQuery(); + } + + //-------------------------------------------------------------------- } diff --git a/tests/system/Database/Builder/WhereTest.php b/tests/system/Database/Builder/WhereTest.php index 0e2dbbaa55a1..8e5597df865c 100644 --- a/tests/system/Database/Builder/WhereTest.php +++ b/tests/system/Database/Builder/WhereTest.php @@ -1,5 +1,6 @@ db->table('neworder'); + + $builder->where('advance_amount <', function (BaseBuilder $builder) { + return $builder->select('MAX(advance_amount)', false)->from('orders')->where('id >', 2); + }); + $expectedSQL = 'SELECT * FROM "neworder" WHERE "advance_amount" < (SELECT MAX(advance_amount) FROM "orders" WHERE "id" > 2)'; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + //-------------------------------------------------------------------- + public function testOrWhere() { $builder = $this->db->table('jobs'); @@ -191,6 +206,21 @@ public function testWhereIn() //-------------------------------------------------------------------- + public function testWhereInClosure() + { + $builder = $this->db->table('jobs'); + + $builder->whereIn('id', function (BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)'; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + //-------------------------------------------------------------------- + public function testWhereNotIn() { $builder = $this->db->table('jobs'); @@ -214,6 +244,21 @@ public function testWhereNotIn() //-------------------------------------------------------------------- + public function testWhereNotInClosure() + { + $builder = $this->db->table('jobs'); + + $builder->whereNotIn('id', function (BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + $expectedSQL = 'SELECT * FROM "jobs" WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)'; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + //-------------------------------------------------------------------- + public function testOrWhereIn() { $builder = $this->db->table('jobs'); @@ -241,6 +286,21 @@ public function testOrWhereIn() //-------------------------------------------------------------------- + public function testOrWhereInClosure() + { + $builder = $this->db->table('jobs'); + + $builder->where('deleted_at', null)->orWhereIn('id', function (BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + $expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)'; + + $this->assertEquals($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + //-------------------------------------------------------------------- + public function testOrWhereNotIn() { $builder = $this->db->table('jobs'); @@ -267,4 +327,19 @@ public function testOrWhereNotIn() } //-------------------------------------------------------------------- + + public function testOrWhereNotInClosure() + { + $builder = $this->db->table('jobs'); + + $builder->where('deleted_at', null)->orWhereNotIn('id', function (BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + $expectedSQL = 'SELECT * FROM "jobs" WHERE "deleted_at" IS NULL OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3)'; + + $this->assertEquals($expectedSQL, 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 613a1e2a2514..977b8e7e7f47 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -274,12 +274,21 @@ methods: $where = "name='Joe' AND status='boss' OR status='active'"; $builder->where($where); -``$builder->where()`` accepts an optional third parameter. If you set it to -FALSE, CodeIgniter will not try to protect your field or table names. + ``$builder->where()`` accepts an optional third parameter. If you set it to + FALSE, CodeIgniter will not try to protect your field or table names. -:: + :: + + $builder->where('MATCH (field) AGAINST ("value")', NULL, FALSE); + +#. **Subqueries:** + You can use an anonymous function to create a subquery. + :: - $builder->where('MATCH (field) AGAINST ("value")', NULL, FALSE); + $builder->where('advance_amount <', function(BaseBuilder $builder) { + return $builder->select('MAX(advance_amount)', false)->from('orders')->where('id >', 2); + }); + // Produces: WHERE "advance_amount" < (SELECT MAX(advance_amount) FROM "orders" WHERE "id" > 2) **$builder->orWhere()** @@ -294,44 +303,85 @@ instances are joined by OR:: Generates a WHERE field IN ('item', 'item') SQL query joined with AND if appropriate -:: + :: + + $names = ['Frank', 'Todd', 'James']; + $builder->whereIn('username', $names); + // Produces: WHERE username IN ('Frank', 'Todd', 'James') + +You can use subqueries instead of an array of values. + + :: - $names = ['Frank', 'Todd', 'James']; - $builder->whereIn('username', $names); - // Produces: WHERE username IN ('Frank', 'Todd', 'James') + $builder->whereIn('id', function(BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + // Produces: WHERE "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) **$builder->orWhereIn()** Generates a WHERE field IN ('item', 'item') SQL query joined with OR if appropriate -:: + :: + + $names = ['Frank', 'Todd', 'James']; + $builder->orWhereIn('username', $names); + // Produces: OR username IN ('Frank', 'Todd', 'James') + +You can use subqueries instead of an array of values. - $names = ['Frank', 'Todd', 'James']; - $builder->orWhereIn('username', $names); - // Produces: OR username IN ('Frank', 'Todd', 'James') + :: + + $builder->orWhereIn('id', function(BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + // Produces: OR "id" IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) **$builder->whereNotIn()** Generates a WHERE field NOT IN ('item', 'item') SQL query joined with AND if appropriate -:: + :: + + $names = ['Frank', 'Todd', 'James']; + $builder->whereNotIn('username', $names); + // Produces: WHERE username NOT IN ('Frank', 'Todd', 'James') + +You can use subqueries instead of an array of values. + + :: + + $builder->whereNotIn('id', function(BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + // Produces: WHERE "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) - $names = ['Frank', 'Todd', 'James']; - $builder->whereNotIn('username', $names); - // Produces: WHERE username NOT IN ('Frank', 'Todd', 'James') **$builder->orWhereNotIn()** Generates a WHERE field NOT IN ('item', 'item') SQL query joined with OR if appropriate -:: + :: + + $names = ['Frank', 'Todd', 'James']; + $builder->orWhereNotIn('username', $names); + // Produces: OR username NOT IN ('Frank', 'Todd', 'James') - $names = ['Frank', 'Todd', 'James']; - $builder->orWhereNotIn('username', $names); - // Produces: OR username NOT IN ('Frank', 'Todd', 'James') +You can use subqueries instead of an array of values. + + :: + + $builder->orWhereNotIn('id', function(BaseBuilder $builder) { + return $builder->select('job_id')->from('users_jobs')->where('user_id', 3); + }); + + // Produces: OR "id" NOT IN (SELECT "job_id" FROM "users_jobs" WHERE "user_id" = 3) ************************ Looking for Similar Data @@ -1125,9 +1175,9 @@ Class Reference .. php:method:: orWhereIn([$key = NULL[, $values = NULL[, $escape = NULL]]]) - :param string $key: The field to search - :param array $values: The values searched on - :param bool $escape: Whether to escape values and identifiers + :param string $key: The field to search + :param array|Closure $values: Array of target values, or anonymous function for subquery + :param bool $escape: Whether to escape values and identifiers :returns: BaseBuilder instance :rtype: object @@ -1136,9 +1186,9 @@ Class Reference .. php:method:: orWhereNotIn([$key = NULL[, $values = NULL[, $escape = NULL]]]) - :param string $key: The field to search - :param array $values: The values searched on - :param bool $escape: Whether to escape values and identifiers + :param string $key: The field to search + :param array|Closure $values: Array of target values, or anonymous function for subquery + :param bool $escape: Whether to escape values and identifiers :returns: BaseBuilder instance :rtype: object @@ -1147,9 +1197,9 @@ Class Reference .. php:method:: whereIn([$key = NULL[, $values = NULL[, $escape = NULL]]]) - :param string $key: Name of field to examine - :param array $values: Array of target values - :param bool $escape: Whether to escape values and identifiers + :param string $key: Name of field to examine + :param array|Closure $values: Array of target values, or anonymous function for subquery + :param bool $escape: Whether to escape values and identifiers :returns: BaseBuilder instance :rtype: object @@ -1158,9 +1208,9 @@ Class Reference .. php:method:: whereNotIn([$key = NULL[, $values = NULL[, $escape = NULL]]]) - :param string $key: Name of field to examine - :param array $values: Array of target values - :param bool $escape: Whether to escape values and identifiers + :param string $key: Name of field to examine + :param array|Closure $values: Array of target values, or anonymous function for subquery + :param bool $escape: Whether to escape values and identifiers :returns: BaseBuilder instance :rtype: object