Skip to content

Commit

Permalink
Impl. binary LIKE for Oracle (#1213)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek authored May 12, 2024
1 parent 3f94d16 commit 8956351
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ trait BinaryTypeCompatibilityTypecastTrait
{
private function binaryTypeValueGetPrefixConst(): string
{
return "atk_binary\ru5f8mzx4vsm8g2c9\r";
return "atk4_binary\ru5f8mzx4vsm8g2c9\r";
}

private function binaryTypeValueEncode(string $value): string
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/Sql/Oracle/PlatformTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function getVarcharTypeDeclarationSQL(array $column)
#[\Override]
public function getBinaryTypeDeclarationSQL(array $column)
{
$lengthEncodedAscii = ($column['length'] ?? 255) * 2 + strlen("atk_binary\ru5f8mzx4vsm8g2c9\r" . hash('crc32b', ''));
$lengthEncodedAscii = ($column['length'] ?? 255) * 2 + strlen("atk4_binary\ru5f8mzx4vsm8g2c9\r" . hash('crc32b', ''));
$column['length'] = intdiv($lengthEncodedAscii + 3, 4);

return $this->getVarcharTypeDeclarationSQL($column);
Expand Down
97 changes: 93 additions & 4 deletions src/Persistence/Sql/Oracle/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,93 @@ class Query extends BaseQuery
protected string $identifierEscapeChar = '"';
protected string $expressionClass = Expression::class;

/**
* @param \Closure(string, string): string $makeSqlFx
*/
protected function _renderConditionBinaryReuseBool(string $sqlLeft, string $sqlRight, \Closure $makeSqlFx, bool $nullFromArgsOnly = false): string
{
$reuse = $this->_renderConditionBinaryReuse($sqlLeft, $sqlRight, static fn () => '') !== '';

return $this->_renderConditionBinaryReuse(
$sqlLeft,
$sqlRight,
static function ($sqlLeft, $sqlRight) use ($reuse, $makeSqlFx, $nullFromArgsOnly) {
$res = $makeSqlFx($sqlLeft, $sqlRight);

if ($reuse) {
// for Oracle v23 and higher "CASE bool WHEN true THEN 1 ..." should be used
// https://dbfiddle.uk/xYhEngrA
$res = 'case when not(' . $res . ') then 0 else case when '
. ($nullFromArgsOnly ? $sqlLeft . ' is not null and ' . $sqlRight . ' is not null' : $res)
. ' then 1 end end';
}

return $res;
}
) . ($reuse ? ' = 1' : '');
}

#[\Override]
protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, string $sqlRight): string
{
return ($negated ? 'not ' : '') . $this->_renderConditionBinaryReuseBool(
$sqlLeft,
$sqlRight,
function ($sqlLeft, $sqlRight) {
$binaryPrefix = "atk4_binary\ru5f8mzx4vsm8g2c9\r";

$startsWithBinaryPrefixFx = function ($sql) use ($binaryPrefix) {
return $sql . ' like ' . $this->escapeStringLiteral($binaryPrefix . str_repeat('_', 8) . '%');
};

$binaryEncodeWithoutPrefixFx = static function ($sql) use ($binaryPrefix, $startsWithBinaryPrefixFx) {
return 'case when ' . $startsWithBinaryPrefixFx($sql) . ' then to_char(substr(' . $sql . ', ' . (strlen($binaryPrefix) + 9) . '))'
. ' else rawtohex(utl_raw.cast_to_raw(' . $sql . ')) end';
};

$replaceMultiFx = function (string $sql, array $replacements) {
$res = $sql;
foreach ($replacements as $search => $replacement) {
$res = 'replace(' . $res . ', '
. $this->escapeStringLiteral((string) $search) . ', '
. $this->escapeStringLiteral($replacement) . ')';
}

return $res;
};

return 'case when ' . $sqlLeft . ' is null or ' . $sqlRight . ' is null then null '
. 'when ' . $startsWithBinaryPrefixFx($sqlLeft) . ' or ' . $startsWithBinaryPrefixFx($sqlRight) . ' then '
. 'case when ' . $this->_renderConditionRegexpOperator(
false,
$binaryEncodeWithoutPrefixFx($sqlLeft),
'concat(' . $this->escapeStringLiteral('^') . ', concat(' . $replaceMultiFx(
$binaryEncodeWithoutPrefixFx($sqlRight),
[
bin2hex('\\\\') => 'x',
bin2hex('\_') => 'y',
bin2hex('\%') => 'z',
bin2hex('\\') => 'x',
bin2hex('_') => '..',
bin2hex('%') => '(..)*',
'x' => bin2hex('\\'),
'y' => bin2hex('_'),
'z' => bin2hex('%'),
]
) . ', ' . $this->escapeStringLiteral('$') . '))'
) . ' then 1 else 0 end'
. ' else '
. 'case when ' . parent::_renderConditionLikeOperator(
false,
$sqlLeft,
$sqlRight
) . ' then 1 else 0 end'
. ' end = 1';
},
true
);
}

#[\Override]
protected function _renderConditionRegexpOperator(bool $negated, string $sqlLeft, string $sqlRight, bool $binary = false): string
{
Expand Down Expand Up @@ -48,7 +135,7 @@ protected function _subrenderCondition(array $row): string
$operatorLc = strtolower($operator ?? '=');

if ($field instanceof Field && in_array($field->type, ['binary', 'blob'], true)
&& in_array($operatorLc, ['like', 'not like', 'regexp', 'not regexp'], true)
&& in_array($operatorLc, ['regexp', 'not regexp'], true)
) {
throw (new Exception('Unsupported binary field operator'))
->addMoreInfo('operator', $operator)
Expand All @@ -64,10 +151,12 @@ protected function _subrenderCondition(array $row): string

$row = [$this->expr('dbms_lob.compare([], [])', [$field, $value]), $operator, 0];
} elseif (in_array($operatorLc, ['like', 'not like'], true)) {
$field = $this->expr('LOWER([])', [$field]);
$value = $this->expr('LOWER([])', [$value]);
if ($field->type === 'text') {
$field = $this->expr('LOWER([])', [$field]);
$value = $this->expr('LOWER([])', [$value]);

$row = [$field, $operator, $value];
$row = [$field, $operator, $value];
}
} elseif (!in_array($operatorLc, ['regexp', 'not regexp'], true)) {
throw (new Exception('Unsupported CLOB/BLOB field operator'))
->addMoreInfo('operator', $operator)
Expand Down
10 changes: 4 additions & 6 deletions tests/ConditionSqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -555,21 +555,17 @@ public function testLikeCondition(string $type, bool $isBinary): void
return $res;
};

if ($this->getDatabasePlatform() instanceof OraclePlatform && $isBinary) {
$this->expectException(Exception::class);
$this->expectExceptionMessage('Unsupported binary field operator');
}

self::assertSame([1], $findIdsLikeFx('name', 'John'));
self::assertSame($isBinary ? [] : [1], $findIdsLikeFx('name', 'john'));
self::assertSame([10], $findIdsLikeFx('name', 'heiß'));
self::assertSame($isBinary ? [] : [10], $findIdsLikeFx('name', 'Heiß'));
self::assertSame([], $findIdsLikeFx('name', 'Joh'));
self::assertSame([1, 3], $findIdsLikeFx('name', 'Jo%'));
self::assertSame(array_values(array_diff(range(1, 14), [1, 3], $this->getDatabasePlatform() instanceof OraclePlatform ? [4] : [])), $findIdsLikeFx('name', 'Jo%', true));
self::assertSame(array_values(array_diff(range(1, 14), [1, 3], $this->getDatabasePlatform() instanceof OraclePlatform && !$isBinary ? [4] : [])), $findIdsLikeFx('name', 'Jo%', true));
self::assertSame([1], $findIdsLikeFx('name', '%John%'));
self::assertSame([1], $findIdsLikeFx('name', 'Jo%n'));
self::assertSame([1], $findIdsLikeFx('name', 'J%n'));
self::assertSame([], $findIdsLikeFx('name', '%W%')); // bin2hex('W') = substr(bin2hex('Peter'), 3, 2)
self::assertSame([1], $findIdsLikeFx('name', 'Jo_n'));
self::assertSame([], $findIdsLikeFx('name', 'J_n'));
self::assertSame($isBinary ? [] : [14], $findIdsLikeFx('name', '123_'));
Expand Down Expand Up @@ -714,6 +710,7 @@ public function testRegexpCondition(string $type, bool $isBinary): void
self::assertSame($isBinary ? [] : [13], $findIdsRegexFx('name', 'Heiß'));
self::assertSame([1], $findIdsRegexFx('name', 'Joh'));
self::assertSame([1], $findIdsRegexFx('name', 'ohn'));
self::assertSame([], $findIdsRegexFx('name', 'W'));
self::assertSame(array_values(array_diff(range(1, 17), [...($this->getDatabasePlatform() instanceof OraclePlatform ? [4] : []), 5, 6, 7, 8, 9, 10, 11, 12])), $findIdsRegexFx('name', 'a', true));

self::assertSame([1], $findIdsRegexFx('c', '1'));
Expand Down Expand Up @@ -773,6 +770,7 @@ public function testRegexpCondition(string $type, bool $isBinary): void
self::assertSame([1], $findIdsRegexFx('name', 'J.*n'));
self::assertSame([1], $findIdsRegexFx('name', 'John.*'));
self::assertSame([2], $findIdsRegexFx('c', '20*$'));
self::assertSame([], $findIdsRegexFx('name', '.*W.*'));
self::assertSame([1], $findIdsRegexFx('name', 'J.?hn'));
self::assertSame([], $findIdsRegexFx('name', 'J.?n'));
self::assertSame([], $findIdsRegexFx('c', '20?$'));
Expand Down
9 changes: 5 additions & 4 deletions tests/Persistence/Sql/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -940,17 +940,18 @@ public function testWhereLike(): void
(new MssqlQuery('[where]'))->where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0]
);

$binaryPrefix = "atk4_binary\ru5f8mzx4vsm8g2c9\r";
self::assertSame(
<<<'EOF'
where "name" like regexp_replace(:xxaaaa, '(\\[\\_%])|(\\)', '\1\2\2') escape chr(92)
where case when "name" is null or :xxaaaa is null then null when "name" like 'BBB________%' or :xxaaaa like 'BBB________%' then case when regexp_like(case when "name" like 'BBB________%' then to_char(substr("name", 38)) else rawtohex(utl_raw.cast_to_raw("name")) end, concat('^', concat(replace(replace(replace(replace(replace(replace(replace(replace(replace(case when :xxaaaa like 'BBB________%' then to_char(substr(:xxaaaa, 38)) else rawtohex(utl_raw.cast_to_raw(:xxaaaa)) end, '5c5c', 'x'), '5c5f', 'y'), '5c25', 'z'), '5c', 'x'), '5f', '..'), '25', '(..)*'), 'x', '5c'), 'y', '5f'), 'z', '25'), '$')), 'in') then 1 else 0 end else case when "name" like regexp_replace(:xxaaaa, '(\\[\\_%])|(\\)', '\1\2\2') escape chr(92) then 1 else 0 end end = 1
EOF,
(new OracleQuery('[where]'))->where('name', 'like', 'foo')->render()[0]
str_replace($binaryPrefix, 'BBB', (new OracleQuery('[where]'))->where('name', 'like', 'foo')->render()[0])
);
self::assertSame(
<<<'EOF'
where sum("a") like regexp_replace(sum("b"), '(\\[\\_%])|(\\)', '\1\2\2') escape chr(92)
where (select case when not(case when "__atk4_reuse_left__" is null or "__atk4_reuse_right__" is null then null when "__atk4_reuse_left__" like 'BBB________%' or "__atk4_reuse_right__" like 'BBB________%' then case when regexp_like(case when "__atk4_reuse_left__" like 'BBB________%' then to_char(substr("__atk4_reuse_left__", 38)) else rawtohex(utl_raw.cast_to_raw("__atk4_reuse_left__")) end, concat('^', concat(replace(replace(replace(replace(replace(replace(replace(replace(replace(case when "__atk4_reuse_right__" like 'BBB________%' then to_char(substr("__atk4_reuse_right__", 38)) else rawtohex(utl_raw.cast_to_raw("__atk4_reuse_right__")) end, '5c5c', 'x'), '5c5f', 'y'), '5c25', 'z'), '5c', 'x'), '5f', '..'), '25', '(..)*'), 'x', '5c'), 'y', '5f'), 'z', '25'), '$')), 'in') then 1 else 0 end else case when "__atk4_reuse_left__" like regexp_replace("__atk4_reuse_right__", '(\\[\\_%])|(\\)', '\1\2\2') escape chr(92) then 1 else 0 end end = 1) then 0 else case when "__atk4_reuse_left__" is not null and "__atk4_reuse_right__" is not null then 1 end end from (select sum("a") "__atk4_reuse_left__", sum("b") "__atk4_reuse_right__" from DUAL) "__atk4_reuse_tmp__") = 1
EOF,
(new OracleQuery('[where]'))->where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0]
str_replace($binaryPrefix, 'BBB', (new OracleQuery('[where]'))->where($this->e('sum({})', ['a']), 'like', $this->e('sum({})', ['b']))->render()[0])
);
}

Expand Down

0 comments on commit 8956351

Please sign in to comment.