Skip to content

Commit

Permalink
Fix basic binary LIKE/REGEXP for PostgreSQL
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Apr 29, 2024
1 parent 77fc8e2 commit e741a8c
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 43 deletions.
2 changes: 1 addition & 1 deletion src/Persistence/Sql/Expression.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ public function getDebugQuery(): string

$i = 0;
$sql = preg_replace_callback(
'~' . self::QUOTED_TOKEN_REGEX . '\K|(?:\?|:\w+)~',
'~' . self::QUOTED_TOKEN_REGEX . '\K|(?:\?|(?<!:):\w+)~',
function ($matches) use ($params, &$i) {
if ($matches[0] === '') {
return '';
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/Sql/Postgresql/ExpressionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ protected function updateRenderBeforeExecute(array $render): array
[$sql, $params] = parent::updateRenderBeforeExecute($render);

$sql = preg_replace_callback(
'~' . self::QUOTED_TOKEN_REGEX . '\K|:\w+~',
'~' . self::QUOTED_TOKEN_REGEX . '\K|(?<!:):\w+~',
static function ($matches) use ($params) {
if ($matches[0] === '') {
return '';
Expand Down
54 changes: 30 additions & 24 deletions src/Persistence/Sql/Postgresql/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,45 @@ class Query extends BaseQuery
protected string $templateUpdate = 'update [table][join] set [set] [where]';
protected string $templateReplace;

#[\Override]
protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, string $sqlRight): string
/**
* @param \Closure(string, string): string $makeSqlFx
*/
private function _renderConditionConditionalCastToText(string $sqlLeft, string $sqlRight, \Closure $makeSqlFx): string
{
$sqlRightEscaped = 'regexp_replace(' . $sqlRight . ', '
. $this->escapeStringLiteral('(\\\[\\\_%])|(\\\)') . ', '
. $this->escapeStringLiteral('\1\2\2') . ', '
. $this->escapeStringLiteral('g') . ')';

return $sqlLeft . ($negated ? ' not' : '') . ' like ' . $sqlRightEscaped
. ' escape ' . $this->escapeStringLiteral('\\');
return $this->_renderConditionBinaryReuse(
$sqlLeft,
$sqlRight,
function ($sqlLeft, $sqlRight) use ($makeSqlFx) {
return 'case when pg_typeof(' . $sqlLeft . ') = ' . $this->escapeStringLiteral('bytea') . '::regtype'
. ' then ' . $makeSqlFx('convert_from(cast(cast(' . $sqlLeft . ' as text) as bytea), '
. $this->escapeStringLiteral('UTF8') . ')', $sqlRight) // will raise SQL error for 0x00 or 0x80+ bytes
. ' else ' . $makeSqlFx('cast(' . $sqlLeft . ' as citext)', $sqlRight)
. ' end';
}
);
}

// needed for PostgreSQL v14 and lower
#[\Override]
protected function _renderConditionRegexpOperator(bool $negated, string $sqlLeft, string $sqlRight, bool $binary = false): string
protected function _renderConditionLikeOperator(bool $negated, string $sqlLeft, string $sqlRight): string
{
return $sqlLeft . ' ' . ($negated ? '!' : '') . '~ ' . $sqlRight;
return $this->_renderConditionConditionalCastToText($sqlLeft, $sqlRight, function ($sqlLeft, $sqlRight) use ($negated) {
$sqlRightEscaped = 'regexp_replace(' . $sqlRight . ', '
. $this->escapeStringLiteral('(\\\[\\\_%])|(\\\)') . ', '
. $this->escapeStringLiteral('\1\2\2') . ', '
. $this->escapeStringLiteral('g') . ')';

return $sqlLeft . ($negated ? ' not' : '') . ' like ' . $sqlRightEscaped
. ' escape ' . $this->escapeStringLiteral('\\');
});
}

// needed for PostgreSQL v14 and lower
#[\Override]
protected function _subrenderCondition(array $row): string
protected function _renderConditionRegexpOperator(bool $negated, string $sqlLeft, string $sqlRight, bool $binary = false): string
{
if (count($row) !== 1) {
[$field, $operator, $value] = $row;

if (in_array(strtolower($operator ?? '='), ['like', 'not like', 'regexp', 'not regexp'], true)) {
$field = new Expression('CAST([] AS citext)', [$field]);

$row = [$field, $operator, $value];
}
}

return parent::_subrenderCondition($row);
return $this->_renderConditionConditionalCastToText($sqlLeft, $sqlRight, static function ($sqlLeft, $sqlRight) use ($negated) {
return $sqlLeft . ' ' . ($negated ? '!' : '') . '~ ' . $sqlRight;
});
}

#[\Override]
Expand Down
18 changes: 4 additions & 14 deletions tests/ConditionSqlTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -544,12 +544,6 @@ public function testLikeCondition(string $type, bool $isBinary): void
return $res;
};

if ($this->getDatabasePlatform() instanceof PostgreSQLPlatform && $isBinary) {
self::assertTrue(true); // @phpstan-ignore-line

return; // TODO
}

if ($this->getDatabasePlatform() instanceof SQLServerPlatform && $isBinary) {
self::assertTrue(true); // @phpstan-ignore-line

Expand Down Expand Up @@ -683,12 +677,6 @@ public function testRegexpCondition(string $type, bool $isBinary): void
self::markTestIncomplete('MSSQL has no REGEXP support yet');
}

if ($this->getDatabasePlatform() instanceof PostgreSQLPlatform && $isBinary) {
self::assertTrue(true); // @phpstan-ignore-line

return; // TODO
}

if ($this->getDatabasePlatform() instanceof OraclePlatform && $isBinary) {
$this->expectException(Exception::class);
$this->expectExceptionMessage('Unsupported binary field operator');
Expand All @@ -703,7 +691,8 @@ public function testRegexpCondition(string $type, bool $isBinary): void

self::assertSame([1], $findIdsRegexFx('name', 'John'));
self::assertSame($isBinary ? [] : [1], $findIdsRegexFx('name', 'john'));
self::assertSame($this->getDatabasePlatform() instanceof MySQLPlatform && $isBinary && !$isMysql5x && !$isMariadb ? [] : [13], $findIdsRegexFx('name', 'heiß')); // TODO investigate/report MySQL 8.x bug
// TODO investigate/report MySQL 8.x bug
self::assertSame($this->getDatabasePlatform() instanceof MySQLPlatform && $isBinary && !$isMysql5x && !$isMariadb ? [] : [13], $findIdsRegexFx('name', 'heiß'));
self::assertSame($isBinary ? [] : [13], $findIdsRegexFx('name', 'Heiß'));
self::assertSame([1], $findIdsRegexFx('name', 'Joh'));
self::assertSame([1], $findIdsRegexFx('name', 'ohn'));
Expand Down Expand Up @@ -776,7 +765,8 @@ public function testRegexpCondition(string $type, bool $isBinary): void
self::assertSame([5, 6], $findIdsRegexFx('name', 'Sa\s'));
self::assertSame([7, 8, 9, 10, 11, 12], $findIdsRegexFx('name', 'Sa\S'));
self::assertSame([1, 3], $findIdsRegexFx('name', '\wo'));
self::assertSame($this->getDatabasePlatform() instanceof MySQLPlatform && $isBinary ? [] : [13], $findIdsRegexFx('name', 'hei\w$')); // TODO align SQLite with MySQL
// TODO align SQLite binary behaviour with MySQL
self::assertSame($isBinary && ($this->getDatabasePlatform() instanceof MySQLPlatform || $this->getDatabasePlatform() instanceof PostgreSQLPlatform) ? [] : [13], $findIdsRegexFx('name', 'hei\w$'));
self::assertSame([10], $findIdsRegexFx('name', '\W\\\\'));
if ($type !== 'string' && !$this->getDatabasePlatform() instanceof OraclePlatform) {
self::assertSame([5], $findIdsRegexFx('name', '\x20'));
Expand Down
4 changes: 2 additions & 2 deletions tests/Persistence/Sql/QueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ public function testWhereSpecialValues(): void
}
self::assertSame(
<<<'EOF'
where CAST(:a AS citext) like regexp_replace(:b, '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92)
where case when pg_typeof("name") = 'bytea'::regtype then convert_from(cast(cast("name" as text) as bytea), 'UTF8') like regexp_replace(:a, '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) else cast("name" as citext) like regexp_replace(:a, '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) end
EOF,
(new PostgresqlQuery('[where]'))->where('name', 'like', 'foo')->render()[0]
);
Expand Down Expand Up @@ -927,7 +927,7 @@ public function testWhereSpecialValues(): void
}
self::assertSame(
<<<'EOF'
where CAST(:a AS citext) ~ :b
where case when pg_typeof("name") = 'bytea'::regtype then convert_from(cast(cast("name" as text) as bytea), 'UTF8') ~ :a else cast("name" as citext) ~ :a end
EOF,
(new PostgresqlQuery('[where]'))->where('name', 'regexp', 'foo')->render()[0]
);
Expand Down
2 changes: 1 addition & 1 deletion tests/Schema/MigratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public function testCharacterTypeFieldCaseSensitivity(string $type, bool $isBina
$model->addCondition('v', 'in', ['MixedCase', 'foo']);
self::assertSameExportUnordered($expectedExport, $model->export(['id']));

if (!$this->getDatabasePlatform() instanceof PostgreSQLPlatform && !$this->getDatabasePlatform() instanceof SQLServerPlatform && !$this->getDatabasePlatform() instanceof OraclePlatform) {
if (!$this->getDatabasePlatform() instanceof SQLServerPlatform && !$this->getDatabasePlatform() instanceof OraclePlatform) {
$model->scope()->clear();
$model->addCondition('v', 'like', 'MixedCase');
self::assertSameExportUnordered($expectedExport, $model->export(['id']));
Expand Down

0 comments on commit e741a8c

Please sign in to comment.