From 111f3601185a247683a48a4dc5d8a530f80a624b Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 10:33:45 +0700 Subject: [PATCH 1/9] Improve performance of `ArrayParser::parse()` method --- src/Data/ArrayParser.php | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Data/ArrayParser.php b/src/Data/ArrayParser.php index bc3b77235..605affd81 100644 --- a/src/Data/ArrayParser.php +++ b/src/Data/ArrayParser.php @@ -4,7 +4,11 @@ namespace Yiisoft\Db\Pgsql\Data; -use function in_array; +use function preg_match; +use function strcspn; +use function stripcslashes; +use function strlen; +use function substr; /** * Array representation to PHP array parser for PostgreSQL Server. @@ -64,16 +68,10 @@ private function parseArray(string $value, int &$i = 0): array */ private function parseQuotedString(string $value, int &$i): string { - for ($result = '', ++$i;; ++$i) { - if ($value[$i] === '\\') { - ++$i; - } elseif ($value[$i] === '"') { - ++$i; - return $result; - } + preg_match('/"((?>[^"\\\\]+|\\\\.)*)"/As', $value, $matches, 0, $i); + $i += strlen($matches[0]); - $result .= $value[$i]; - } + return stripcslashes($matches[1]); } /** @@ -81,14 +79,10 @@ private function parseQuotedString(string $value, int &$i): string */ private function parseUnquotedString(string $value, int &$i): ?string { - for ($result = '';; ++$i) { - if (in_array($value[$i], [',', '}'], true)) { - return $result !== 'NULL' - ? $result - : null; - } + $length = strcspn($value, ',}', $i); + $result = substr($value, $i, $length); + $i += $length; - $result .= $value[$i]; - } + return $result !== 'NULL' ? $result : null; } } From 7373c2757364d8f64554af408e8b6a3ae69b44ac Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 11:03:05 +0700 Subject: [PATCH 2/9] Add test --- tests/ArrayParserTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/ArrayParserTest.php b/tests/ArrayParserTest.php index 5737616c5..88a057498 100644 --- a/tests/ArrayParserTest.php +++ b/tests/ArrayParserTest.php @@ -25,6 +25,10 @@ public function testParser(): void $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $arrayParse->parse('{1,-2,NULL,42}')); $this->assertSame([[0 => 'text'], [0 => null], [0 => '1']], $arrayParse->parse('{{text},{NULL},{1}}')); $this->assertSame([0 => ''], $arrayParse->parse('{""}')); + $this->assertSame( + [0 => ',', 1 => '}', 2 => 'NULL', 3 => 't', 4 => 'f'], + $arrayParse->parse('{",","}","NULL",t,f}'), + ); $this->assertSame( [0 => '[",","null",true,"false","f"]'], $arrayParse->parse('{"[\",\",\"null\",true,\"false\",\"f\"]"}'), From 3cbe4e4a5d511d996b44ec68aad31ba8f4ff61a6 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 11:30:10 +0700 Subject: [PATCH 3/9] Add line to CHANGELOG.md [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff3d3363b..63c341b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.0.1 under development -- no changes in this release. +- Enh #477: Improve performance of `ArrayParser::parse()` method (@Tigrov) ## 2.0.0 December 05, 2025 From 7f4dbf5ae15e89038b8695165d8349b6801f7ec0 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 11:36:40 +0700 Subject: [PATCH 4/9] Improve test --- tests/ArrayParserTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ArrayParserTest.php b/tests/ArrayParserTest.php index 88a057498..2f000aca7 100644 --- a/tests/ArrayParserTest.php +++ b/tests/ArrayParserTest.php @@ -26,8 +26,8 @@ public function testParser(): void $this->assertSame([[0 => 'text'], [0 => null], [0 => '1']], $arrayParse->parse('{{text},{NULL},{1}}')); $this->assertSame([0 => ''], $arrayParse->parse('{""}')); $this->assertSame( - [0 => ',', 1 => '}', 2 => 'NULL', 3 => 't', 4 => 'f'], - $arrayParse->parse('{",","}","NULL",t,f}'), + [0 => ',', 1 => '}', 2 => '"', 3 => '\\', 4 => '"\\,}', 5 => 'NULL', 6 => 't', 7 => 'f'], + $arrayParse->parse('{",","}","\\"","\\\\","\\"\\\\,}","NULL",t,f}'), ); $this->assertSame( [0 => '[",","null",true,"false","f"]'], From 6c0a87bfa3273a07e5338f70e9db49e2200f80c9 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 11:48:25 +0700 Subject: [PATCH 5/9] Improve test --- tests/ArrayParserTest.php | 60 +++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/tests/ArrayParserTest.php b/tests/ArrayParserTest.php index 2f000aca7..1c57f8897 100644 --- a/tests/ArrayParserTest.php +++ b/tests/ArrayParserTest.php @@ -4,37 +4,53 @@ namespace Yiisoft\Db\Pgsql\Tests; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Yiisoft\Db\Pgsql\Data\ArrayParser; /** * @group pgsql - * - * @psalm-suppress PropertyNotSetInConstructor */ final class ArrayParserTest extends TestCase { - public function testParser(): void + public static function parserProvider(): iterable { - $arrayParse = new ArrayParser(); - - $this->assertSame([0 => null, 1 => null], $arrayParse->parse('{NULL,NULL}')); - $this->assertSame([], $arrayParse->parse('{}')); - $this->assertSame([0 => null, 1 => null], $arrayParse->parse('{,}')); - $this->assertSame([0 => '1', 1 => '2', 2 => '3'], $arrayParse->parse('{1,2,3}')); - $this->assertSame([0 => '1', 1 => '-2', 2 => null, 3 => '42'], $arrayParse->parse('{1,-2,NULL,42}')); - $this->assertSame([[0 => 'text'], [0 => null], [0 => '1']], $arrayParse->parse('{{text},{NULL},{1}}')); - $this->assertSame([0 => ''], $arrayParse->parse('{""}')); - $this->assertSame( - [0 => ',', 1 => '}', 2 => '"', 3 => '\\', 4 => '"\\,}', 5 => 'NULL', 6 => 't', 7 => 'f'], - $arrayParse->parse('{",","}","\\"","\\\\","\\"\\\\,}","NULL",t,f}'), - ); - $this->assertSame( - [0 => '[",","null",true,"false","f"]'], - $arrayParse->parse('{"[\",\",\"null\",true,\"false\",\"f\"]"}'), - ); - + yield [[], '{}']; + yield [[''], '{""}']; + yield [[null, null], '{NULL,NULL}']; + yield [[null, null], '{,}']; + yield [ + ["a\nb"], + "{\"a\nb\"}", + ]; + yield [ + ['1', '2', '3'], + '{1,2,3}', + ]; + yield [ + ['1', '-2', null, '42'], + '{1,-2,NULL,42}', + ]; + yield [ + [['text'], [null], ['1']], + '{{text},{NULL},{1}}', + ]; + yield [ + [',', '}', '"', '\\', '"\\,}', 'NULL', 't', 'f'], + '{",","}","\\"","\\\\","\\"\\\\,}","NULL",t,f}', + ]; + yield [ + ['[",","null",true,"false","f"]'], + '{"[\",\",\"null\",true,\"false\",\"f\"]"}', + ]; // Similar cases can be in default values - $this->assertSame(null, $arrayParse->parse("'{one,two}'::text[]")); + yield [null, "'{one,two}'::text[]"]; + } + + #[DataProvider('parserProvider')] + public function testParser(?array $expected, string $value): void + { + $arrayParse = new ArrayParser(); + $this->assertSame($expected, $arrayParse->parse($value)); } } From 0f3c5a8785590a81dc50f5f10254242b91c3959c Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 13:34:56 +0700 Subject: [PATCH 6/9] Add test with multibyte strings --- tests/ArrayParserTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/ArrayParserTest.php b/tests/ArrayParserTest.php index 1c57f8897..ac19ae5fa 100644 --- a/tests/ArrayParserTest.php +++ b/tests/ArrayParserTest.php @@ -43,6 +43,11 @@ public static function parserProvider(): iterable ['[",","null",true,"false","f"]'], '{"[\",\",\"null\",true,\"false\",\"f\"]"}', ]; + // Multibyte strings + yield [ + ['ๆˆ‘', '๐Ÿ‘๐Ÿป', 'multibyte ัั‚ั€ะพะบะฐๆˆ‘๐Ÿ‘๐Ÿป', 'ื ื˜ืฉื•ืค ืฆืจื›ื ื•ืช'], + '{ๆˆ‘,๐Ÿ‘๐Ÿป,"multibyte ัั‚ั€ะพะบะฐๆˆ‘๐Ÿ‘๐Ÿป","ื ื˜ืฉื•ืค ืฆืจื›ื ื•ืช"}', + ]; // Similar cases can be in default values yield [null, "'{one,two}'::text[]"]; } From 79bf0fde32fa97075abcced8a80af8c87a2541b0 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 14:56:15 +0700 Subject: [PATCH 7/9] Remove `As` modificators from regex --- src/Data/ArrayParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Data/ArrayParser.php b/src/Data/ArrayParser.php index 605affd81..feb67c050 100644 --- a/src/Data/ArrayParser.php +++ b/src/Data/ArrayParser.php @@ -68,7 +68,7 @@ private function parseArray(string $value, int &$i = 0): array */ private function parseQuotedString(string $value, int &$i): string { - preg_match('/"((?>[^"\\\\]+|\\\\.)*)"/As', $value, $matches, 0, $i); + preg_match('/"((?>[^"\\\\]+|\\\\.)*)"/', $value, $matches, 0, $i); $i += strlen($matches[0]); return stripcslashes($matches[1]); From 06bc1f6e0a90cce729c7df168f97e6ceb74cbd52 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 9 Dec 2025 15:28:35 +0700 Subject: [PATCH 8/9] Remove group from regex --- src/Data/ArrayParser.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Data/ArrayParser.php b/src/Data/ArrayParser.php index feb67c050..d63325b7d 100644 --- a/src/Data/ArrayParser.php +++ b/src/Data/ArrayParser.php @@ -68,10 +68,10 @@ private function parseArray(string $value, int &$i = 0): array */ private function parseQuotedString(string $value, int &$i): string { - preg_match('/"((?>[^"\\\\]+|\\\\.)*)"/', $value, $matches, 0, $i); - $i += strlen($matches[0]); + preg_match('/(?>[^"\\\\]+|\\\\.)*/', $value, $matches, 0, ++$i); + $i += strlen($matches[0]) + 1; - return stripcslashes($matches[1]); + return stripcslashes($matches[0]); } /** From 79ae9aa78ea1c91e30a72549ff0c75022f1c246e Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 10 Dec 2025 10:13:40 +0700 Subject: [PATCH 9/9] Improve --- src/Data/ArrayParser.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Data/ArrayParser.php b/src/Data/ArrayParser.php index d63325b7d..1866ec0c1 100644 --- a/src/Data/ArrayParser.php +++ b/src/Data/ArrayParser.php @@ -68,8 +68,8 @@ private function parseArray(string $value, int &$i = 0): array */ private function parseQuotedString(string $value, int &$i): string { - preg_match('/(?>[^"\\\\]+|\\\\.)*/', $value, $matches, 0, ++$i); - $i += strlen($matches[0]) + 1; + preg_match('/(?>[^"\\\\]+|\\\\.)*/', $value, $matches, 0, $i + 1); + $i += strlen($matches[0]) + 2; return stripcslashes($matches[0]); }