Skip to content

Commit

Permalink
Make LIKE operator escapable
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Apr 15, 2024
1 parent c2baf07 commit 2822a19
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 6 deletions.
12 changes: 12 additions & 0 deletions src/Persistence/Sql/Oracle/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ class Query extends BaseQuery
protected string $identifierEscapeChar = '"';
protected string $expressionClass = Expression::class;

#[\Override]
protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, string $sqlRight): string
{
$sqlRight = 'regexp_replace(regexp_replace(' . $sqlRight
. ', ' . $this->escapeStringLiteral('((\\\\\\\)*)(\\\([^_%]))?')
. ', ' . $this->escapeStringLiteral('\1\4')
. '), ' . $this->escapeStringLiteral('((^|[^\\\])(\\\\\\\)*\\\)$')
. ', ' . $this->escapeStringLiteral('\1\\\\') . ')';

return parent::_renderConditionLikeOperator($negated, $sqlLeft, $sqlRight);
}

#[\Override]
public function render(): array
{
Expand Down
10 changes: 10 additions & 0 deletions src/Persistence/Sql/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,12 @@ protected function _renderConditionInOperator(bool $negated, string $sqlLeft, ar
return $sqlLeft . ($negated ? ' not' : '') . ' in (' . implode(', ', $sqlValues) . ')';
}

protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, string $sqlRight): string
{
return $sqlLeft . ($negated ? ' not' : '') . ' like ' . $sqlRight
. ' escape ' . $this->escapeStringLiteral('\\');
}

/**
* @param array{mixed}|array{mixed, string|null, mixed} $row
*/
Expand Down Expand Up @@ -605,6 +611,10 @@ protected function _subrenderCondition(array $row): string
// otherwise just escape value
$value = $this->consume($value, self::ESCAPE_PARAM);

if (in_array($operator, ['like', 'not like'], true)) {
return $this->_renderConditionLikeOperator($operator === 'not like', $field, $value);
}

return $this->_renderConditionBinary($operator, $field, $value);
}

Expand Down
19 changes: 16 additions & 3 deletions tests/ConditionSqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ public function testLikeCondition(): void
['id' => 2, 'name' => 'Peter', 'c' => 2000],
['id' => 3, 'name' => 'Joe', 'c' => 50],
['id' => 4, 'name' => 'Ca%ro_li\ne', 'c' => null],
['id' => 5, 'name' => 'Ca.ro.li.ne', 'c' => null],
['id' => 5, 'name' => 'Ca.ro.li\\\ne', 'c' => null],
],
]);

Expand Down Expand Up @@ -541,8 +541,21 @@ public function testLikeCondition(): void
self::assertSame([1], $findIdsLikeFx('c', '%0%', true));

self::assertSame([4, 5], $findIdsLikeFx('name', '%Ca%ro%'));
self::assertSame([4], $findIdsLikeFx('name', '%Ca\%ro%'));

self::assertSame([4, 5], $findIdsLikeFx('name', '%ro_li%'));
// self::assertSame([4, 5], $findIdsLikeFx('name', '%li_\ne%'));
// self::assertSame([4], $findIdsLikeFx('name', '%li\\ne%'));
self::assertSame([4], $findIdsLikeFx('name', '%ro\_li%'));

self::assertSame([], $findIdsLikeFx('name', '%li\ne%'));
self::assertSame([4, 5], $findIdsLikeFx('name', '%li%\ne%'));
self::assertSame([4], $findIdsLikeFx('name', '%li\\\ne%'));
self::assertSame([4], $findIdsLikeFx('name', '%li\\\\\ne%'));
self::assertSame([5], $findIdsLikeFx('name', '%li\\\\\\\ne%'));
self::assertSame([5], $findIdsLikeFx('name', '%li\\\\\\\\\ne%'));
self::assertSame([], $findIdsLikeFx('name', '%li\\\\\\\\\\\ne%'));
self::assertSame([5], $findIdsLikeFx('name', '%.li%ne'));
self::assertSame([], $findIdsLikeFx('name', '%.li%ne\\'));
self::assertSame([], $findIdsLikeFx('name', '%.li%ne\\\\'));
self::assertSame([], $findIdsLikeFx('name', '%*li%ne'));
}
}
17 changes: 14 additions & 3 deletions tests/Persistence/Sql/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Atk4\Data\Persistence\Sql\Mysql\Connection as MysqlConnection;
use Atk4\Data\Persistence\Sql\Mysql\Expression as MysqlExpression;
use Atk4\Data\Persistence\Sql\Mysql\Query as MysqlQuery;
use Atk4\Data\Persistence\Sql\Oracle\Query as OracleQuery;
use Atk4\Data\Persistence\Sql\Query;
use Atk4\Data\Persistence\Sql\Sqlite\Connection as SqliteConnection;
use PHPUnit\Framework\Attributes\DataProvider;
Expand Down Expand Up @@ -49,6 +50,10 @@ protected function escapeStringLiteral(string $value): string
#[\Override]
protected function escapeStringLiteral(string $value): string
{
if ($value === '\\') {
return '\'\\\'';
}

return null; // @phpstan-ignore-line
}
};
Expand Down Expand Up @@ -772,13 +777,19 @@ public function testWhereSpecialValues(): void

// like | not like
self::assertSame(
'where "name" like :a',
'where "name" like :a escape \'\\\'',
$this->q('[where]')->where('name', 'like', 'foo')->render()[0]
);
self::assertSame(
'where "name" not like :a',
'where "name" not like :a escape \'\\\'',
$this->q('[where]')->where('name', 'not like', 'foo')->render()[0]
);
self::assertSame(
<<<'EOF'
where "name" like regexp_replace(regexp_replace(:xxaaaa, '((\\\\)*)(\\([^_%]))?', '\1\4'), '((^|[^\\])(\\\\)*\\)$', concat('\1', rpad(chr(92), 2, chr(92)))) escape chr(92)
EOF,
(new OracleQuery('[where]'))->where('name', 'like', 'foo')->render()[0]
);
}

public function testWhereInWithNullException(): void
Expand Down Expand Up @@ -1328,7 +1339,7 @@ public function testCaseExprNormal(): void
->caseWhen(['status', 'like', '%Used%'], 't2.expose_used')
->caseElse(null)
->render()[0];
self::assertSame('case when "status" = :a then :b when "status" like :c then :d else :e end', $s);
self::assertSame('case when "status" = :a then :b when "status" like :c escape \'\\\' then :d else :e end', $s);

// with subqueries
$age = $this->e('year(now()) - year(birth_date)');
Expand Down

0 comments on commit 2822a19

Please sign in to comment.