diff --git a/src/Persistence/Sql/Postgresql/Query.php b/src/Persistence/Sql/Postgresql/Query.php index a5adbd86b..5be49dfe2 100644 --- a/src/Persistence/Sql/Postgresql/Query.php +++ b/src/Persistence/Sql/Postgresql/Query.php @@ -34,23 +34,38 @@ function ($sqlLeft, $sqlRight) use ($makeSqlFx) { }; $escapeNonUtf8Fx = function ($sql, $neverBytea = false) use ($iffByteaSqlFx) { - $byteaSql = 'cast(replace(cast(' . $sql . ' as text), ' . $this->escapeStringLiteral('\\') - . ', ' . $this->escapeStringLiteral('\\\\') . ') as bytea)'; - - $sql = $neverBytea - ? $byteaSql - : $iffByteaSqlFx( + $doubleBackslashesFx = function ($sql) { + return 'replace(' . $sql . ', ' . $this->escapeStringLiteral('\\') + . ', ' . $this->escapeStringLiteral('\\\\') . ')'; + }; + + $byteaSql = 'cast(' . $doubleBackslashesFx('cast(' . $sql . ' as text)') . ' as bytea)'; + if (!$neverBytea) { + $byteaSql = $iffByteaSqlFx( $sql, 'decode(' . $iffByteaSqlFx( $sql, - 'substring(cast(' . $sql . ' as text) from 3)', + $doubleBackslashesFx('substring(cast(' . $sql . ' as text) from 3)'), $this->escapeStringLiteral('') ) . ', ' . $this->escapeStringLiteral('hex') . ')', $byteaSql ); + } + + // 0x00 and 0x80+ bytes will be escaped as "\xddd" + $res = 'encode(' . $byteaSql . ', ' . $this->escapeStringLiteral('escape') . ')'; + + // replace backslash in "\xddd" for LIKE/REGEXP + $res = 'regexp_replace(' . $res . ', ' + . $this->escapeStringLiteral('(?escapeStringLiteral('\1~~bytea~\3~~') . ', ' + . $this->escapeStringLiteral('g') . ')'; + + // revert double backslashes + $res = 'replace(' . $res . ', ' . $this->escapeStringLiteral('\\\\') + . ', ' . $this->escapeStringLiteral('\\') . ')'; - return 'replace(encode(' . $sql . ', ' . $this->escapeStringLiteral('escape') . '), ' - . $this->escapeStringLiteral('\\\\') . ', ' . $this->escapeStringLiteral('\\') . ')'; + return $res; }; return $iffByteaSqlFx( diff --git a/tests/ConditionSqlTest.php b/tests/ConditionSqlTest.php index 0415af9c4..55e7bc3bb 100644 --- a/tests/ConditionSqlTest.php +++ b/tests/ConditionSqlTest.php @@ -530,6 +530,9 @@ public function testLikeCondition(string $type, bool $isBinary): void ['name' => 'Ja[n]e'], ['name' => 'Ja\[^n]e'], ['name' => 'heiß'], + ['name' => 'hei\ß'], + ['name' => 'hei\\\ß'], + ['name' => 'hei\123'], ]); $findIdsLikeFx = function (string $field, string $value, bool $negated = false) use ($u) { @@ -567,7 +570,7 @@ public function testLikeCondition(string $type, bool $isBinary): void self::assertSame($isBinary ? [] : [9], $findIdsLikeFx('name', 'Heiß')); self::assertSame([], $findIdsLikeFx('name', 'Joh')); self::assertSame([1, 3], $findIdsLikeFx('name', 'Jo%')); - self::assertSame(array_values(array_diff(range(1, 9), [1, 3])), $findIdsLikeFx('name', 'Jo%', true)); + self::assertSame(array_values(array_diff(range(1, 12), [1, 3])), $findIdsLikeFx('name', 'Jo%', true)); self::assertSame([1], $findIdsLikeFx('name', '%John%')); self::assertSame([1], $findIdsLikeFx('name', 'Jo%n')); self::assertSame([1], $findIdsLikeFx('name', 'J%n')); @@ -607,6 +610,14 @@ public function testLikeCondition(string $type, bool $isBinary): void self::assertSame([], $findIdsLikeFx('name', '%li\\\\\%e%')); self::assertSame([5], $findIdsLikeFx('name', '%li\\\\\\\%e%')); self::assertSame([], $findIdsLikeFx('name', '%li\\\\\\\\\%e%')); + self::assertSame([10], $findIdsLikeFx('name', 'hei\ß')); + self::assertSame([10], $findIdsLikeFx('name', 'hei\\\ß')); + self::assertSame([11], $findIdsLikeFx('name', 'hei\\\\\ß')); + self::assertSame([11], $findIdsLikeFx('name', 'hei\\\\\\\ß')); + self::assertSame([], $findIdsLikeFx('name', 'hei\\\\\\\\\ß')); + self::assertSame([12], $findIdsLikeFx('name', 'hei\123')); + self::assertSame([12], $findIdsLikeFx('name', 'hei\\\123')); + self::assertSame([], $findIdsLikeFx('name', 'hei\\\\\123')); self::assertSame([4], $findIdsLikeFx('name', '%l_\ne%')); self::assertSame([5], $findIdsLikeFx('name', '%l__\ne%')); @@ -663,6 +674,9 @@ public function testRegexpCondition(string $type, bool $isBinary): void ['name' => 'Sa~ra'], ['name' => 'Sa$ra'], ['name' => 'heiß'], + ['name' => 'hei\ß'], + ['name' => 'hei\\\ß'], + ['name' => 'hei\123'], ]); if ($this->getDatabasePlatform() instanceof SQLServerPlatform) { @@ -710,7 +724,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([1, 2, 3, ...($this->getDatabasePlatform() instanceof OraclePlatform ? [] : [4]), 13], $findIdsRegexFx('name', 'a', true)); + self::assertSame([1, 2, 3, ...($this->getDatabasePlatform() instanceof OraclePlatform ? [] : [4]), 13, 14, 15, 16], $findIdsRegexFx('name', 'a', true)); self::assertSame([1], $findIdsRegexFx('c', '1')); self::assertSame([2], $findIdsRegexFx('c', '2000')); @@ -719,9 +733,14 @@ public function testRegexpCondition(string $type, bool $isBinary): void self::assertSame([1, 2], $findIdsRegexFx('rating', '\.5')); self::assertSame([2], $findIdsRegexFx('rating', '2\.5')); - self::assertSame([9, 10], $findIdsRegexFx('name', '\\\\')); - self::assertSame([10], $findIdsRegexFx('name', '\\\\\\\\')); + self::assertSame([9, 10, 14, 15, 16], $findIdsRegexFx('name', '\\\\')); + self::assertSame([10, 15], $findIdsRegexFx('name', '\\\\\\\\')); self::assertSame([], $findIdsRegexFx('name', '\\\\\\\\\\\\')); + self::assertSame([14], $findIdsRegexFx('name', 'hei\\\ß')); + self::assertSame([15], $findIdsRegexFx('name', 'hei\\\\\\\ß')); + self::assertSame([], $findIdsRegexFx('name', 'hei\\\\\\\\\\\ß')); + self::assertSame([16], $findIdsRegexFx('name', 'hei\\\123')); + self::assertSame([], $findIdsRegexFx('name', 'hei\\\\\\\123')); self::assertSame([7], $findIdsRegexFx('name', '\.')); self::assertSame([12], $findIdsRegexFx('name', '\$')); self::assertSame([8], $findIdsRegexFx('name', '/ra')); @@ -739,12 +758,12 @@ public function testRegexpCondition(string $type, bool $isBinary): void self::assertSame([5], $findIdsRegexFx('name', '\ ra')); } - self::assertSame([2, 3, 13], $findIdsRegexFx('name', '.e')); - self::assertSame(array_values(array_diff(range(1, 13), [4])), $findIdsRegexFx('name', '.')); + self::assertSame([2, 3, 13, 14, 15, 16], $findIdsRegexFx('name', '.e')); + self::assertSame(array_values(array_diff(range(1, 16), [4])), $findIdsRegexFx('name', '.')); self::assertSame([5, 6, 7, 8, 9, 11, 12], $findIdsRegexFx('name', 'Sa.ra')); - self::assertSame([2, 3, 13], $findIdsRegexFx('name', '[e]')); - self::assertSame([1, 2, 3, 13], $findIdsRegexFx('name', '[eo]')); - self::assertSame([1, 2, 3, ...($isBinary ? [] : [13])], $findIdsRegexFx('name', '[A-P][aeo]')); + self::assertSame([2, 3, 13, 14, 15, 16], $findIdsRegexFx('name', '[e]')); + self::assertSame([1, 2, 3, 13, 14, 15, 16], $findIdsRegexFx('name', '[eo]')); + self::assertSame([1, 2, 3, ...($isBinary ? [] : [13, 14, 15, 16])], $findIdsRegexFx('name', '[A-P][aeo]')); self::assertSame([3], $findIdsRegexFx('name', 'o[^h]')); self::assertSame([5, 6, 7, 8, 9, 10, 11, 12], $findIdsRegexFx('name', '^Sa')); self::assertSame([], $findIdsRegexFx('name', '^ra')); @@ -789,7 +808,7 @@ public function testRegexpCondition(string $type, bool $isBinary): void if (!$this->getDatabasePlatform() instanceof PostgreSQLPlatform || !$isBinary) { self::assertSame([13], $findIdsRegexFx('name', 'hei\w$')); } - self::assertSame([10], $findIdsRegexFx('name', '\W\\\\')); + self::assertSame([10, 15], $findIdsRegexFx('name', '\W\\\\')); if ($type !== 'string' && !$this->getDatabasePlatform() instanceof OraclePlatform) { self::assertSame([5], $findIdsRegexFx('name', '\x20')); self::assertSame([6], $findIdsRegexFx('name', '\n')); diff --git a/tests/Persistence/Sql/QueryTest.php b/tests/Persistence/Sql/QueryTest.php index e62afd562..df72be6be 100644 --- a/tests/Persistence/Sql/QueryTest.php +++ b/tests/Persistence/Sql/QueryTest.php @@ -879,7 +879,7 @@ public function testWhereSpecialValues(): void } self::assertSame( <<<'EOF' - where case when pg_typeof("name") = 'bytea'::regtype then replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then substring(cast("name" as text) from 3) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), repeat(chr(92), 2), chr(92)) like regexp_replace(replace(encode(cast(replace(cast(:a as text), chr(92), repeat(chr(92), 2)) as bytea), 'escape'), repeat(chr(92), 2), chr(92)), '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) else cast("name" as citext) like regexp_replace(:a, '(\\[\\_%])|(\\)', '\1\2\2', 'g') escape chr(92) end + where case when pg_typeof("name") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then replace(substring(cast("name" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?where('name', 'like', 'foo')->render()[0] ); @@ -911,7 +911,7 @@ public function testWhereSpecialValues(): void } self::assertSame( <<<'EOF' - where case when pg_typeof("name") = 'bytea'::regtype then replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then substring(cast("name" as text) from 3) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), repeat(chr(92), 2), chr(92)) ~ replace(encode(cast(replace(cast(:a as text), chr(92), repeat(chr(92), 2)) as bytea), 'escape'), repeat(chr(92), 2), chr(92)) else cast("name" as citext) ~ :a end + where case when pg_typeof("name") = 'bytea'::regtype then replace(regexp_replace(encode(case when pg_typeof("name") = 'bytea'::regtype then decode(case when pg_typeof("name") = 'bytea'::regtype then replace(substring(cast("name" as text) from 3), chr(92), repeat(chr(92), 2)) else '' end, 'hex') else cast(replace(cast("name" as text), chr(92), repeat(chr(92), 2)) as bytea) end, 'escape'), '(?where('name', 'regexp', 'foo')->render()[0] );