From a2af4a9443082d66cdbeb92654df64f680618d48 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 7 Feb 2026 15:02:04 +0700 Subject: [PATCH 1/6] Improve performance of `AbstractSqlParser` class methods --- src/Syntax/AbstractSqlParser.php | 84 ++++++++++++-------------------- 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/src/Syntax/AbstractSqlParser.php b/src/Syntax/AbstractSqlParser.php index c0bc580a7..2bb242050 100644 --- a/src/Syntax/AbstractSqlParser.php +++ b/src/Syntax/AbstractSqlParser.php @@ -4,7 +4,11 @@ namespace Yiisoft\Db\Syntax; +use function preg_match; +use function preg_quote; +use function strcspn; use function strlen; +use function strspn; use function substr; /** @@ -14,6 +18,11 @@ */ abstract class AbstractSqlParser { + /** @var string Letter symbols, equals to `[_a-zA-Z]` in regular expressions */ + protected const LETTER_CHARS = '_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + /** @var string Word symbols, equals to `\w` in regular expressions */ + protected const WORD_CHARS = '0123456789' . self::LETTER_CHARS; + /** * @var int Length of SQL statement. */ @@ -48,19 +57,9 @@ abstract public function getNextPlaceholder(?int &$position = null): ?string; */ final protected function parseWord(): string { - $word = ''; - $continue = true; - - while ($continue && $this->position < $this->length) { - match ($this->sql[$this->position]) { - '_', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', - 'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', - 'V', 'W', 'X', 'Y', 'Z' => $word .= $this->sql[$this->position++], - default => $continue = false, - }; - } + $length = strspn($this->sql, self::WORD_CHARS, $this->position); + $word = substr($this->sql, $this->position, $length); + $this->position += $length; return $word; } @@ -72,14 +71,17 @@ final protected function parseWord(): string */ protected function parseIdentifier(): string { - return match ($this->sql[$this->position]) { - '_', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', - 'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', - 'V', 'W', 'X', 'Y', 'Z' => $this->sql[$this->position++] . $this->parseWord(), - default => '', - }; + $length = strspn($this->sql, self::LETTER_CHARS, $this->position); + + if ($length === 0) { + return ''; + } + + $length += strspn($this->sql, self::WORD_CHARS, $this->position + $length); + $word = substr($this->sql, $this->position, $length); + $this->position += $length; + + return $word; } /** @@ -97,16 +99,8 @@ final protected function skipQuotedWithoutEscape(string $endChar): void */ final protected function skipQuotedWithEscape(string $endChar): void { - for (; $this->position < $this->length; ++$this->position) { - if ($this->sql[$this->position] === $endChar) { - ++$this->position; - return; - } - - if ($this->sql[$this->position] === '\\') { - ++$this->position; - } - } + preg_match("/(?>[^$endChar\\\\]+|\\\\.)*/", $this->sql, $matches, 0, $this->position); + $this->position += strlen($matches[0]) + 1; } /** @@ -114,9 +108,8 @@ final protected function skipQuotedWithEscape(string $endChar): void */ final protected function skipChars(string $char): void { - while ($this->position < $this->length && $this->sql[$this->position] === $char) { - ++$this->position; - } + $length = strspn($this->sql, $char, $this->position); + $this->position += $length; } /** @@ -124,12 +117,8 @@ final protected function skipChars(string $char): void */ final protected function skipToAfterChar(string $char): void { - for (; $this->position < $this->length; ++$this->position) { - if ($this->sql[$this->position] === $char) { - ++$this->position; - return; - } - } + $length = strcspn($this->sql, $char, $this->position); + $this->position += $length + 1; } /** @@ -137,17 +126,8 @@ final protected function skipToAfterChar(string $char): void */ final protected function skipToAfterString(string $string): void { - $firstChar = $string[0]; - $subString = substr($string, 1); - $length = strlen($subString); - - do { - $this->skipToAfterChar($firstChar); - - if (substr($this->sql, $this->position, $length) === $subString) { - $this->position += $length; - return; - } - } while ($this->position + $length < $this->length); + $quotedString = preg_quote($string, '/'); + preg_match("/.*?$quotedString/", $this->sql, $matches, 0, $this->position); + $this->position += strlen($matches[0]) + 1; } } From 099a52c6f1cb31abb48e3032eb3282c1a847bf50 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 7 Feb 2026 15:29:26 +0700 Subject: [PATCH 2/6] Fix regex --- src/Syntax/AbstractSqlParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Syntax/AbstractSqlParser.php b/src/Syntax/AbstractSqlParser.php index 2bb242050..f43044fe1 100644 --- a/src/Syntax/AbstractSqlParser.php +++ b/src/Syntax/AbstractSqlParser.php @@ -127,7 +127,7 @@ final protected function skipToAfterChar(string $char): void final protected function skipToAfterString(string $string): void { $quotedString = preg_quote($string, '/'); - preg_match("/.*?$quotedString/", $this->sql, $matches, 0, $this->position); + preg_match("/.*?$quotedString/s", $this->sql, $matches, 0, $this->position); $this->position += strlen($matches[0]) + 1; } } From 658eaa6465ce79b803ba2292ae447b28115e8bbf Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 7 Feb 2026 15:44:37 +0700 Subject: [PATCH 3/6] Improve --- src/Syntax/AbstractSqlParser.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Syntax/AbstractSqlParser.php b/src/Syntax/AbstractSqlParser.php index f43044fe1..4a7dd1c6c 100644 --- a/src/Syntax/AbstractSqlParser.php +++ b/src/Syntax/AbstractSqlParser.php @@ -5,9 +5,9 @@ namespace Yiisoft\Db\Syntax; use function preg_match; -use function preg_quote; use function strcspn; use function strlen; +use function strpos; use function strspn; use function substr; @@ -126,8 +126,7 @@ final protected function skipToAfterChar(string $char): void */ final protected function skipToAfterString(string $string): void { - $quotedString = preg_quote($string, '/'); - preg_match("/.*?$quotedString/s", $this->sql, $matches, 0, $this->position); - $this->position += strlen($matches[0]) + 1; + $pos = strpos($this->sql, $string, $this->position); + $this->position = $pos + strlen($string); } } From 271e9747c8a7d6c05f86cb34a877bae3370c4e00 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 7 Feb 2026 15:54:07 +0700 Subject: [PATCH 4/6] Improve --- src/Syntax/AbstractSqlParser.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Syntax/AbstractSqlParser.php b/src/Syntax/AbstractSqlParser.php index 4a7dd1c6c..2a8626e02 100644 --- a/src/Syntax/AbstractSqlParser.php +++ b/src/Syntax/AbstractSqlParser.php @@ -18,9 +18,9 @@ */ abstract class AbstractSqlParser { - /** @var string Letter symbols, equals to `[_a-zA-Z]` in regular expressions */ + /** @var string Letter characters, equivalent to `[_a-zA-Z]` in regular expressions */ protected const LETTER_CHARS = '_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - /** @var string Word symbols, equals to `\w` in regular expressions */ + /** @var string Word characters, equivalent to `\w` in regular expressions */ protected const WORD_CHARS = '0123456789' . self::LETTER_CHARS; /** @@ -42,18 +42,18 @@ public function __construct(protected string $sql) } /** - * Returns the next placeholder from the current position in SQL statement. + * Returns the next placeholder from the current position in an SQL statement. * - * @param int|null $position Position of the placeholder in SQL statement. + * @param int|null $position Position of the placeholder in an SQL statement. * * @return string|null The next placeholder or null if it is not found. */ abstract public function getNextPlaceholder(?int &$position = null): ?string; /** - * Parses and returns word symbols. Equals to `\w+` in regular expressions. + * Parses and returns word characters. Equivalent to `\w+` in regular expressions. * - * @return string Parsed word symbols. + * @return string Parsed word characters. */ final protected function parseWord(): string { @@ -65,7 +65,7 @@ final protected function parseWord(): string } /** - * Parses and returns identifier. Equals to `[_a-zA-Z]\w+` in regular expressions. + * Parses and returns identifier. Equivalent to `[_a-zA-Z]\w*` in regular expressions. * * @return string Parsed identifier. */ @@ -126,6 +126,7 @@ final protected function skipToAfterChar(string $char): void */ final protected function skipToAfterString(string $string): void { + /** @var int $pos */ $pos = strpos($this->sql, $string, $this->position); $this->position = $pos + strlen($string); } From a2e5c8ac0d4f34273264918fbb1a4a3597ad5903 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 7 Feb 2026 16:06:43 +0700 Subject: [PATCH 5/6] Update doc [skip ci] --- src/Syntax/AbstractSqlParser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Syntax/AbstractSqlParser.php b/src/Syntax/AbstractSqlParser.php index 2a8626e02..a966a2adf 100644 --- a/src/Syntax/AbstractSqlParser.php +++ b/src/Syntax/AbstractSqlParser.php @@ -51,7 +51,7 @@ public function __construct(protected string $sql) abstract public function getNextPlaceholder(?int &$position = null): ?string; /** - * Parses and returns word characters. Equivalent to `\w+` in regular expressions. + * Parses and returns word characters. Equivalent to `\w*` in regular expressions. * * @return string Parsed word characters. */ From 8fba095ce06863af6072935b733eb53eda467974 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 7 Feb 2026 16:10:17 +0700 Subject: [PATCH 6/6] Add line to CHANGELOG.md [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d7d3c20..a5db7281a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Enh #1148: Add `declare(strict_types=1)` to `Yiisoft\Db\Constant\ColumnInfoSource` (@mspirkov) - Enh #1158: Explicitly mark readonly properties (@vjik) - Enh #1156: Remove unnecessary files from Composer package (@mspirkov) +- Enh #1159: Improve performance of `AbstractSqlParser` class methods (@Tigrov) ## 2.0.0 December 05, 2025