Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Case sensitive option in LikeCondition #939

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
75 changes: 56 additions & 19 deletions src/QueryBuilder/Condition/Builder/LikeConditionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Yiisoft\Db\QueryBuilder\Condition\Builder;

use Yiisoft\Db\Command\Param;
use Yiisoft\Db\Constant\DataType;
use Yiisoft\Db\Exception\Exception;
use Yiisoft\Db\Exception\InvalidArgumentException;
use Yiisoft\Db\Exception\InvalidConfigException;
Expand Down Expand Up @@ -51,16 +53,14 @@ public function __construct(
*/
public function build(LikeConditionInterface $expression, array &$params = []): string
{
$operator = strtoupper($expression->getOperator());
$column = $expression->getColumn();
$values = $expression->getValue();
$escape = $expression->getEscapingReplacements();

if ($escape === []) {
$escape = $this->escapingReplacements;
}

[$andor, $not, $operator] = $this->parseOperator($operator);
[$andor, $not, $operator] = $this->parseOperator($expression);

if (!is_array($values)) {
$values = [$values];
Expand All @@ -70,39 +70,76 @@ public function build(LikeConditionInterface $expression, array &$params = []):
return $not ? '' : '0=1';
}

if ($column instanceof ExpressionInterface) {
$column = $this->queryBuilder->buildExpression($column, $params);
} elseif (!str_contains($column, '(')) {
$column = $this->queryBuilder->quoter()->quoteColumnName($column);
}
$column = $this->prepareColumn($expression, $params);

$parts = [];

/** @psalm-var string[] $values */
/** @psalm-var list<string|ExpressionInterface> $values */
foreach ($values as $value) {
if ($value instanceof ExpressionInterface) {
$phName = $this->queryBuilder->buildExpression($value, $params);
} else {
$phName = $this->queryBuilder->bindParam(
$escape === null ? $value : ('%' . strtr($value, $escape) . '%'),
$params
);
}
$parts[] = "$column $operator $phName$this->escapeSql";
$placeholderName = $this->preparePlaceholderName($value, $expression, $escape, $params);
$parts[] = "$column $operator $placeholderName$this->escapeSql";
}

return implode($andor, $parts);
}

/**
* Prepare column to use in SQL.
*
* @throws Exception
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws NotSupportedException
*/
protected function prepareColumn(LikeConditionInterface $expression, array &$params): string
{
$column = $expression->getColumn();

if ($column instanceof ExpressionInterface) {
return $this->queryBuilder->buildExpression($column, $params);
}

if (!str_contains($column, '(')) {
return $this->queryBuilder->quoter()->quoteColumnName($column);
}

return $column;
}

/**
* Prepare value to use in SQL.
*
* @throws Exception
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws NotSupportedException
* @return string
*/
protected function preparePlaceholderName(
string|ExpressionInterface $value,
LikeConditionInterface $expression,
array|null $escape,
array &$params,
): string {
if ($value instanceof ExpressionInterface) {
return $this->queryBuilder->buildExpression($value, $params);
}
return $this->queryBuilder->bindParam(
new Param($escape === null ? $value : ('%' . strtr($value, $escape) . '%'), DataType::STRING),
$params
);
}

/**
* Parses operator and returns its parts.
*
* @throws InvalidArgumentException
*
* @psalm-return array{0: string, 1: bool, 2: string}
*/
protected function parseOperator(string $operator): array
protected function parseOperator(LikeConditionInterface $expression): array
{
$operator = strtoupper($expression->getOperator());
if (!preg_match('/^(AND |OR |)((NOT |)I?LIKE)/', $operator, $matches)) {
throw new InvalidArgumentException("Invalid operator in like condition: \"$operator\"");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface ConditionInterface extends ExpressionInterface
* Creates object by array-definition.
*
* @param string $operator Operator in uppercase.
* @param array $operands Array of corresponding operands
* @param array $operands Array of corresponding operands
*
* @throws InvalidArgumentException If input parameters aren't suitable for this condition.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,9 @@ public function getOperator(): string;
* @return array|ExpressionInterface|int|Iterator|string|null The value to the right of {@see operator}.
*/
public function getValue(): array|int|string|Iterator|ExpressionInterface|null;

/**
* @return bool|null Whether the comparison is case-sensitive. `null` means using the default behavior.
*/
public function getCaseSensitive(): ?bool;
}
13 changes: 10 additions & 3 deletions src/QueryBuilder/Condition/LikeCondition.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ final class LikeCondition implements LikeConditionInterface
protected array|null $escapingReplacements = [];

public function __construct(
private string|ExpressionInterface $column,
private string $operator,
private array|int|string|Iterator|ExpressionInterface|null $value
private readonly string|ExpressionInterface $column,
private readonly string $operator,
private readonly array|int|string|Iterator|ExpressionInterface|null $value,
private readonly ?bool $caseSensitive = null,
) {
}

Expand All @@ -48,6 +49,11 @@ public function getValue(): array|int|string|Iterator|ExpressionInterface|null
return $this->value;
}

public function getCaseSensitive(): ?bool
{
return $this->caseSensitive;
}

public function setEscapingReplacements(array|null $escapingReplacements): void
{
$this->escapingReplacements = $escapingReplacements;
Expand All @@ -68,6 +74,7 @@ public static function fromArrayDefinition(string $operator, array $operands): s
self::validateColumn($operator, $operands[0]),
$operator,
self::validateValue($operator, $operands[1]),
isset($operands['caseSensitive']) ? (bool) $operands['caseSensitive'] : null,
);

if (array_key_exists(2, $operands) && (is_array($operands[2]) || $operands[2] === null)) {
Expand Down
29 changes: 12 additions & 17 deletions tests/AbstractQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,7 @@ public function testBatchInsert(
$this->assertSame($expectedParams, $params);
}

/**
* @dataProvider \Yiisoft\Db\Tests\Provider\QueryBuilderProvider::buildCondition
*
* @throws Exception
* @throws InvalidConfigException
* @throws InvalidArgumentException
* @throws NotSupportedException
*/
#[DataProviderExternal(QueryBuilderProvider::class, 'buildCondition')]
public function testBuildCondition(
array|ExpressionInterface|string $condition,
string|null $expected,
Expand Down Expand Up @@ -451,14 +444,7 @@ public function testBuildJoin(): void
);
}

/**
* @dataProvider \Yiisoft\Db\Tests\Provider\QueryBuilderProvider::buildLikeCondition
*
* @throws Exception
* @throws InvalidConfigException
* @throws InvalidArgumentException
* @throws NotSupportedException
*/
#[DataProviderExternal(QueryBuilderProvider::class, 'buildLikeCondition')]
public function testBuildLikeCondition(
array|ExpressionInterface $condition,
string $expected,
Expand All @@ -476,7 +462,16 @@ public function testBuildLikeCondition(
. (empty($expected) ? '' : ' WHERE ' . DbHelper::replaceQuotes($expected, $db->getDriverName())),
$sql
);
$this->assertSame($expectedParams, $params);
$this->assertSame(array_keys($expectedParams), array_keys($params));
foreach ($params as $name => $value) {
if ($value instanceof Param) {
$this->assertInstanceOf(Param::class, $expectedParams[$name]);
$this->assertSame($expectedParams[$name]->getValue(), $value->getValue());
$this->assertSame($expectedParams[$name]->getType(), $value->getType());
} else {
$this->assertSame($expectedParams[$name], $value);
}
}
}

public function testBuildLimit(): void
Expand Down
55 changes: 55 additions & 0 deletions tests/Common/CommonQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\Db\Tests\Common;

use PHPUnit\Framework\Attributes\DataProvider;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Query\Query;
use Yiisoft\Db\Tests\AbstractQueryTest;
Expand Down Expand Up @@ -81,4 +82,58 @@ public function testSelectWithoutFrom()

$db->close();
}

public function testLikeDefaultCaseSensitive(): void
{
$db = $this->getConnection(true);

$result = (new Query($db))
->select('name')
->from('customer')
->where(['like', 'name', 'user1'])
->scalar();


$this->assertSame('user1', $result);
}

public static function dataLikeCaseSensitive(): iterable
{
yield 'sameCase' => ['user1', 'user1'];
yield 'otherCase' => [false, 'USER1'];
}

#[DataProvider('dataLikeCaseSensitive')]
public function testLikeCaseSensitive(mixed $expected, string $value): void
{
$db = $this->getConnection(true);

$result = (new Query($db))
->select('name')
->from('customer')
->where(['like', 'name', $value, 'caseSensitive' => true])
->scalar();

$this->assertSame($expected, $result);
}

public static function dataLikeCaseInsensitive(): iterable
{
yield 'sameCase' => ['user1', 'user1'];
yield 'otherCase' => ['user1', 'USER1'];
}

#[DataProvider('dataLikeCaseInsensitive')]
public function testLikeCaseInsensitive(mixed $expected, string $value): void
{
$db = $this->getConnection(true);

$result = (new Query($db))
->select('name')
->from('customer')
->where(['like', 'name', $value, 'caseSensitive' => false])
->scalar();

$this->assertSame($expected, $result);
}
}
17 changes: 15 additions & 2 deletions tests/Db/QueryBuilder/Condition/LikeConditionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

namespace Yiisoft\Db\Tests\Db\QueryBuilder\Condition;

use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\TestCase;
use Yiisoft\Db\Exception\InvalidArgumentException;
use Yiisoft\Db\QueryBuilder\Condition\LikeCondition;

/**
* @group db
*
* @psalm-suppress PropertyNotSetInConstructor
*/
final class LikeConditionTest extends TestCase
{
Expand All @@ -22,6 +21,7 @@ public function testConstructor(): void
$this->assertSame('id', $likeCondition->getColumn());
$this->assertSame('LIKE', $likeCondition->getOperator());
$this->assertSame('test', $likeCondition->getValue());
$this->assertNull($likeCondition->getCaseSensitive());
}

public function testFromArrayDefinition(): void
Expand Down Expand Up @@ -76,4 +76,17 @@ public function testSetEscapingReplacements(): void

$this->assertSame(['%' => '\%', '_' => '\_'], $likeCondition->getEscapingReplacements());
}

#[TestWith([null])]
#[TestWith([true])]
#[TestWith([false])]
public function testFromArrayDefinitionCaseSensitive(?bool $caseSensitive): void
{
$likeCondition = LikeCondition::fromArrayDefinition('LIKE', ['id', 'test', 'caseSensitive' => $caseSensitive]);

$this->assertSame('id', $likeCondition->getColumn());
$this->assertSame('LIKE', $likeCondition->getOperator());
$this->assertSame('test', $likeCondition->getValue());
$this->assertSame($caseSensitive, $likeCondition->getCaseSensitive());
}
}
Loading