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 diff --git a/src/Syntax/AbstractSqlParser.php b/src/Syntax/AbstractSqlParser.php index c0bc580a7..a966a2adf 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 strcspn; use function strlen; +use function strpos; +use function strspn; use function substr; /** @@ -14,6 +18,11 @@ */ abstract class AbstractSqlParser { + /** @var string Letter characters, equivalent to `[_a-zA-Z]` in regular expressions */ + protected const LETTER_CHARS = '_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + /** @var string Word characters, equivalent to `\w` in regular expressions */ + protected const WORD_CHARS = '0123456789' . self::LETTER_CHARS; + /** * @var int Length of SQL statement. */ @@ -33,53 +42,46 @@ 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 { - $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; } /** - * 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. */ 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); + /** @var int $pos */ + $pos = strpos($this->sql, $string, $this->position); + $this->position = $pos + strlen($string); } }