diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 51960986de54..6a19032eda5a 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -932,7 +932,7 @@ protected function _whereIn(?string $key = null, $values = null, bool $not = fal * Generates a %LIKE% portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -945,7 +945,7 @@ public function like($field, string $match = '', string $side = 'both', ?bool $e * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -958,7 +958,7 @@ public function notLike($field, string $match = '', string $side = 'both', ?bool * Generates a %LIKE% portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -971,7 +971,7 @@ public function orLike($field, string $match = '', string $side = 'both', ?bool * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -984,7 +984,7 @@ public function orNotLike($field, string $match = '', string $side = 'both', ?bo * Generates a %LIKE% portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -997,7 +997,7 @@ public function havingLike($field, string $match = '', string $side = 'both', ?b * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'AND'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -1010,7 +1010,7 @@ public function notHavingLike($field, string $match = '', string $side = 'both', * Generates a %LIKE% portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -1023,7 +1023,7 @@ public function orHavingLike($field, string $match = '', string $side = 'both', * Generates a NOT LIKE portion of the query. * Separates multiple calls with 'OR'. * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ @@ -1042,20 +1042,54 @@ public function orNotHavingLike($field, string $match = '', string $side = 'both * @used-by notHavingLike() * @used-by orNotHavingLike() * - * @param mixed $field + * @param array|RawSql|string $field * * @return $this */ protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', ?bool $escape = null, bool $insensitiveSearch = false, string $clause = 'QBWhere') { - if (! is_array($field)) { - $field = [$field => $match]; - } - $escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers; $side = strtolower($side); - foreach ($field as $k => $v) { + if ($field instanceof RawSql) { + $k = (string) $field; + $v = $match; + $insensitiveSearch = false; + + $prefix = empty($this->{$clause}) ? $this->groupGetType('') : $this->groupGetType($type); + + if ($side === 'none') { + $bind = $this->setBind($field->getBindingKey(), $v, $escape); + } elseif ($side === 'before') { + $bind = $this->setBind($field->getBindingKey(), "%{$v}", $escape); + } elseif ($side === 'after') { + $bind = $this->setBind($field->getBindingKey(), "{$v}%", $escape); + } else { + $bind = $this->setBind($field->getBindingKey(), "%{$v}%", $escape); + } + + $likeStatement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch); + + // some platforms require an escape sequence definition for LIKE wildcards + if ($escape === true && $this->db->likeEscapeStr !== '') { + $likeStatement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar); + } + + $this->{$clause}[] = [ + 'condition' => $field->with($likeStatement), + 'escape' => $escape, + ]; + + return $this; + } + + if (! is_array($field)) { + $keyValue = [$field => $match]; + } else { + $keyValue = $field; + } + + foreach ($keyValue as $k => $v) { if ($insensitiveSearch === true) { $v = strtolower($v); } @@ -2430,6 +2464,12 @@ protected function compileWhereHaving(string $qbKey): string continue; } + if ($qbkey['condition'] instanceof RawSql) { + $qbkey = $qbkey['condition']; + + continue; + } + if ($qbkey['escape'] === false) { $qbkey = $qbkey['condition']; diff --git a/tests/system/Database/Builder/LikeTest.php b/tests/system/Database/Builder/LikeTest.php index 2d81db2b90fa..22afc5c7549c 100644 --- a/tests/system/Database/Builder/LikeTest.php +++ b/tests/system/Database/Builder/LikeTest.php @@ -12,6 +12,7 @@ namespace CodeIgniter\Database\Builder; use CodeIgniter\Database\BaseBuilder; +use CodeIgniter\Database\RawSql; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; @@ -47,6 +48,29 @@ public function testSimpleLike() $this->assertSame($expectedBinds, $builder->getBinds()); } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/3970 + */ + public function testLikeWithRawSql() + { + $builder = new BaseBuilder('users', $this->db); + + $sql = "concat(users.name, ' ', IF(users.surname IS NULL or users.surname = '', '', users.surname))"; + $rawSql = new RawSql($sql); + $builder->like($rawSql, 'value', 'both'); + + $expectedSQL = "SELECT * FROM \"users\" WHERE {$sql} LIKE '%value%' ESCAPE '!' "; + $expectedBinds = [ + $rawSql->getBindingKey() => [ + '%value%', + true, + ], + ]; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + $this->assertSame($expectedBinds, $builder->getBinds()); + } + public function testLikeNoSide() { $builder = new BaseBuilder('job', $this->db);