first commit

This commit is contained in:
dev@siliconpin.com
2025-08-07 11:53:41 +05:30
commit a3067c5ad4
4795 changed files with 782758 additions and 0 deletions

View File

@@ -0,0 +1,539 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function array_key_exists;
use function in_array;
use function is_numeric;
use function is_string;
/**
* Parses an alter operation.
*
* @final
*/
class AlterOperation extends Component
{
/**
* All database options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $DB_OPTIONS = [
'CHARACTER SET' => [
1,
'var',
],
'CHARSET' => [
1,
'var',
],
'DEFAULT CHARACTER SET' => [
1,
'var',
],
'DEFAULT CHARSET' => [
1,
'var',
],
'UPGRADE' => [
1,
'var',
],
'COLLATE' => [
2,
'var',
],
'DEFAULT COLLATE' => [
2,
'var',
],
];
/**
* All table options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $TABLE_OPTIONS = [
'ENGINE' => [
1,
'var=',
],
'AUTO_INCREMENT' => [
1,
'var=',
],
'AVG_ROW_LENGTH' => [
1,
'var',
],
'MAX_ROWS' => [
1,
'var',
],
'ROW_FORMAT' => [
1,
'var',
],
'COMMENT' => [
1,
'var',
],
'ADD' => 1,
'ALTER' => 1,
'ANALYZE' => 1,
'CHANGE' => 1,
'CHARSET' => 1,
'CHECK' => 1,
'COALESCE' => 1,
'CONVERT' => 1,
'DEFAULT CHARSET' => 1,
'DISABLE' => 1,
'DISCARD' => 1,
'DROP' => 1,
'ENABLE' => 1,
'IMPORT' => 1,
'MODIFY' => 1,
'OPTIMIZE' => 1,
'ORDER' => 1,
'REBUILD' => 1,
'REMOVE' => 1,
'RENAME' => 1,
'REORGANIZE' => 1,
'REPAIR' => 1,
'UPGRADE' => 1,
'COLUMN' => 2,
'CONSTRAINT' => 2,
'DEFAULT' => 2,
'BY' => 2,
'FOREIGN' => 2,
'FULLTEXT' => 2,
'KEY' => 2,
'KEYS' => 2,
'PARTITION' => 2,
'PARTITION BY' => 2,
'PARTITIONING' => 2,
'PRIMARY KEY' => 2,
'SPATIAL' => 2,
'TABLESPACE' => 2,
'INDEX' => [
2,
'var',
],
'CHARACTER SET' => 3,
'TO' => [
3,
'var',
],
];
/**
* All user options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $USER_OPTIONS = [
'ATTRIBUTE' => [
1,
'var',
],
'COMMENT' => [
1,
'var',
],
'REQUIRE' => [
1,
'var',
],
'BY' => [
2,
'expr',
],
'PASSWORD' => [
2,
'var',
],
'WITH' => [
2,
'var',
],
'ACCOUNT' => 1,
'DEFAULT' => 1,
'LOCK' => 2,
'UNLOCK' => 2,
'IDENTIFIED' => 3,
];
/**
* All view options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $VIEW_OPTIONS = ['AS' => 1];
/**
* All event options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $EVENT_OPTIONS = [
'ON SCHEDULE' => 1,
'EVERY' => [
2,
'expr',
],
'AT' => [
2,
'expr',
],
'STARTS' => [
3,
'expr',
],
'ENDS' => [
4,
'expr',
],
'ON COMPLETION PRESERVE' => 5,
'ON COMPLETION NOT PRESERVE' => 5,
'RENAME' => 6,
'TO' => [
7,
'var',
],
'ENABLE' => 8,
'DISABLE' => 8,
'DISABLE ON SLAVE' => 8,
'COMMENT' => [
9,
'var',
],
'DO' => 10,
];
/**
* Options of this operation.
*
* @var OptionsArray
*/
public $options;
/**
* The altered field.
*
* @var Expression|string|null
*/
public $field;
/**
* The partitions.
*
* @var Component[]|ArrayObj|null
*/
public $partitions;
/**
* Unparsed tokens.
*
* @var Token[]|string
*/
public $unknown = [];
/**
* @param OptionsArray $options options of alter operation
* @param Expression|string|null $field altered field
* @param Component[]|ArrayObj|null $partitions partitions definition found in the operation
* @param Token[] $unknown unparsed tokens found at the end of operation
*/
public function __construct(
$options = null,
$field = null,
$partitions = null,
$unknown = []
) {
$this->partitions = $partitions;
$this->options = $options;
$this->field = $field;
$this->unknown = $unknown;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return AlterOperation
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* Counts brackets.
*
* @var int
*/
$brackets = 0;
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------[ options ]---------------------> 1
*
* 1 ----------------------[ field ]----------------------> 2
*
* 1 -------------[ PARTITION / PARTITION BY ]------------> 3
*
* 2 -------------------------[ , ]-----------------------> 0
*
* @var int
*/
$state = 0;
/**
* partition state.
*
* @var int
*/
$partitionState = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping comments.
if ($token->type === Token::TYPE_COMMENT) {
continue;
}
// Skipping whitespaces.
if ($token->type === Token::TYPE_WHITESPACE) {
if ($state === 2) {
// When parsing the unknown part, the whitespaces are
// included to not break anything.
$ret->unknown[] = $token;
continue;
}
}
if ($state === 0) {
$ret->options = OptionsArray::parse($parser, $list, $options);
// Not only when aliasing but also when parsing the body of an event, we just list the tokens of the
// body in the unknown tokens list, as they define their own statements.
if ($ret->options->has('AS') || $ret->options->has('DO')) {
for (; $list->idx < $list->count; ++$list->idx) {
if ($list->tokens[$list->idx]->type === Token::TYPE_DELIMITER) {
break;
}
$ret->unknown[] = $list->tokens[$list->idx];
}
break;
}
$state = 1;
if ($ret->options->has('PARTITION') || $token->value === 'PARTITION BY') {
$state = 3;
$list->getPrevious(); // in order to check whether it's partition or partition by.
}
} elseif ($state === 1) {
$ret->field = Expression::parse(
$parser,
$list,
[
'breakOnAlias' => true,
'parseField' => 'column',
]
);
if ($ret->field === null) {
// No field was read. We go back one token so the next
// iteration will parse the same token, but in state 2.
--$list->idx;
}
$state = 2;
} elseif ($state === 2) {
if (is_string($token->value) || is_numeric($token->value)) {
$arrayKey = $token->value;
} else {
$arrayKey = $token->token;
}
if ($token->type === Token::TYPE_OPERATOR) {
if ($token->value === '(') {
++$brackets;
} elseif ($token->value === ')') {
--$brackets;
} elseif (($token->value === ',') && ($brackets === 0)) {
break;
}
} elseif (! self::checkIfTokenQuotedSymbol($token)) {
if (! empty(Parser::$STATEMENT_PARSERS[$token->value])) {
$list->idx++; // Ignore the current token
$nextToken = $list->getNext();
if ($token->value === 'SET' && $nextToken !== null && $nextToken->value === '(') {
// To avoid adding the tokens between the SET() parentheses to the unknown tokens
$list->getNextOfTypeAndValue(Token::TYPE_OPERATOR, ')');
} elseif ($token->value === 'SET' && $nextToken !== null && $nextToken->value === 'DEFAULT') {
// to avoid adding the `DEFAULT` token to the unknown tokens.
++$list->idx;
} else {
// We have reached the end of ALTER operation and suddenly found
// a start to new statement, but have not find a delimiter between them
$parser->error(
'A new statement was found, but no delimiter between it and the previous one.',
$token
);
break;
}
} elseif (
(array_key_exists($arrayKey, self::$DB_OPTIONS)
|| array_key_exists($arrayKey, self::$TABLE_OPTIONS))
&& ! self::checkIfColumnDefinitionKeyword($arrayKey)
) {
// This alter operation has finished, which means a comma
// was missing before start of new alter operation
$parser->error('Missing comma before start of a new alter operation.', $token);
break;
}
}
$ret->unknown[] = $token;
} elseif ($state === 3) {
if ($partitionState === 0) {
$list->idx++; // Ignore the current token
$nextToken = $list->getNext();
if (
($token->type === Token::TYPE_KEYWORD)
&& (($token->keyword === 'PARTITION BY')
|| ($token->keyword === 'PARTITION' && $nextToken && $nextToken->value !== '('))
) {
$partitionState = 1;
} elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITION')) {
$partitionState = 2;
}
--$list->idx; // to decrease the idx by one, because the last getNext returned and increased it.
// reverting the effect of the getNext
$list->getPrevious();
$list->getPrevious();
++$list->idx; // to index the idx by one, because the last getPrevious returned and decreased it.
} elseif ($partitionState === 1) {
// Building the expression used for partitioning.
if (empty($ret->field)) {
$ret->field = '';
}
$ret->field .= $token->type === Token::TYPE_WHITESPACE ? ' ' : $token->token;
} elseif ($partitionState === 2) {
$ret->partitions = ArrayObj::parse(
$parser,
$list,
['type' => PartitionDefinition::class]
);
}
}
}
if ($ret->options->isEmpty()) {
$parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
}
--$list->idx;
return $ret;
}
/**
* @param AlterOperation $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
$ret = $component->options . ' ';
if (isset($component->field) && ($component->field !== '')) {
$ret .= $component->field . ' ';
}
$ret .= TokensList::build($component->unknown);
if (isset($component->partitions)) {
$ret .= PartitionDefinition::build($component->partitions);
}
return $ret;
}
/**
* Check if token's value is one of the common keywords
* between column and table alteration
*
* @param string $tokenValue Value of current token
*
* @return bool
*/
private static function checkIfColumnDefinitionKeyword($tokenValue)
{
$commonOptions = [
'AUTO_INCREMENT',
'COMMENT',
'DEFAULT',
'CHARACTER SET',
'COLLATE',
'PRIMARY',
'UNIQUE',
'PRIMARY KEY',
'UNIQUE KEY',
];
// Since these options can be used for
// both table as well as a specific column in the table
return in_array($tokenValue, $commonOptions);
}
/**
* Check if token is symbol and quoted with backtick
*
* @param Token $token token to check
*
* @return bool
*/
private static function checkIfTokenQuotedSymbol($token)
{
return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use PhpMyAdmin\SqlParser\Translator;
use function count;
use function sprintf;
/**
* `VALUES` keyword parser.
*
* @final
*/
class Array2d extends Component
{
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return ArrayObj[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
/**
* The number of values in each set.
*
* @var int
*/
$count = -1;
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ----------------------[ array ]----------------------> 1
*
* 1 ------------------------[ , ]------------------------> 0
* 1 -----------------------[ else ]----------------------> (END)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
// No keyword is expected.
if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_RESERVED)) {
break;
}
if ($state === 0) {
if ($token->value !== '(') {
break;
}
$arr = ArrayObj::parse($parser, $list, $options);
$arrCount = count($arr->values);
if ($count === -1) {
$count = $arrCount;
} elseif ($arrCount !== $count) {
$parser->error(
sprintf(
Translator::gettext('%1$d values were expected, but found %2$d.'),
$count,
$arrCount
),
$token
);
}
$ret[] = $arr;
$state = 1;
} elseif ($state === 1) {
if ($token->value !== ',') {
break;
}
$state = 0;
}
}
if ($state === 0) {
$parser->error('An opening bracket followed by a set of values was expected.', $list->tokens[$list->idx]);
}
--$list->idx;
return $ret;
}
/**
* @param ArrayObj[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
return ArrayObj::build($component);
}
}

View File

@@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
use function strlen;
use function trim;
/**
* Parses an array.
*
* @final
*/
class ArrayObj extends Component
{
/**
* The array that contains the unprocessed value of each token.
*
* @var string[]
*/
public $raw = [];
/**
* The array that contains the processed value of each token.
*
* @var string[]
*/
public $values = [];
/**
* @param string[] $raw the unprocessed values
* @param string[] $values the processed values
*/
public function __construct(array $raw = [], array $values = [])
{
$this->raw = $raw;
$this->values = $values;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return ArrayObj|Component[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = empty($options['type']) ? new static() : [];
/**
* The last raw expression.
*
* @var string
*/
$lastRaw = '';
/**
* The last value.
*
* @var string
*/
$lastValue = '';
/**
* Counts brackets.
*
* @var int
*/
$brackets = 0;
/**
* Last separator (bracket or comma).
*
* @var bool
*/
$isCommaLast = false;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
$lastRaw .= $token->token;
$lastValue = trim($lastValue) . ' ';
continue;
}
if (($brackets === 0) && (($token->type !== Token::TYPE_OPERATOR) || ($token->value !== '('))) {
$parser->error('An opening bracket was expected.', $token);
break;
}
if ($token->type === Token::TYPE_OPERATOR) {
if ($token->value === '(') {
if (++$brackets === 1) { // 1 is the base level.
continue;
}
} elseif ($token->value === ')') {
if (--$brackets === 0) { // Array ended.
break;
}
} elseif ($token->value === ',') {
if ($brackets === 1) {
$isCommaLast = true;
if (empty($options['type'])) {
$ret->raw[] = trim($lastRaw);
$ret->values[] = trim($lastValue);
$lastRaw = $lastValue = '';
}
}
continue;
}
}
if (empty($options['type'])) {
$lastRaw .= $token->token;
$lastValue .= $token->value;
} else {
$ret[] = $options['type']::parse(
$parser,
$list,
empty($options['typeOptions']) ? [] : $options['typeOptions']
);
}
}
// Handling last element.
//
// This is treated differently to treat the following cases:
//
// => []
// [,] => ['', '']
// [] => []
// [a,] => ['a', '']
// [a] => ['a']
$lastRaw = trim($lastRaw);
if (empty($options['type']) && ((strlen($lastRaw) > 0) || ($isCommaLast))) {
$ret->raw[] = $lastRaw;
$ret->values[] = trim($lastValue);
}
return $ret;
}
/**
* @param ArrayObj|ArrayObj[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(', ', $component);
}
if (! empty($component->raw)) {
return '(' . implode(', ', $component->raw) . ')';
}
return '(' . implode(', ', $component->values) . ')';
}
}

View File

@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Context;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function count;
/**
* Parses a reference to a CASE expression.
*
* @final
*/
class CaseExpression extends Component
{
/**
* The value to be compared.
*
* @var Expression|null
*/
public $value;
/**
* The conditions in WHEN clauses.
*
* @var Condition[][]
*/
public $conditions = [];
/**
* The results matching with the WHEN clauses.
*
* @var Expression[]
*/
public $results = [];
/**
* The values to be compared against.
*
* @var Expression[]
*/
public $compare_values = [];
/**
* The result in ELSE section of expr.
*
* @var Expression|null
*/
public $else_result;
/**
* The alias of this CASE statement.
*
* @var string|null
*/
public $alias;
/**
* The sub-expression.
*
* @var string
*/
public $expr = '';
public function __construct()
{
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return CaseExpression
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* State of parser.
*
* @var int
*/
$state = 0;
/**
* Syntax type (type 0 or type 1).
*
* @var int
*/
$type = 0;
++$list->idx; // Skip 'CASE'
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
if ($token->type === Token::TYPE_KEYWORD) {
switch ($token->keyword) {
case 'WHEN':
++$list->idx; // Skip 'WHEN'
$newCondition = Condition::parse($parser, $list);
$type = 1;
$state = 1;
$ret->conditions[] = $newCondition;
break;
case 'ELSE':
++$list->idx; // Skip 'ELSE'
$ret->else_result = Expression::parse($parser, $list);
$state = 0; // last clause of CASE expression
break;
case 'END':
$state = 3; // end of CASE expression
++$list->idx;
break 2;
default:
$parser->error('Unexpected keyword.', $token);
break 2;
}
} else {
$ret->value = Expression::parse($parser, $list);
$type = 0;
$state = 1;
}
} elseif ($state === 1) {
if ($type === 0) {
if ($token->type === Token::TYPE_KEYWORD) {
switch ($token->keyword) {
case 'WHEN':
++$list->idx; // Skip 'WHEN'
$newValue = Expression::parse($parser, $list);
$state = 2;
$ret->compare_values[] = $newValue;
break;
case 'ELSE':
++$list->idx; // Skip 'ELSE'
$ret->else_result = Expression::parse($parser, $list);
$state = 0; // last clause of CASE expression
break;
case 'END':
$state = 3; // end of CASE expression
++$list->idx;
break 2;
default:
$parser->error('Unexpected keyword.', $token);
break 2;
}
}
} else {
if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'THEN') {
++$list->idx; // Skip 'THEN'
$newResult = Expression::parse($parser, $list);
$state = 0;
$ret->results[] = $newResult;
} elseif ($token->type === Token::TYPE_KEYWORD) {
$parser->error('Unexpected keyword.', $token);
break;
}
}
} elseif ($state === 2) {
if ($type === 0) {
if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'THEN') {
++$list->idx; // Skip 'THEN'
$newResult = Expression::parse($parser, $list);
$ret->results[] = $newResult;
$state = 1;
} elseif ($token->type === Token::TYPE_KEYWORD) {
$parser->error('Unexpected keyword.', $token);
break;
}
}
}
}
if ($state !== 3) {
$parser->error('Unexpected end of CASE expression', $list->tokens[$list->idx - 1]);
} else {
// Parse for alias of CASE expression
$asFound = false;
for (; $list->idx < $list->count; ++$list->idx) {
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
// Handle optional AS keyword before alias
if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'AS') {
if ($asFound || ! empty($ret->alias)) {
$parser->error('Potential duplicate alias of CASE expression.', $token);
break;
}
$asFound = true;
continue;
}
if (
$asFound
&& $token->type === Token::TYPE_KEYWORD
&& ($token->flags & Token::FLAG_KEYWORD_RESERVED || $token->flags & Token::FLAG_KEYWORD_FUNCTION)
) {
$parser->error('An alias expected after AS but got ' . $token->value, $token);
$asFound = false;
break;
}
if (
$asFound
|| $token->type === Token::TYPE_STRING
|| ($token->type === Token::TYPE_SYMBOL && ! $token->flags & Token::FLAG_SYMBOL_VARIABLE)
|| $token->type === Token::TYPE_NONE
) {
// An alias is expected (the keyword `AS` was previously found).
if (! empty($ret->alias)) {
$parser->error('An alias was previously found.', $token);
break;
}
$ret->alias = $token->value;
$asFound = false;
continue;
}
break;
}
if ($asFound) {
$parser->error('An alias was expected after AS.', $list->tokens[$list->idx - 1]);
}
$ret->expr = self::build($ret);
}
--$list->idx;
return $ret;
}
/**
* @param CaseExpression $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
$ret = 'CASE ';
if (isset($component->value)) {
// Syntax type 0
$ret .= $component->value . ' ';
$valuesCount = count($component->compare_values);
$resultsCount = count($component->results);
for ($i = 0; $i < $valuesCount && $i < $resultsCount; ++$i) {
$ret .= 'WHEN ' . $component->compare_values[$i] . ' ';
$ret .= 'THEN ' . $component->results[$i] . ' ';
}
} else {
// Syntax type 1
$valuesCount = count($component->conditions);
$resultsCount = count($component->results);
for ($i = 0; $i < $valuesCount && $i < $resultsCount; ++$i) {
$ret .= 'WHEN ' . Condition::build($component->conditions[$i]) . ' ';
$ret .= 'THEN ' . $component->results[$i] . ' ';
}
}
if (isset($component->else_result)) {
$ret .= 'ELSE ' . $component->else_result . ' ';
}
$ret .= 'END';
if ($component->alias) {
$ret .= ' AS ' . Context::escape($component->alias);
}
return $ret;
}
}

View File

@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function in_array;
use function is_array;
use function trim;
/**
* `WHERE` keyword parser.
*
* @final
*/
class Condition extends Component
{
/**
* Logical operators that can be used to delimit expressions.
*
* @var string[]
*/
public static $DELIMITERS = [
'&&',
'||',
'AND',
'OR',
'XOR',
];
/**
* List of allowed reserved keywords in conditions.
*
* @var array<string, int>
*/
public static $ALLOWED_KEYWORDS = [
'ALL' => 1,
'AND' => 1,
'BETWEEN' => 1,
'EXISTS' => 1,
'IF' => 1,
'IN' => 1,
'INTERVAL' => 1,
'IS' => 1,
'LIKE' => 1,
'MATCH' => 1,
'NOT IN' => 1,
'NOT NULL' => 1,
'NOT' => 1,
'NULL' => 1,
'OR' => 1,
'REGEXP' => 1,
'RLIKE' => 1,
'SOUNDS' => 1,
'XOR' => 1,
];
/**
* Identifiers recognized.
*
* @var array<int, mixed>
*/
public $identifiers = [];
/**
* Whether this component is an operator.
*
* @var bool
*/
public $isOperator = false;
/**
* The condition.
*
* @var string
*/
public $expr;
/**
* @param string $expr the condition or the operator
*/
public function __construct($expr = null)
{
$this->expr = trim((string) $expr);
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return Condition[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* Counts brackets.
*
* @var int
*/
$brackets = 0;
/**
* Whether there was a `BETWEEN` keyword before or not.
*
* It is required to keep track of them because their structure contains
* the keyword `AND`, which is also an operator that delimits
* expressions.
*
* @var bool
*/
$betweenBefore = false;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if ($token->type === Token::TYPE_COMMENT) {
continue;
}
// Replacing all whitespaces (new lines, tabs, etc.) with a single
// space character.
if ($token->type === Token::TYPE_WHITESPACE) {
$expr->expr .= ' ';
continue;
}
// Conditions are delimited by logical operators.
if (in_array($token->value, static::$DELIMITERS, true)) {
if ($betweenBefore && ($token->value === 'AND')) {
// The syntax of keyword `BETWEEN` is hard-coded.
$betweenBefore = false;
} else {
// The expression ended.
$expr->expr = trim($expr->expr);
if (! empty($expr->expr)) {
$ret[] = $expr;
}
// Adding the operator.
$expr = new static($token->value);
$expr->isOperator = true;
$ret[] = $expr;
// Preparing to parse another condition.
$expr = new static();
continue;
}
}
if (
($token->type === Token::TYPE_KEYWORD)
&& ($token->flags & Token::FLAG_KEYWORD_RESERVED)
&& ! ($token->flags & Token::FLAG_KEYWORD_FUNCTION)
) {
if ($token->value === 'BETWEEN') {
$betweenBefore = true;
}
if (($brackets === 0) && empty(static::$ALLOWED_KEYWORDS[$token->value])) {
break;
}
}
if ($token->type === Token::TYPE_OPERATOR) {
if ($token->value === '(') {
++$brackets;
} elseif ($token->value === ')') {
if ($brackets === 0) {
break;
}
--$brackets;
}
}
$expr->expr .= $token->token;
if (
($token->type !== Token::TYPE_NONE)
&& (($token->type !== Token::TYPE_KEYWORD)
|| ($token->flags & Token::FLAG_KEYWORD_RESERVED))
&& ($token->type !== Token::TYPE_STRING)
&& ($token->type !== Token::TYPE_SYMBOL)
) {
continue;
}
if (in_array($token->value, $expr->identifiers)) {
continue;
}
$expr->identifiers[] = $token->value;
}
// Last iteration was not processed.
$expr->expr = trim($expr->expr);
if (! empty($expr->expr)) {
$ret[] = $expr;
}
--$list->idx;
return $ret;
}
/**
* @param Condition[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(' ', $component);
}
return $component->expr;
}
}

View File

@@ -0,0 +1,362 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Context;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
use function trim;
/**
* Parses the create definition of a column or a key.
*
* Used for parsing `CREATE TABLE` statement.
*
* @final
*/
class CreateDefinition extends Component
{
/**
* All field options.
*
* @var array<string, bool|int|array<int, int|string|array<string, bool>>>
* @psalm-var array<string, (bool|positive-int|array{
* 0: positive-int,
* 1: ('var'|'var='|'expr'|'expr='),
* 2?: array<string, bool>
* })>
*/
public static $FIELD_OPTIONS = [
// Tells the `OptionsArray` to not sort the options.
// See the note below.
'_UNSORTED' => true,
'NOT NULL' => 1,
'NULL' => 1,
'DEFAULT' => [
2,
'expr',
['breakOnAlias' => true],
],
/* Following are not according to grammar, but MySQL happily accepts
* these at any location */
'CHARSET' => [
2,
'var',
],
'COLLATE' => [
3,
'var',
],
'AUTO_INCREMENT' => 3,
'PRIMARY' => 4,
'PRIMARY KEY' => 4,
'UNIQUE' => 4,
'UNIQUE KEY' => 4,
'COMMENT' => [
5,
'var',
],
'COLUMN_FORMAT' => [
6,
'var',
],
'ON UPDATE' => [
7,
'expr',
],
// Generated columns options.
'GENERATED ALWAYS' => 8,
'AS' => [
9,
'expr',
['parenthesesDelimited' => true],
],
'VIRTUAL' => 10,
'PERSISTENT' => 11,
'STORED' => 11,
'CHECK' => [
12,
'expr',
['parenthesesDelimited' => true],
],
'INVISIBLE' => 13,
'ENFORCED' => 14,
'NOT' => 15,
'COMPRESSED' => 16,
// Common entries.
//
// NOTE: Some of the common options are not in the same order which
// causes troubles when checking if the options are in the right order.
// I should find a way to define multiple sets of options and make the
// parser select the right set.
//
// 'UNIQUE' => 4,
// 'UNIQUE KEY' => 4,
// 'COMMENT' => [5, 'var'],
// 'NOT NULL' => 1,
// 'NULL' => 1,
// 'PRIMARY' => 4,
// 'PRIMARY KEY' => 4,
];
/**
* The name of the new column.
*
* @var string|null
*/
public $name;
/**
* Whether this field is a constraint or not.
*
* @var bool|null
*/
public $isConstraint;
/**
* The data type of thew new column.
*
* @var DataType|null
*/
public $type;
/**
* The key.
*
* @var Key|null
*/
public $key;
/**
* The table that is referenced.
*
* @var Reference|null
*/
public $references;
/**
* The options of this field.
*
* @var OptionsArray|null
*/
public $options;
/**
* @param string|null $name the name of the field
* @param OptionsArray|null $options the options of this field
* @param DataType|Key|null $type the data type of this field or the key
* @param bool $isConstraint whether this field is a constraint or not
* @param Reference|null $references references
*/
public function __construct(
$name = null,
$options = null,
$type = null,
$isConstraint = false,
$references = null
) {
$this->name = $name;
$this->options = $options;
if ($type instanceof DataType) {
$this->type = $type;
} elseif ($type instanceof Key) {
$this->key = $type;
$this->isConstraint = $isConstraint;
$this->references = $references;
}
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return CreateDefinition[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -----------------------[ ( ]------------------------> 1
*
* 1 --------------------[ CONSTRAINT ]------------------> 1
* 1 -----------------------[ key ]----------------------> 2
* 1 -------------[ constraint / column name ]-----------> 2
*
* 2 --------------------[ data type ]-------------------> 3
*
* 3 ---------------------[ options ]--------------------> 4
*
* 4 --------------------[ REFERENCES ]------------------> 4
*
* 5 ------------------------[ , ]-----------------------> 1
* 5 ------------------------[ ) ]-----------------------> 6 (-1)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
if (($token->type !== Token::TYPE_OPERATOR) || ($token->value !== '(')) {
$parser->error('An opening bracket was expected.', $token);
break;
}
$state = 1;
} elseif ($state === 1) {
if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'CONSTRAINT') {
$expr->isConstraint = true;
} elseif (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_KEY)) {
$expr->key = Key::parse($parser, $list);
$state = 4;
} elseif ($token->type === Token::TYPE_SYMBOL || $token->type === Token::TYPE_NONE) {
$expr->name = $token->value;
if (! $expr->isConstraint) {
$state = 2;
}
} elseif ($token->type === Token::TYPE_KEYWORD) {
if ($token->flags & Token::FLAG_KEYWORD_RESERVED) {
// Reserved keywords can't be used
// as field names without backquotes
$parser->error(
'A symbol name was expected! '
. 'A reserved keyword can not be used '
. 'as a column name without backquotes.',
$token
);
return $ret;
}
// Non-reserved keywords are allowed without backquotes
$expr->name = $token->value;
$state = 2;
} else {
$parser->error('A symbol name was expected!', $token);
return $ret;
}
} elseif ($state === 2) {
$expr->type = DataType::parse($parser, $list);
$state = 3;
} elseif ($state === 3) {
$expr->options = OptionsArray::parse($parser, $list, static::$FIELD_OPTIONS);
$state = 4;
} elseif ($state === 4) {
if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'REFERENCES') {
++$list->idx; // Skipping keyword 'REFERENCES'.
$expr->references = Reference::parse($parser, $list);
} else {
--$list->idx;
}
$state = 5;
} elseif ($state === 5) {
if (! empty($expr->type) || ! empty($expr->key)) {
$ret[] = $expr;
}
$expr = new static();
if ($token->value === ',') {
$state = 1;
} elseif ($token->value === ')') {
$state = 6;
++$list->idx;
break;
} else {
$parser->error('A comma or a closing bracket was expected.', $token);
$state = 0;
break;
}
}
}
// Last iteration was not saved.
if (! empty($expr->type) || ! empty($expr->key)) {
$ret[] = $expr;
}
if (($state !== 0) && ($state !== 6)) {
$parser->error('A closing bracket was expected.', $list->tokens[$list->idx - 1]);
}
--$list->idx;
return $ret;
}
/**
* @param CreateDefinition|CreateDefinition[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return "(\n " . implode(",\n ", $component) . "\n)";
}
$tmp = '';
if ($component->isConstraint) {
$tmp .= 'CONSTRAINT ';
}
if (isset($component->name) && ($component->name !== '')) {
$tmp .= Context::escape($component->name) . ' ';
}
if (! empty($component->type)) {
$tmp .= DataType::build(
$component->type,
['lowercase' => true]
) . ' ';
}
if (! empty($component->key)) {
$tmp .= $component->key . ' ';
}
if (! empty($component->references)) {
$tmp .= 'REFERENCES ' . $component->references . ' ';
}
$tmp .= $component->options;
return trim($tmp);
}
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function strtolower;
use function strtoupper;
use function trim;
/**
* Parses a data type.
*
* @final
*/
class DataType extends Component
{
/**
* All data type options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $DATA_TYPE_OPTIONS = [
'BINARY' => 1,
'CHARACTER SET' => [
2,
'var',
],
'CHARSET' => [
2,
'var',
],
'COLLATE' => [
3,
'var',
],
'UNSIGNED' => 4,
'ZEROFILL' => 5,
];
/**
* The name of the data type.
*
* @var string
*/
public $name;
/**
* The parameters of this data type.
*
* Some data types have no parameters.
* Numeric types might have parameters for the maximum number of digits,
* precision, etc.
* String types might have parameters for the maximum length stored.
* `ENUM` and `SET` have parameters for possible values.
*
* For more information, check the MySQL manual.
*
* @var int[]|string[]
*/
public $parameters = [];
/**
* The options of this data type.
*
* @var OptionsArray
*/
public $options;
/**
* @param string $name the name of this data type
* @param int[]|string[] $parameters the parameters (size or possible values)
* @param OptionsArray $options the options of this data type
*/
public function __construct(
$name = null,
array $parameters = [],
$options = null
) {
$this->name = $name;
$this->parameters = $parameters;
$this->options = $options;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return DataType|null
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -------------------[ data type ]--------------------> 1
*
* 1 ----------------[ size and options ]----------------> 2
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$ret->name = strtoupper((string) $token->value);
if (($token->type !== Token::TYPE_KEYWORD) || (! ($token->flags & Token::FLAG_KEYWORD_DATA_TYPE))) {
$parser->error('Unrecognized data type.', $token);
}
$state = 1;
} elseif ($state === 1) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
$parameters = ArrayObj::parse($parser, $list);
++$list->idx;
$ret->parameters = ($ret->name === 'ENUM') || ($ret->name === 'SET') ?
$parameters->raw : $parameters->values;
}
$ret->options = OptionsArray::parse($parser, $list, static::$DATA_TYPE_OPTIONS);
++$list->idx;
break;
}
}
if (empty($ret->name)) {
return null;
}
--$list->idx;
return $ret;
}
/**
* @param DataType $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
$name = empty($options['lowercase']) ?
$component->name : strtolower($component->name);
$parameters = '';
if (! empty($component->parameters)) {
$parameters = '(' . implode(',', $component->parameters) . ')';
}
return trim($name . $parameters . ' ' . $component->options);
}
}

View File

@@ -0,0 +1,487 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Context;
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
use function rtrim;
use function strlen;
use function trim;
/**
* Parses a reference to an expression (column, table or database name, function
* call, mathematical expression, etc.).
*
* @final
*/
#[\AllowDynamicProperties]
class Expression extends Component
{
/**
* List of allowed reserved keywords in expressions.
*
* @var array<string, int>
*/
private static $ALLOWED_KEYWORDS = [
'AND' => 1,
'AS' => 1,
'BETWEEN' => 1,
'CASE' => 1,
'DUAL' => 1,
'DIV' => 1,
'IS' => 1,
'MOD' => 1,
'NOT' => 1,
'NOT NULL' => 1,
'NULL' => 1,
'OR' => 1,
'OVER' => 1,
'REGEXP' => 1,
'RLIKE' => 1,
'XOR' => 1,
];
/**
* The name of this database.
*
* @var string|null
*/
public $database;
/**
* The name of this table.
*
* @var string|null
*/
public $table;
/**
* The name of the column.
*
* @var string|null
*/
public $column;
/**
* The sub-expression.
*
* @var string|null
*/
public $expr = '';
/**
* The alias of this expression.
*
* @var string|null
*/
public $alias;
/**
* The name of the function.
*
* @var mixed
*/
public $function;
/**
* The type of subquery.
*
* @var string|null
*/
public $subquery;
/**
* Syntax:
* new Expression('expr')
* new Expression('expr', 'alias')
* new Expression('database', 'table', 'column')
* new Expression('database', 'table', 'column', 'alias')
*
* If the database, table or column name is not required, pass an empty
* string.
*
* @param string|null $database The name of the database or the expression.
* @param string|null $table The name of the table or the alias of the expression.
* @param string|null $column the name of the column
* @param string|null $alias the name of the alias
*/
public function __construct($database = null, $table = null, $column = null, $alias = null)
{
if (($column === null) && ($alias === null)) {
$this->expr = $database; // case 1
$this->alias = $table; // case 2
} else {
$this->database = $database; // case 3
$this->table = $table; // case 3
$this->column = $column; // case 3
$this->alias = $alias; // case 4
}
}
/**
* Possible options:.
*
* `field`
*
* First field to be filled.
* If this is not specified, it takes the value of `parseField`.
*
* `parseField`
*
* Specifies the type of the field parsed. It may be `database`,
* `table` or `column`. These expressions may not include
* parentheses.
*
* `breakOnAlias`
*
* If not empty, breaks when the alias occurs (it is not included).
*
* `breakOnParentheses`
*
* If not empty, breaks when the first parentheses occurs.
*
* `parenthesesDelimited`
*
* If not empty, breaks after last parentheses occurred.
*
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return Expression|null
*
* @throws ParserException
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* Whether current tokens make an expression or a table reference.
*
* @var bool
*/
$isExpr = false;
/**
* Whether a period was previously found.
*
* @var bool
*/
$dot = false;
/**
* Whether an alias is expected. Is 2 if `AS` keyword was found.
*
* @var bool
*/
$alias = false;
/**
* Counts brackets.
*
* @var int
*/
$brackets = 0;
/**
* Keeps track of the last two previous tokens.
*
* @var Token[]
*/
$prev = [
null,
null,
];
// When a field is parsed, no parentheses are expected.
if (! empty($options['parseField'])) {
$options['breakOnParentheses'] = true;
$options['field'] = $options['parseField'];
}
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
if ($isExpr) {
$ret->expr .= $token->token;
}
continue;
}
if ($token->type === Token::TYPE_KEYWORD) {
if (($brackets > 0) && empty($ret->subquery) && ! empty(Parser::$STATEMENT_PARSERS[$token->keyword])) {
// A `(` was previously found and this keyword is the
// beginning of a statement, so this is a subquery.
$ret->subquery = $token->keyword;
} elseif (
($token->flags & Token::FLAG_KEYWORD_FUNCTION)
&& (empty($options['parseField'])
&& ! $alias)
) {
$isExpr = true;
} elseif (($token->flags & Token::FLAG_KEYWORD_RESERVED) && ($brackets === 0)) {
if (empty(self::$ALLOWED_KEYWORDS[$token->keyword])) {
// A reserved keyword that is not allowed in the
// expression was found so the expression must have
// ended and a new clause is starting.
break;
}
if ($token->keyword === 'AS') {
if (! empty($options['breakOnAlias'])) {
break;
}
if ($alias) {
$parser->error('An alias was expected.', $token);
break;
}
$alias = true;
continue;
}
if ($token->keyword === 'CASE') {
// For a use of CASE like
// 'SELECT a = CASE .... END, b=1, `id`, ... FROM ...'
$tempCaseExpr = CaseExpression::parse($parser, $list);
$ret->expr .= CaseExpression::build($tempCaseExpr);
$isExpr = true;
continue;
}
$isExpr = true;
} elseif ($brackets === 0 && strlen((string) $ret->expr) > 0 && ! $alias) {
/* End of expression */
break;
}
}
if (
($token->type === Token::TYPE_NUMBER)
|| ($token->type === Token::TYPE_BOOL)
|| (($token->type === Token::TYPE_SYMBOL)
&& ($token->flags & Token::FLAG_SYMBOL_VARIABLE))
|| (($token->type === Token::TYPE_SYMBOL)
&& ($token->flags & Token::FLAG_SYMBOL_PARAMETER))
|| (($token->type === Token::TYPE_OPERATOR)
&& ($token->value !== '.'))
) {
if (! empty($options['parseField'])) {
break;
}
// Numbers, booleans and operators (except dot) are usually part
// of expressions.
$isExpr = true;
}
if ($token->type === Token::TYPE_OPERATOR) {
if (! empty($options['breakOnParentheses']) && (($token->value === '(') || ($token->value === ')'))) {
// No brackets were expected.
break;
}
if ($token->value === '(') {
++$brackets;
if (
empty($ret->function) && ($prev[1] !== null)
&& (($prev[1]->type === Token::TYPE_NONE)
|| ($prev[1]->type === Token::TYPE_SYMBOL)
|| (($prev[1]->type === Token::TYPE_KEYWORD)
&& ($prev[1]->flags & Token::FLAG_KEYWORD_FUNCTION)))
) {
$ret->function = $prev[1]->value;
}
} elseif ($token->value === ')') {
if ($brackets === 0) {
// Not our bracket
break;
}
--$brackets;
if ($brackets === 0) {
if (! empty($options['parenthesesDelimited'])) {
// The current token is the last bracket, the next
// one will be outside the expression.
$ret->expr .= $token->token;
++$list->idx;
break;
}
} elseif ($brackets < 0) {
// $parser->error('Unexpected closing bracket.', $token);
// $brackets = 0;
break;
}
} elseif ($token->value === ',') {
// Expressions are comma-delimited.
if ($brackets === 0) {
break;
}
}
}
// Saving the previous tokens.
$prev[0] = $prev[1];
$prev[1] = $token;
if ($alias) {
// An alias is expected (the keyword `AS` was previously found).
if (! empty($ret->alias)) {
$parser->error('An alias was previously found.', $token);
break;
}
$ret->alias = $token->value;
$alias = false;
} elseif ($isExpr) {
// Handling aliases.
if (
$brackets === 0
&& ($prev[0] === null
|| (($prev[0]->type !== Token::TYPE_OPERATOR || $prev[0]->token === ')')
&& ($prev[0]->type !== Token::TYPE_KEYWORD
|| ! ($prev[0]->flags & Token::FLAG_KEYWORD_RESERVED))))
&& (($prev[1]->type === Token::TYPE_STRING)
|| ($prev[1]->type === Token::TYPE_SYMBOL
&& ! ($prev[1]->flags & Token::FLAG_SYMBOL_VARIABLE))
|| ($prev[1]->type === Token::TYPE_NONE))
) {
if (! empty($ret->alias)) {
$parser->error('An alias was previously found.', $token);
break;
}
$ret->alias = $prev[1]->value;
} else {
$currIdx = $list->idx;
--$list->idx;
$beforeToken = $list->getPrevious();
$list->idx = $currIdx;
// columns names tokens are of type NONE, or SYMBOL (`col`), and the columns options
// would start with a token of type KEYWORD, in that case, we want to have a space
// between the tokens.
if (
$ret->expr !== null &&
$beforeToken &&
($beforeToken->type === Token::TYPE_NONE ||
$beforeToken->type === Token::TYPE_SYMBOL || $beforeToken->type === Token::TYPE_STRING) &&
$token->type === Token::TYPE_KEYWORD
) {
$ret->expr = rtrim($ret->expr, ' ') . ' ';
}
$ret->expr .= $token->token;
}
} elseif (! $isExpr) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '.')) {
// Found a `.` which means we expect a column name and
// the column name we parsed is actually the table name
// and the table name is actually a database name.
if (! empty($ret->database) || $dot) {
$parser->error('Unexpected dot.', $token);
}
$ret->database = $ret->table;
$ret->table = $ret->column;
$ret->column = null;
$dot = true;
$ret->expr .= $token->token;
} else {
$field = empty($options['field']) ? 'column' : $options['field'];
if (empty($ret->$field)) {
$ret->$field = $token->value;
$ret->expr .= $token->token;
$dot = false;
} else {
// No alias is expected.
if (! empty($options['breakOnAlias'])) {
break;
}
if (! empty($ret->alias)) {
$parser->error('An alias was previously found.', $token);
break;
}
$ret->alias = $token->value;
}
}
}
}
if ($alias) {
$parser->error('An alias was expected.', $list->tokens[$list->idx - 1]);
}
// White-spaces might be added at the end.
$ret->expr = trim((string) $ret->expr);
if ($ret->expr === '') {
return null;
}
--$list->idx;
return $ret;
}
/**
* @param Expression|Expression[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(', ', $component);
}
if ($component->expr !== '' && $component->expr !== null) {
$ret = $component->expr;
} else {
$fields = [];
if (isset($component->database) && ($component->database !== '')) {
$fields[] = $component->database;
}
if (isset($component->table) && ($component->table !== '')) {
$fields[] = $component->table;
}
if (isset($component->column) && ($component->column !== '')) {
$fields[] = $component->column;
}
$ret = implode('.', Context::escape($fields));
}
if (! empty($component->alias)) {
$ret .= ' AS ' . Context::escape($component->alias);
}
return $ret;
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function count;
use function implode;
use function is_array;
use function preg_match;
use function strlen;
use function substr;
/**
* Parses a list of expressions delimited by a comma.
*
* @final
*/
class ExpressionArray extends Component
{
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return Expression[]
*
* @throws ParserException
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ----------------------[ array ]---------------------> 1
*
* 1 ------------------------[ , ]------------------------> 0
* 1 -----------------------[ else ]----------------------> (END)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if (
($token->type === Token::TYPE_KEYWORD)
&& ($token->flags & Token::FLAG_KEYWORD_RESERVED)
&& ((~$token->flags & Token::FLAG_KEYWORD_FUNCTION))
&& ($token->value !== 'DUAL')
&& ($token->value !== 'NULL')
&& ($token->value !== 'CASE')
&& ($token->value !== 'NOT')
) {
// No keyword is expected.
break;
}
if ($state === 0) {
if ($token->type === Token::TYPE_KEYWORD && $token->value === 'CASE') {
$expr = CaseExpression::parse($parser, $list, $options);
} else {
$expr = Expression::parse($parser, $list, $options);
}
if ($expr === null) {
break;
}
$ret[] = $expr;
$state = 1;
} elseif ($state === 1) {
if ($token->value !== ',') {
break;
}
$state = 0;
}
}
if ($state === 0) {
$parser->error('An expression was expected.', $list->tokens[$list->idx]);
}
--$list->idx;
if (is_array($ret)) {
$retIndex = count($ret) - 1;
if (isset($ret[$retIndex])) {
$expr = $ret[$retIndex]->expr;
if (preg_match('/\s*--\s.*$/', $expr, $matches)) {
$found = $matches[0];
$ret[$retIndex]->expr = substr($expr, 0, strlen($expr) - strlen($found));
}
}
}
return $ret;
}
/**
* @param Expression[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
$ret = [];
foreach ($component as $frag) {
$ret[] = $frag::build($frag);
}
return implode(', ', $ret);
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function is_array;
/**
* Parses a function call.
*
* @final
*/
class FunctionCall extends Component
{
/**
* The name of this function.
*
* @var string|null
*/
public $name;
/**
* The list of parameters.
*
* @var ArrayObj|null
*/
public $parameters;
/**
* @param string|null $name the name of the function to be called
* @param string[]|ArrayObj|null $parameters the parameters of this function
*/
public function __construct($name = null, $parameters = null)
{
$this->name = $name;
if (is_array($parameters)) {
$this->parameters = new ArrayObj($parameters);
} elseif ($parameters instanceof ArrayObj) {
$this->parameters = $parameters;
}
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return FunctionCall
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ----------------------[ name ]-----------------------> 1
*
* 1 --------------------[ parameters ]-------------------> (END)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$ret->name = $token->value;
$state = 1;
} elseif ($state === 1) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
$ret->parameters = ArrayObj::parse($parser, $list);
}
break;
}
}
return $ret;
}
/**
* @param FunctionCall $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
return $component->name . $component->parameters;
}
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
use function trim;
/**
* `GROUP BY` keyword parser.
*
* @final
*/
class GroupKeyword extends Component
{
/** @var mixed */
public $type;
/**
* The expression that is used for grouping.
*
* @var Expression
*/
public $expr;
/**
* @param Expression $expr the expression that we are sorting by
*/
public function __construct($expr = null)
{
$this->expr = $expr;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return GroupKeyword[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 --------------------[ expression ]-------------------> 1
*
* 1 ------------------------[ , ]------------------------> 0
* 1 -------------------[ ASC / DESC ]--------------------> 1
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$expr->expr = Expression::parse($parser, $list);
$state = 1;
} elseif ($state === 1) {
if (
($token->type === Token::TYPE_KEYWORD)
&& (($token->keyword === 'ASC') || ($token->keyword === 'DESC'))
) {
$expr->type = $token->keyword;
} elseif (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) {
if (! empty($expr->expr)) {
$ret[] = $expr;
}
$expr = new static();
$state = 0;
} else {
break;
}
}
}
// Last iteration was not processed.
if (! empty($expr->expr)) {
$ret[] = $expr;
}
--$list->idx;
return $ret;
}
/**
* @param GroupKeyword|GroupKeyword[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(', ', $component);
}
return trim((string) $component->expr);
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
/**
* Parses an Index hint.
*
* @final
*/
class IndexHint extends Component
{
/**
* The type of hint (USE/FORCE/IGNORE)
*
* @var string
*/
public $type;
/**
* What the hint is for (INDEX/KEY)
*
* @var string
*/
public $indexOrKey;
/**
* The clause for which this hint is (JOIN/ORDER BY/GROUP BY)
*
* @var string
*/
public $for;
/**
* List of indexes in this hint
*
* @var Expression[]
*/
public $indexes = [];
/**
* @param string $type the type of hint (USE/FORCE/IGNORE)
* @param string $indexOrKey What the hint is for (INDEX/KEY)
* @param string $for the clause for which this hint is (JOIN/ORDER BY/GROUP BY)
* @param Expression[] $indexes List of indexes in this hint
*/
public function __construct(
?string $type = null,
?string $indexOrKey = null,
?string $for = null,
array $indexes = []
) {
$this->type = $type;
$this->indexOrKey = $indexOrKey;
$this->for = $for;
$this->indexes = $indexes;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return IndexHint|Component[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
$expr->type = $options['type'] ?? null;
/**
* The state of the parser.
*
* Below are the states of the parser.
* 0 ----------------- [ USE/IGNORE/FORCE ]-----------------> 1
* 1 -------------------- [ INDEX/KEY ] --------------------> 2
* 2 ----------------------- [ FOR ] -----------------------> 3
* 2 -------------------- [ expr_list ] --------------------> 0
* 3 -------------- [ JOIN/GROUP BY/ORDER BY ] -------------> 4
* 4 -------------------- [ expr_list ] --------------------> 0
*
* @var int
*/
$state = 0;
// By design, the parser will parse first token after the keyword. So, the keyword
// must be analyzed too, in order to determine the type of this index hint.
if ($list->idx > 0) {
--$list->idx;
}
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
switch ($state) {
case 0:
if ($token->type === Token::TYPE_KEYWORD) {
if ($token->keyword !== 'USE' && $token->keyword !== 'IGNORE' && $token->keyword !== 'FORCE') {
break 2;
}
$expr->type = $token->keyword;
$state = 1;
}
break;
case 1:
if ($token->type === Token::TYPE_KEYWORD) {
if ($token->keyword === 'INDEX' || $token->keyword === 'KEY') {
$expr->indexOrKey = $token->keyword;
} else {
$parser->error('Unexpected keyword.', $token);
}
$state = 2;
} else {
// we expect the token to be a keyword
$parser->error('Unexpected token.', $token);
}
break;
case 2:
if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'FOR') {
$state = 3;
} else {
$expr->indexes = ExpressionArray::parse($parser, $list);
$state = 0;
$ret[] = $expr;
$expr = new static();
}
break;
case 3:
if ($token->type === Token::TYPE_KEYWORD) {
if (
$token->keyword === 'JOIN'
|| $token->keyword === 'GROUP BY'
|| $token->keyword === 'ORDER BY'
) {
$expr->for = $token->keyword;
} else {
$parser->error('Unexpected keyword.', $token);
}
$state = 4;
} else {
// we expect the token to be a keyword
$parser->error('Unexpected token.', $token);
}
break;
case 4:
$expr->indexes = ExpressionArray::parse($parser, $list);
$state = 0;
$ret[] = $expr;
$expr = new static();
break;
}
}
--$list->idx;
return $ret;
}
/**
* @param IndexHint|IndexHint[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(' ', $component);
}
$ret = $component->type . ' ' . $component->indexOrKey . ' ';
if ($component->for !== null) {
$ret .= 'FOR ' . $component->for . ' ';
}
return $ret . ExpressionArray::build($component->indexes);
}
}

View File

@@ -0,0 +1,298 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function trim;
/**
* `INTO` keyword parser.
*
* @final
*/
class IntoKeyword extends Component
{
/**
* FIELDS/COLUMNS Options for `SELECT...INTO` statements.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $FIELDS_OPTIONS = [
'TERMINATED BY' => [
1,
'expr',
],
'OPTIONALLY' => 2,
'ENCLOSED BY' => [
3,
'expr',
],
'ESCAPED BY' => [
4,
'expr',
],
];
/**
* LINES Options for `SELECT...INTO` statements.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $LINES_OPTIONS = [
'STARTING BY' => [
1,
'expr',
],
'TERMINATED BY' => [
2,
'expr',
],
];
/**
* Type of target (OUTFILE or SYMBOL).
*
* @var string|null
*/
public $type;
/**
* The destination, which can be a table or a file.
*
* @var string|Expression|null
*/
public $dest;
/**
* The name of the columns.
*
* @var string[]|null
*/
public $columns;
/**
* The values to be selected into (SELECT .. INTO @var1).
*
* @var Expression[]|null
*/
public $values;
/**
* Options for FIELDS/COLUMNS keyword.
*
* @see static::$FIELDS_OPTIONS
*
* @var OptionsArray|null
*/
public $fields_options;
/**
* Whether to use `FIELDS` or `COLUMNS` while building.
*
* @var bool|null
*/
public $fields_keyword;
/**
* Options for OPTIONS keyword.
*
* @see static::$LINES_OPTIONS
*
* @var OptionsArray|null
*/
public $lines_options;
/**
* @param string|null $type type of destination (may be OUTFILE)
* @param string|Expression|null $dest actual destination
* @param string[]|null $columns column list of destination
* @param Expression[]|null $values selected fields
* @param OptionsArray|null $fieldsOptions options for FIELDS/COLUMNS keyword
* @param bool|null $fieldsKeyword options for OPTIONS keyword
*/
public function __construct(
$type = null,
$dest = null,
$columns = null,
$values = null,
$fieldsOptions = null,
$fieldsKeyword = null
) {
$this->type = $type;
$this->dest = $dest;
$this->columns = $columns;
$this->values = $values;
$this->fields_options = $fieldsOptions;
$this->fields_keyword = $fieldsKeyword;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return IntoKeyword
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -----------------------[ name ]----------------------> 1
* 0 ---------------------[ OUTFILE ]---------------------> 2
*
* 1 ------------------------[ ( ]------------------------> (END)
*
* 2 ---------------------[ filename ]--------------------> 1
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_RESERVED)) {
if (($state === 0) && ($token->keyword === 'OUTFILE')) {
$ret->type = 'OUTFILE';
$state = 2;
continue;
}
// No other keyword is expected except for $state = 4, which expects `LINES`
if ($state !== 4) {
break;
}
}
if ($state === 0) {
if (
(isset($options['fromInsert'])
&& $options['fromInsert'])
|| (isset($options['fromReplace'])
&& $options['fromReplace'])
) {
$ret->dest = Expression::parse(
$parser,
$list,
[
'parseField' => 'table',
'breakOnAlias' => true,
]
);
} else {
$ret->values = ExpressionArray::parse($parser, $list);
}
$state = 1;
} elseif ($state === 1) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
$ret->columns = ArrayObj::parse($parser, $list)->values;
++$list->idx;
}
break;
} elseif ($state === 2) {
$ret->dest = $token->value;
$state = 3;
} elseif ($state === 3) {
$ret->parseFileOptions($parser, $list, $token->value);
$state = 4;
} elseif ($state === 4) {
if ($token->type === Token::TYPE_KEYWORD && $token->keyword !== 'LINES') {
break;
}
$ret->parseFileOptions($parser, $list, $token->value);
$state = 5;
}
}
--$list->idx;
return $ret;
}
/**
* @param Parser $parser The parser
* @param TokensList $list A token list
* @param string $keyword They keyword
*
* @return void
*/
public function parseFileOptions(Parser $parser, TokensList $list, $keyword = 'FIELDS')
{
++$list->idx;
if ($keyword === 'FIELDS' || $keyword === 'COLUMNS') {
// parse field options
$this->fields_options = OptionsArray::parse($parser, $list, static::$FIELDS_OPTIONS);
$this->fields_keyword = ($keyword === 'FIELDS');
} else {
// parse line options
$this->lines_options = OptionsArray::parse($parser, $list, static::$LINES_OPTIONS);
}
}
/**
* @param IntoKeyword $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if ($component->dest instanceof Expression) {
$columns = ! empty($component->columns) ? '(`' . implode('`, `', $component->columns) . '`)' : '';
return $component->dest . $columns;
}
if (isset($component->values)) {
return ExpressionArray::build($component->values);
}
$ret = 'OUTFILE "' . $component->dest . '"';
$fieldsOptionsString = OptionsArray::build($component->fields_options);
if (trim($fieldsOptionsString) !== '') {
$ret .= $component->fields_keyword ? ' FIELDS' : ' COLUMNS';
$ret .= ' ' . $fieldsOptionsString;
}
$linesOptionsString = OptionsArray::build($component->lines_options, ['expr' => true]);
if (trim($linesOptionsString) !== '') {
$ret .= ' LINES ' . $linesOptionsString;
}
return $ret;
}
}

View File

@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function array_search;
use function implode;
/**
* `JOIN` keyword parser.
*
* @final
*/
class JoinKeyword extends Component
{
/**
* Types of join.
*
* @var array<string, string>
*/
public static $JOINS = [
'CROSS JOIN' => 'CROSS',
'FULL JOIN' => 'FULL',
'FULL OUTER JOIN' => 'FULL',
'INNER JOIN' => 'INNER',
'JOIN' => 'JOIN',
'LEFT JOIN' => 'LEFT',
'LEFT OUTER JOIN' => 'LEFT',
'RIGHT JOIN' => 'RIGHT',
'RIGHT OUTER JOIN' => 'RIGHT',
'NATURAL JOIN' => 'NATURAL',
'NATURAL LEFT JOIN' => 'NATURAL LEFT',
'NATURAL RIGHT JOIN' => 'NATURAL RIGHT',
'NATURAL LEFT OUTER JOIN' => 'NATURAL LEFT OUTER',
'NATURAL RIGHT OUTER JOIN' => 'NATURAL RIGHT OUTER',
'STRAIGHT_JOIN' => 'STRAIGHT',
];
/**
* Type of this join.
*
* @see static::$JOINS
*
* @var string
*/
public $type;
/**
* Join expression.
*
* @var Expression
*/
public $expr;
/**
* Join conditions.
*
* @var Condition[]
*/
public $on;
/**
* Columns in Using clause.
*
* @var ArrayObj
*/
public $using;
/**
* @see JoinKeyword::$JOINS
*
* @param string $type Join type
* @param Expression $expr join expression
* @param Condition[] $on join conditions
* @param ArrayObj $using columns joined
*/
public function __construct($type = null, $expr = null, $on = null, $using = null)
{
$this->type = $type;
$this->expr = $expr;
$this->on = $on;
$this->using = $using;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return JoinKeyword[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -----------------------[ JOIN ]----------------------> 1
*
* 1 -----------------------[ expr ]----------------------> 2
*
* 2 ------------------------[ ON ]-----------------------> 3
* 2 -----------------------[ USING ]---------------------> 4
*
* 3 --------------------[ conditions ]-------------------> 0
*
* 4 ----------------------[ columns ]--------------------> 0
*
* @var int
*/
$state = 0;
// By design, the parser will parse first token after the keyword.
// In this case, the keyword must be analyzed too, in order to determine
// the type of this join.
if ($list->idx > 0) {
--$list->idx;
}
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
if (($token->type !== Token::TYPE_KEYWORD) || empty(static::$JOINS[$token->keyword])) {
break;
}
$expr->type = static::$JOINS[$token->keyword];
$state = 1;
} elseif ($state === 1) {
$expr->expr = Expression::parse($parser, $list, ['field' => 'table']);
$state = 2;
} elseif ($state === 2) {
if ($token->type === Token::TYPE_KEYWORD) {
switch ($token->keyword) {
case 'ON':
$state = 3;
break;
case 'USING':
$state = 4;
break;
default:
if (empty(static::$JOINS[$token->keyword])) {
/* Next clause is starting */
break 2;
}
$ret[] = $expr;
$expr = new static();
$expr->type = static::$JOINS[$token->keyword];
$state = 1;
break;
}
}
} elseif ($state === 3) {
$expr->on = Condition::parse($parser, $list);
$ret[] = $expr;
$expr = new static();
$state = 0;
} elseif ($state === 4) {
$expr->using = ArrayObj::parse($parser, $list);
$ret[] = $expr;
$expr = new static();
$state = 0;
}
}
if (! empty($expr->type)) {
$ret[] = $expr;
}
--$list->idx;
return $ret;
}
/**
* @param JoinKeyword[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
$ret = [];
foreach ($component as $c) {
$ret[] = array_search($c->type, static::$JOINS) . ' ' . $c->expr
. (! empty($c->on)
? ' ON ' . Condition::build($c->on) : '')
. (! empty($c->using)
? ' USING ' . ArrayObj::build($c->using) : '');
}
return implode(' ', $ret);
}
}

View File

@@ -0,0 +1,307 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Context;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function trim;
/**
* Parses the definition of a key.
*
* Used for parsing `CREATE TABLE` statement.
*
* @final
*/
class Key extends Component
{
/**
* All key options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $KEY_OPTIONS = [
'KEY_BLOCK_SIZE' => [
1,
'var=',
],
'USING' => [
2,
'var',
],
'WITH PARSER' => [
3,
'var',
],
'COMMENT' => [
4,
'var',
],
// MariaDB options
'CLUSTERING' => [
4,
'var=',
],
'ENGINE_ATTRIBUTE' => [
5,
'var=',
],
'SECONDARY_ENGINE_ATTRIBUTE' => [
5,
'var=',
],
// MariaDB & MySQL options
'VISIBLE' => 6,
'INVISIBLE' => 6,
// MariaDB options
'IGNORED' => 10,
'NOT IGNORED' => 10,
];
/**
* The name of this key.
*
* @var string
*/
public $name;
/**
* The key columns
*
* @var array<int, array<string, int|string>>
* @phpstan-var array{name?: string, length?: int, order?: string}[]
*/
public $columns;
/**
* The type of this key.
*
* @var string
*/
public $type;
/**
* The expression if the Key is not using column names
*
* @var string|null
*/
public $expr = null;
/**
* The options of this key or null if none where found.
*
* @var OptionsArray|null
*/
public $options;
/**
* @param string $name the name of the key
* @param array<int, array<string, int|string>> $columns the columns covered by this key
* @param string $type the type of this key
* @param OptionsArray $options the options of this key
* @phpstan-param array{name?: string, length?: int, order?: string}[] $columns
*/
public function __construct(
$name = null,
array $columns = [],
$type = null,
$options = null
) {
$this->name = $name;
$this->columns = $columns;
$this->type = $type;
$this->options = $options;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return Key
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* Last parsed column.
*
* @var array<string,mixed>
*/
$lastColumn = [];
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------[ type ]---------------------------> 1
*
* 1 ---------------------[ name ]---------------------------> 1
* 1 ---------------------[ columns ]------------------------> 2
* 1 ---------------------[ expression ]---------------------> 5
*
* 2 ---------------------[ column length ]------------------> 3
* 3 ---------------------[ column length ]------------------> 2
* 2 ---------------------[ options ]------------------------> 4
* 5 ---------------------[ expression ]---------------------> 4
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$ret->type = $token->value;
$state = 1;
} elseif ($state === 1) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
$positionBeforeSearch = $list->idx;
$list->idx++;// Ignore the current token "(" or the search condition will always be true
$nextToken = $list->getNext();
$list->idx = $positionBeforeSearch;// Restore the position
if ($nextToken !== null && $nextToken->value === '(') {
// Switch to expression mode
$state = 5;
} else {
$state = 2;
}
} else {
$ret->name = $token->value;
}
} elseif ($state === 2) {
if ($token->type === Token::TYPE_OPERATOR) {
if ($token->value === '(') {
$state = 3;
} elseif (($token->value === ',') || ($token->value === ')')) {
$state = $token->value === ',' ? 2 : 4;
if (! empty($lastColumn)) {
$ret->columns[] = $lastColumn;
$lastColumn = [];
}
}
} elseif (
(
$token->type === Token::TYPE_KEYWORD
)
&&
(
($token->keyword === 'ASC') || ($token->keyword === 'DESC')
)
) {
$lastColumn['order'] = $token->keyword;
} else {
$lastColumn['name'] = $token->value;
}
} elseif ($state === 3) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ')')) {
$state = 2;
} else {
$lastColumn['length'] = $token->value;
}
} elseif ($state === 4) {
$ret->options = OptionsArray::parse($parser, $list, static::$KEY_OPTIONS);
++$list->idx;
break;
} elseif ($state === 5) {
if ($token->type === Token::TYPE_OPERATOR) {
// This got back to here and we reached the end of the expression
if ($token->value === ')') {
$state = 4;// go back to state 4 to fetch options
continue;
}
// The expression is not finished, adding a separator for the next expression
if ($token->value === ',') {
$ret->expr .= ', ';
continue;
}
// Start of the expression
if ($token->value === '(') {
// This is the first expression, set to empty
if ($ret->expr === null) {
$ret->expr = '';
}
$ret->expr .= Expression::parse($parser, $list, ['parenthesesDelimited' => true]);
continue;
}
// Another unexpected operator was found
}
// Something else than an operator was found
$parser->error('Unexpected token.', $token);
}
}
--$list->idx;
return $ret;
}
/**
* @param Key $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
$ret = $component->type . ' ';
if (! empty($component->name)) {
$ret .= Context::escape($component->name) . ' ';
}
if ($component->expr !== null) {
return $ret . '(' . $component->expr . ') ' . $component->options;
}
$columns = [];
foreach ($component->columns as $column) {
$tmp = '';
if (isset($column['name'])) {
$tmp .= Context::escape($column['name']);
}
if (isset($column['length'])) {
$tmp .= '(' . $column['length'] . ')';
}
if (isset($column['order'])) {
$tmp .= ' ' . $column['order'];
}
$columns[] = $tmp;
}
$ret .= '(' . implode(',', $columns) . ') ' . $component->options;
return trim($ret);
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
/**
* `LIMIT` keyword parser.
*
* @final
*/
class Limit extends Component
{
/**
* The number of rows skipped.
*
* @var int
*/
public $offset;
/**
* The number of rows to be returned.
*
* @var int
*/
public $rowCount;
/**
* @param int $rowCount the row count
* @param int $offset the offset
*/
public function __construct($rowCount = 0, $offset = 0)
{
$this->rowCount = $rowCount;
$this->offset = $offset;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return Limit
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
$offset = false;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_RESERVED)) {
break;
}
if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'OFFSET') {
if ($offset) {
$parser->error('An offset was expected.', $token);
}
$offset = true;
continue;
}
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) {
$ret->offset = $ret->rowCount;
$ret->rowCount = 0;
continue;
}
// Skip if not a number
if (($token->type !== Token::TYPE_NUMBER)) {
break;
}
if ($offset) {
$ret->offset = $token->value;
$offset = false;
} else {
$ret->rowCount = $token->value;
}
}
if ($offset) {
$parser->error('An offset was expected.', $list->tokens[$list->idx - 1]);
}
--$list->idx;
return $ret;
}
/**
* @param Limit $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
return $component->offset . ', ' . $component->rowCount;
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
/**
* Parses a reference to a LOCK expression.
*
* @final
*/
class LockExpression extends Component
{
/**
* The table to be locked.
*
* @var Expression
*/
public $table;
/**
* The type of lock to be applied.
*
* @var string
*/
public $type;
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return LockExpression
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------- [ tbl_name ] -----------------> 1
* 1 ---------------- [ lock_type ] ----------------> 2
* 2 -------------------- [ , ] --------------------> break
*
* @var int
*/
$state = 0;
$prevToken = null;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if (
$token->type === Token::TYPE_DELIMITER
|| ($token->type === Token::TYPE_OPERATOR
&& $token->value === ',')
) {
break;
}
if ($state === 0) {
$ret->table = Expression::parse($parser, $list, ['parseField' => 'table']);
$state = 1;
} elseif ($state === 1) {
// parse lock type
$ret->type = self::parseLockType($parser, $list);
$state = 2;
}
$prevToken = $token;
}
// 2 is the only valid end state
if ($state !== 2) {
$parser->error('Unexpected end of LOCK expression.', $prevToken);
}
--$list->idx;
return $ret;
}
/**
* @param LockExpression|LockExpression[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(', ', $component);
}
return $component->table . ' ' . $component->type;
}
/**
* @return string
*/
private static function parseLockType(Parser $parser, TokensList $list)
{
$lockType = '';
/**
* The state of the parser while parsing for lock type.
*
* Below are the states of the parser.
*
* 0 ---------------- [ READ ] -----------------> 1
* 0 ------------- [ LOW_PRIORITY ] ------------> 2
* 0 ---------------- [ WRITE ] ----------------> 3
* 1 ---------------- [ LOCAL ] ----------------> 3
* 2 ---------------- [ WRITE ] ----------------> 3
*
* @var int
*/
$state = 0;
$prevToken = null;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if (
$token->type === Token::TYPE_DELIMITER
|| ($token->type === Token::TYPE_OPERATOR
&& $token->value === ',')
) {
--$list->idx;
break;
}
// Skipping whitespaces and comments.
if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
continue;
}
// We only expect keywords
if ($token->type !== Token::TYPE_KEYWORD) {
$parser->error('Unexpected token.', $token);
break;
}
if ($state === 0) {
if ($token->keyword === 'READ') {
$state = 1;
} elseif ($token->keyword === 'LOW_PRIORITY') {
$state = 2;
} elseif ($token->keyword === 'WRITE') {
$state = 3;
} else {
$parser->error('Unexpected keyword.', $token);
break;
}
$lockType .= $token->keyword;
} elseif ($state === 1) {
if ($token->keyword !== 'LOCAL') {
$parser->error('Unexpected keyword.', $token);
break;
}
$lockType .= ' ' . $token->keyword;
$state = 3;
} elseif ($state === 2) {
if ($token->keyword !== 'WRITE') {
$parser->error('Unexpected keyword.', $token);
break;
}
$lockType .= ' ' . $token->keyword;
$state = 3; // parsing over
}
$prevToken = $token;
}
// Only two possible end states
if ($state !== 1 && $state !== 3) {
$parser->error('Unexpected end of Lock expression.', $prevToken);
}
return $lockType;
}
}

View File

@@ -0,0 +1,381 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use PhpMyAdmin\SqlParser\Translator;
use function array_merge_recursive;
use function count;
use function implode;
use function is_array;
use function ksort;
use function sprintf;
use function strcasecmp;
use function strtoupper;
/**
* Parses a list of options.
*
* @final
*/
class OptionsArray extends Component
{
/**
* ArrayObj of selected options.
*
* @var array<int, mixed>
*/
public $options = [];
/**
* @param array<int, mixed> $options The array of options. Options that have a value
* must be an array with at least two keys `name` and
* `expr` or `value`.
*/
public function __construct(array $options = [])
{
$this->options = $options;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return OptionsArray
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* The ID that will be assigned to duplicate options.
*
* @var int
*/
$lastAssignedId = count($options) + 1;
/**
* The option that was processed last time.
*/
$lastOption = null;
/**
* The index of the option that was processed last time.
*
* @var int
*/
$lastOptionId = 0;
/**
* Counts brackets.
*
* @var int
*/
$brackets = 0;
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------[ option ]----------------------> 1
*
* 1 -------------------[ = (optional) ]------------------> 2
*
* 2 ----------------------[ value ]----------------------> 0
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping comments.
if ($token->type === Token::TYPE_COMMENT) {
continue;
}
// Skipping whitespace if not parsing value.
if (($token->type === Token::TYPE_WHITESPACE) && ($brackets === 0)) {
continue;
}
if ($lastOption === null) {
$upper = strtoupper($token->token);
if (! isset($options[$upper])) {
// There is no option to be processed.
break;
}
$lastOption = $options[$upper];
$lastOptionId = is_array($lastOption) ?
$lastOption[0] : $lastOption;
$state = 0;
// Checking for option conflicts.
// For example, in `SELECT` statements the keywords `ALL`
// and `DISTINCT` conflict and if used together, they
// produce an invalid query.
//
// Usually, tokens can be identified in the array by the
// option ID, but if conflicts occur, a generated option ID
// is used.
//
// The first pseudo duplicate ID is the maximum value of the
// real options (e.g. if there are 5 options, the first
// fake ID is 6).
if (isset($ret->options[$lastOptionId])) {
$parser->error(
sprintf(
Translator::gettext('This option conflicts with "%1$s".'),
is_array($ret->options[$lastOptionId])
? $ret->options[$lastOptionId]['name']
: $ret->options[$lastOptionId]
),
$token
);
$lastOptionId = $lastAssignedId++;
}
}
if ($state === 0) {
if (! is_array($lastOption)) {
// This is a just keyword option without any value.
// This is the beginning and the end of it.
$ret->options[$lastOptionId] = $token->value;
$lastOption = null;
$state = 0;
} elseif (($lastOption[1] === 'var') || ($lastOption[1] === 'var=')) {
// This is a keyword that is followed by a value.
// This is only the beginning. The value is parsed in state
// 1 and 2. State 1 is used to skip the first equals sign
// and state 2 to parse the actual value.
$ret->options[$lastOptionId] = [
// @var string The name of the option.
'name' => $token->value,
// @var bool Whether it contains an equal sign.
// This is used by the builder to rebuild it.
'equals' => $lastOption[1] === 'var=',
// @var string Raw value.
'expr' => '',
// @var string Processed value.
'value' => '',
];
$state = 1;
} elseif ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') {
// This is a keyword that is followed by an expression.
// The expression is used by the specialized parser.
// Skipping this option in order to parse the expression.
++$list->idx;
$ret->options[$lastOptionId] = [
// @var string The name of the option.
'name' => $token->value,
// @var bool Whether it contains an equal sign.
// This is used by the builder to rebuild it.
'equals' => $lastOption[1] === 'expr=',
// @var Expression The parsed expression.
'expr' => '',
];
$state = 1;
}
} elseif ($state === 1) {
$state = 2;
if ($token->token === '=') {
$ret->options[$lastOptionId]['equals'] = true;
continue;
}
}
// This is outside the `elseif` group above because the change might
// change this iteration.
if ($state !== 2) {
continue;
}
if ($lastOption[1] === 'expr' || $lastOption[1] === 'expr=') {
$ret->options[$lastOptionId]['expr'] = Expression::parse(
$parser,
$list,
empty($lastOption[2]) ? [] : $lastOption[2]
);
if ($ret->options[$lastOptionId]['expr'] !== null) {
$ret->options[$lastOptionId]['value']
= $ret->options[$lastOptionId]['expr']->expr;
}
$lastOption = null;
$state = 0;
} else {
if ($token->token === '(') {
++$brackets;
} elseif ($token->token === ')') {
--$brackets;
}
$ret->options[$lastOptionId]['expr'] .= $token->token;
if (
! (($token->token === '(') && ($brackets === 1)
|| (($token->token === ')') && ($brackets === 0)))
) {
// First pair of brackets is being skipped.
$ret->options[$lastOptionId]['value'] .= $token->value;
}
// Checking if we finished parsing.
if ($brackets === 0) {
$lastOption = null;
}
}
}
/*
* We reached the end of statement without getting a value
* for an option for which a value was required
*/
if (
$state === 1
&& $lastOption
&& ($lastOption[1] === 'expr'
|| $lastOption[1] === 'var'
|| $lastOption[1] === 'var='
|| $lastOption[1] === 'expr=')
) {
$parser->error(
sprintf(
'Value/Expression for the option %1$s was expected.',
$ret->options[$lastOptionId]['name']
),
$list->tokens[$list->idx - 1]
);
}
if (empty($options['_UNSORTED'])) {
ksort($ret->options);
}
--$list->idx;
return $ret;
}
/**
* @param OptionsArray $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (empty($component->options)) {
return '';
}
$options = [];
foreach ($component->options as $option) {
if (! is_array($option)) {
$options[] = $option;
} else {
$options[] = $option['name']
. (! empty($option['equals']) && $option['equals'] ? '=' : ' ')
. (! empty($option['expr']) ? $option['expr'] : $option['value']);
}
}
return implode(' ', $options);
}
/**
* Checks if it has the specified option and returns it value or true.
*
* @param string $key the key to be checked
* @param bool $getExpr Gets the expression instead of the value.
* The value is the processed form of the expression.
*
* @return mixed
*/
public function has($key, $getExpr = false)
{
foreach ($this->options as $option) {
if (is_array($option)) {
if (! strcasecmp($key, $option['name'])) {
return $getExpr ? $option['expr'] : $option['value'];
}
} elseif (! strcasecmp($key, $option)) {
return true;
}
}
return false;
}
/**
* Removes the option from the array.
*
* @param string $key the key to be removed
*
* @return bool whether the key was found and deleted or not
*/
public function remove($key)
{
foreach ($this->options as $idx => $option) {
if (is_array($option)) {
if (! strcasecmp($key, $option['name'])) {
unset($this->options[$idx]);
return true;
}
} elseif (! strcasecmp($key, $option)) {
unset($this->options[$idx]);
return true;
}
}
return false;
}
/**
* Merges the specified options with these ones. Values with same ID will be
* replaced.
*
* @param array<int, mixed>|OptionsArray $options the options to be merged
*
* @return void
*/
public function merge($options)
{
if (is_array($options)) {
$this->options = array_merge_recursive($this->options, $options);
} elseif ($options instanceof self) {
$this->options = array_merge_recursive($this->options, $options->options);
}
}
/**
* Checks tf there are no options set.
*
* @return bool
*/
public function isEmpty()
{
return empty($this->options);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
/**
* `ORDER BY` keyword parser.
*
* @final
*/
class OrderKeyword extends Component
{
/**
* The expression that is used for ordering.
*
* @var Expression
*/
public $expr;
/**
* The order type.
*
* @var string
*/
public $type;
/**
* @param Expression $expr the expression that we are sorting by
* @param string $type the sorting type
*/
public function __construct($expr = null, $type = 'ASC')
{
$this->expr = $expr;
$this->type = $type;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return OrderKeyword[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 --------------------[ expression ]-------------------> 1
*
* 1 ------------------------[ , ]------------------------> 0
* 1 -------------------[ ASC / DESC ]--------------------> 1
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$expr->expr = Expression::parse($parser, $list);
$state = 1;
} elseif ($state === 1) {
if (
($token->type === Token::TYPE_KEYWORD)
&& (($token->keyword === 'ASC') || ($token->keyword === 'DESC'))
) {
$expr->type = $token->keyword;
} elseif (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) {
if (! empty($expr->expr)) {
$ret[] = $expr;
}
$expr = new static();
$state = 0;
} else {
break;
}
}
}
// Last iteration was not processed.
if (! empty($expr->expr)) {
$ret[] = $expr;
}
--$list->idx;
return $ret;
}
/**
* @param OrderKeyword|OrderKeyword[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(', ', $component);
}
return $component->expr . ' ' . $component->type;
}
}

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Context;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
use function trim;
/**
* The definition of a parameter of a function or procedure.
*
* @final
*/
class ParameterDefinition extends Component
{
/**
* The name of the new column.
*
* @var string
*/
public $name;
/**
* Parameter's direction (IN, OUT or INOUT).
*
* @var string
*/
public $inOut;
/**
* The data type of thew new column.
*
* @var DataType
*/
public $type;
/**
* @param string $name parameter's name
* @param string $inOut parameter's directional type (IN / OUT or None)
* @param DataType $type parameter's type
*/
public function __construct($name = null, $inOut = null, $type = null)
{
$this->name = $name;
$this->inOut = $inOut;
$this->type = $type;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return ParameterDefinition[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -----------------------[ ( ]------------------------> 1
*
* 1 ----------------[ IN / OUT / INOUT ]----------------> 1
* 1 ----------------------[ name ]----------------------> 2
*
* 2 -------------------[ data type ]--------------------> 3
*
* 3 ------------------------[ , ]-----------------------> 1
* 3 ------------------------[ ) ]-----------------------> (END)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
$state = 1;
}
continue;
} elseif ($state === 1) {
if (($token->value === 'IN') || ($token->value === 'OUT') || ($token->value === 'INOUT')) {
$expr->inOut = $token->value;
++$list->idx;
} elseif ($token->value === ')') {
++$list->idx;
break;
} else {
$expr->name = $token->value;
$state = 2;
}
} elseif ($state === 2) {
$expr->type = DataType::parse($parser, $list);
$state = 3;
} elseif ($state === 3) {
$ret[] = $expr;
$expr = new static();
if ($token->value === ',') {
$state = 1;
} elseif ($token->value === ')') {
++$list->idx;
break;
}
}
}
// Last iteration was not saved.
if (isset($expr->name) && ($expr->name !== '')) {
$ret[] = $expr;
}
--$list->idx;
return $ret;
}
/**
* @param ParameterDefinition[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return '(' . implode(', ', $component) . ')';
}
$tmp = '';
if (! empty($component->inOut)) {
$tmp .= $component->inOut . ' ';
}
return trim(
$tmp . Context::escape($component->name) . ' ' . $component->type
);
}
}

View File

@@ -0,0 +1,253 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
use function trim;
/**
* Parses the create definition of a partition.
*
* Used for parsing `CREATE TABLE` statement.
*
* @final
*/
class PartitionDefinition extends Component
{
/**
* All field options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'STORAGE ENGINE' => [
1,
'var',
],
'ENGINE' => [
1,
'var',
],
'COMMENT' => [
2,
'var',
],
'DATA DIRECTORY' => [
3,
'var',
],
'INDEX DIRECTORY' => [
4,
'var',
],
'MAX_ROWS' => [
5,
'var',
],
'MIN_ROWS' => [
6,
'var',
],
'TABLESPACE' => [
7,
'var',
],
'NODEGROUP' => [
8,
'var',
],
];
/**
* Whether this entry is a subpartition or a partition.
*
* @var bool
*/
public $isSubpartition;
/**
* The name of this partition.
*
* @var string
*/
public $name;
/**
* The type of this partition (what follows the `VALUES` keyword).
*
* @var string
*/
public $type;
/**
* The expression used to defined this partition.
*
* @var Expression|string
*/
public $expr;
/**
* The subpartitions of this partition.
*
* @var PartitionDefinition[]
*/
public $subpartitions;
/**
* The options of this field.
*
* @var OptionsArray
*/
public $options;
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return PartitionDefinition
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -------------[ PARTITION | SUBPARTITION ]------------> 1
*
* 1 -----------------------[ name ]----------------------> 2
*
* 2 ----------------------[ VALUES ]---------------------> 3
*
* 3 ---------------------[ LESS THAN ]-------------------> 4
* 3 ------------------------[ IN ]-----------------------> 4
*
* 4 -----------------------[ expr ]----------------------> 5
*
* 5 ----------------------[ options ]--------------------> 6
*
* 6 ------------------[ subpartitions ]------------------> (END)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$ret->isSubpartition = ($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'SUBPARTITION');
$state = 1;
} elseif ($state === 1) {
$ret->name = $token->value;
// Looking ahead for a 'VALUES' keyword.
// Loop until the end of the partition name (delimited by a whitespace)
while ($nextToken = $list->tokens[++$list->idx]) {
if ($nextToken->type !== Token::TYPE_NONE) {
break;
}
$ret->name .= $nextToken->value;
}
$idx = $list->idx--;
// Get the first token after the white space.
$nextToken = $list->tokens[++$idx];
$state = ($nextToken->type === Token::TYPE_KEYWORD)
&& ($nextToken->value === 'VALUES')
? 2 : 5;
} elseif ($state === 2) {
$state = 3;
} elseif ($state === 3) {
$ret->type = $token->value;
$state = 4;
} elseif ($state === 4) {
if ($token->value === 'MAXVALUE') {
$ret->expr = $token->value;
} else {
$ret->expr = Expression::parse(
$parser,
$list,
[
'parenthesesDelimited' => true,
'breakOnAlias' => true,
]
);
}
$state = 5;
} elseif ($state === 5) {
$ret->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
$state = 6;
} elseif ($state === 6) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
$ret->subpartitions = ArrayObj::parse(
$parser,
$list,
['type' => 'PhpMyAdmin\\SqlParser\\Components\\PartitionDefinition']
);
++$list->idx;
}
break;
}
}
--$list->idx;
return $ret;
}
/**
* @param PartitionDefinition|PartitionDefinition[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return "(\n" . implode(",\n", $component) . "\n)";
}
if ($component->isSubpartition) {
return trim('SUBPARTITION ' . $component->name . ' ' . $component->options);
}
$subpartitions = empty($component->subpartitions) ? '' : ' ' . self::build($component->subpartitions);
return trim(
'PARTITION ' . $component->name
. (empty($component->type) ? '' : ' VALUES ' . $component->type . ' ' . $component->expr . ' ')
. (! empty($component->options) && ! empty($component->type) ? '' : ' ')
. $component->options . $subpartitions
);
}
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Context;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function trim;
/**
* `REFERENCES` keyword parser.
*
* @final
*/
class Reference extends Component
{
/**
* All references options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $REFERENCES_OPTIONS = [
'MATCH' => [
1,
'var',
],
'ON DELETE' => [
2,
'var',
],
'ON UPDATE' => [
3,
'var',
],
];
/**
* The referenced table.
*
* @var Expression
*/
public $table;
/**
* The referenced columns.
*
* @var string[]
*/
public $columns;
/**
* The options of the referencing.
*
* @var OptionsArray
*/
public $options;
/**
* @param Expression $table the name of the table referenced
* @param string[] $columns the columns referenced
* @param OptionsArray $options the options
*/
public function __construct($table = null, array $columns = [], $options = null)
{
$this->table = $table;
$this->columns = $columns;
$this->options = $options;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return Reference
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ----------------------[ table ]---------------------> 1
*
* 1 ---------------------[ columns ]--------------------> 2
*
* 2 ---------------------[ options ]--------------------> (END)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$ret->table = Expression::parse(
$parser,
$list,
[
'parseField' => 'table',
'breakOnAlias' => true,
]
);
$state = 1;
} elseif ($state === 1) {
$ret->columns = ArrayObj::parse($parser, $list)->values;
$state = 2;
} elseif ($state === 2) {
$ret->options = OptionsArray::parse($parser, $list, static::$REFERENCES_OPTIONS);
++$list->idx;
break;
}
}
--$list->idx;
return $ret;
}
/**
* @param Reference $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
return trim(
$component->table
. ' (' . implode(', ', Context::escape($component->columns)) . ') '
. $component->options
);
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
/**
* `RENAME TABLE` keyword parser.
*
* @final
*/
class RenameOperation extends Component
{
/**
* The old table name.
*
* @var Expression
*/
public $old;
/**
* The new table name.
*
* @var Expression
*/
public $new;
/**
* @param Expression $old old expression
* @param Expression $new new expression containing new name
*/
public function __construct($old = null, $new = null)
{
$this->old = $old;
$this->new = $new;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return RenameOperation[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------[ old name ]--------------------> 1
*
* 1 ------------------------[ TO ]-----------------------> 2
*
* 2 ---------------------[ new name ]--------------------> 3
*
* 3 ------------------------[ , ]------------------------> 0
* 3 -----------------------[ else ]----------------------> (END)
*
* @var int
*/
$state = 0;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$expr->old = Expression::parse(
$parser,
$list,
[
'breakOnAlias' => true,
'parseField' => 'table',
]
);
if (empty($expr->old)) {
$parser->error('The old name of the table was expected.', $token);
}
$state = 1;
} elseif ($state === 1) {
if ($token->type !== Token::TYPE_KEYWORD || $token->keyword !== 'TO') {
$parser->error('Keyword "TO" was expected.', $token);
break;
}
$state = 2;
} elseif ($state === 2) {
$expr->new = Expression::parse(
$parser,
$list,
[
'breakOnAlias' => true,
'parseField' => 'table',
]
);
if (empty($expr->new)) {
$parser->error('The new name of the table was expected.', $token);
}
$state = 3;
} elseif ($state === 3) {
if (($token->type !== Token::TYPE_OPERATOR) || ($token->value !== ',')) {
break;
}
$ret[] = $expr;
$expr = new static();
$state = 0;
}
}
if ($state !== 3) {
$parser->error('A rename operation was expected.', $list->tokens[$list->idx - 1]);
}
// Last iteration was not saved.
if (! empty($expr->old)) {
$ret[] = $expr;
}
--$list->idx;
return $ret;
}
/**
* @param RenameOperation $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(', ', $component);
}
return $component->old . ' TO ' . $component->new;
}
}

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
use function is_array;
use function trim;
/**
* `SET` keyword parser.
*
* @final
*/
class SetOperation extends Component
{
/**
* The name of the column that is being updated.
*
* @var string
*/
public $column;
/**
* The new value.
*
* @var string
*/
public $value;
/**
* @param string $column Field's name..
* @param string $value new value
*/
public function __construct($column = '', $value = '')
{
$this->column = $column;
$this->value = $value;
}
/**
* @param Parser $parser the parser that serves as context
* @param TokensList $list the list of tokens that are being parsed
* @param array<string, mixed> $options parameters for parsing
*
* @return SetOperation[]
*/
public static function parse(Parser $parser, TokensList $list, array $options = [])
{
$ret = [];
$expr = new static();
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------[ col_name ]--------------------> 0
* 0 ------------------------[ = ]------------------------> 1
* 1 -----------------------[ value ]---------------------> 1
* 1 ------------------------[ , ]------------------------> 0
*
* @var int
*/
$state = 0;
/**
* Token when the parser has seen the latest comma
*
* @var Token
*/
$commaLastSeenAt = null;
for (; $list->idx < $list->count; ++$list->idx) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
// End of statement.
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
// No keyword is expected.
if (
($token->type === Token::TYPE_KEYWORD)
&& ($token->flags & Token::FLAG_KEYWORD_RESERVED)
&& ($state === 0)
) {
break;
}
if ($state === 0) {
if ($token->token === '=') {
$state = 1;
} elseif ($token->value !== ',') {
$expr->column .= $token->token;
} elseif ($token->value === ',') {
$commaLastSeenAt = $token;
}
} elseif ($state === 1) {
$tmp = Expression::parse(
$parser,
$list,
['breakOnAlias' => true]
);
if ($tmp === null) {
$parser->error('Missing expression.', $token);
break;
}
$expr->column = trim($expr->column);
$expr->value = $tmp->expr;
$ret[] = $expr;
$expr = new static();
$state = 0;
$commaLastSeenAt = null;
}
}
--$list->idx;
// We saw a comma, but didn't see a column-value pair after it
if ($commaLastSeenAt !== null) {
$parser->error('Unexpected token.', $commaLastSeenAt);
}
return $ret;
}
/**
* @param SetOperation|SetOperation[] $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
if (is_array($component)) {
return implode(', ', $component);
}
return $component->column . ' = ' . $component->value;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use function implode;
/**
* `UNION` keyword builder.
*
* @final
*/
class UnionKeyword extends Component
{
/**
* @param array<UnionKeyword[]> $component the component to be built
* @param array<string, mixed> $options parameters for building
*
* @return string
*/
public static function build($component, array $options = [])
{
$tmp = [];
foreach ($component as $componentPart) {
$tmp[] = $componentPart[0] . ' ' . $componentPart[1];
}
return implode(' ', $tmp);
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Components;
use PhpMyAdmin\SqlParser\Component;
use PhpMyAdmin\SqlParser\Parser;
use RuntimeException;
/**
* `WITH` keyword builder.
*
* @final
*/
final class WithKeyword extends Component
{
/** @var string */
public $name;
/** @var ArrayObj[] */
public $columns = [];
/** @var Parser|null */
public $statement;
public function __construct(string $name)
{
$this->name = $name;
}
/**
* @param WithKeyword $component
* @param array<string, mixed> $options
*
* @return string
*/
public static function build($component, array $options = [])
{
if (! $component instanceof WithKeyword) {
throw new RuntimeException('Can not build a component that is not a WithKeyword');
}
if (! isset($component->statement)) {
throw new RuntimeException('No statement inside WITH');
}
$str = $component->name;
if ($component->columns) {
$str .= ArrayObj::build($component->columns);
}
$str .= ' AS (';
foreach ($component->statement->statements as $statement) {
$str .= $statement->build();
}
$str .= ')';
return $str;
}
}