Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
94 changes: 37 additions & 57 deletions src/Syntax/AbstractSqlParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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.
*/
Expand All @@ -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;
}

/**
Expand All @@ -97,57 +99,35 @@ 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;
}

/**
* Skips all specified characters.
*/
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;
}

/**
* Skips to the character after the specified character.
*/
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;
}

/**
* Skips to the character after the specified string.
*/
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);
}
}