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 diff --git a/src/Data/ArrayParser.php b/src/Data/ArrayParser.php index bc3b77235..1866ec0c1 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('/(?>[^"\\\\]+|\\\\.)*/', $value, $matches, 0, $i + 1); + $i += strlen($matches[0]) + 2; - $result .= $value[$i]; - } + return stripcslashes($matches[0]); } /** @@ -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; } } diff --git a/tests/ArrayParserTest.php b/tests/ArrayParserTest.php index 5737616c5..ac19ae5fa 100644 --- a/tests/ArrayParserTest.php +++ b/tests/ArrayParserTest.php @@ -4,33 +4,58 @@ 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 => '[",","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\"]"}', + ]; + // Multibyte strings + yield [ + ['ๆˆ‘', '๐Ÿ‘๐Ÿป', 'multibyte ัั‚ั€ะพะบะฐๆˆ‘๐Ÿ‘๐Ÿป', 'ื ื˜ืฉื•ืค ืฆืจื›ื ื•ืช'], + '{ๆˆ‘,๐Ÿ‘๐Ÿป,"multibyte ัั‚ั€ะพะบะฐๆˆ‘๐Ÿ‘๐Ÿป","ื ื˜ืฉื•ืค ืฆืจื›ื ื•ืช"}', + ]; // 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)); } }