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,146 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\AlterOperation;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function implode;
/**
* `ALTER` statement.
*/
class AlterStatement extends Statement
{
/**
* Table affected.
*
* @var Expression|null
*/
public $table;
/**
* Column affected by this statement.
*
* @var AlterOperation[]|null
*/
public $altered = [];
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'ONLINE' => 1,
'OFFLINE' => 1,
'IGNORE' => 2,
'DATABASE' => 3,
'EVENT' => 3,
'FUNCTION' => 3,
'PROCEDURE' => 3,
'SERVER' => 3,
'TABLE' => 3,
'TABLESPACE' => 3,
'USER' => 3,
'VIEW' => 3,
];
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
++$list->idx; // Skipping `ALTER`.
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx;
// Parsing affected table.
$this->table = Expression::parse(
$parser,
$list,
[
'parseField' => 'table',
'breakOnAlias' => true,
]
);
++$list->idx; // Skipping field.
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -----------------[ alter operation ]-----------------> 1
*
* 1 -------------------------[ , ]-----------------------> 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 whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
if ($state === 0) {
$options = [];
if ($this->options->has('DATABASE')) {
$options = AlterOperation::$DB_OPTIONS;
} elseif ($this->options->has('TABLE')) {
$options = AlterOperation::$TABLE_OPTIONS;
} elseif ($this->options->has('VIEW')) {
$options = AlterOperation::$VIEW_OPTIONS;
} elseif ($this->options->has('USER')) {
$options = AlterOperation::$USER_OPTIONS;
} elseif ($this->options->has('EVENT')) {
$options = AlterOperation::$EVENT_OPTIONS;
}
$this->altered[] = AlterOperation::parse($parser, $list, $options);
$state = 1;
} elseif ($state === 1) {
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === ',')) {
$state = 0;
}
}
}
}
/**
* @return string
*/
public function build()
{
$tmp = [];
foreach ($this->altered as $altered) {
$tmp[] = $altered::build($altered);
}
return 'ALTER ' . OptionsArray::build($this->options)
. ' ' . Expression::build($this->table)
. ' ' . implode(', ', $tmp);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Statement;
/**
* `ANALYZE` statement.
*
* ANALYZE [NO_WRITE_TO_BINLOG | LOCAL] TABLE
* tbl_name [, tbl_name] ...
*/
class AnalyzeStatement extends Statement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'TABLE' => 1,
'NO_WRITE_TO_BINLOG' => 2,
'LOCAL' => 3,
];
/**
* Analyzed tables.
*
* @var Expression[]|null
*/
public $tables;
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
/**
* `BACKUP` statement.
*
* BACKUP TABLE tbl_name [, tbl_name] ... TO '/path/to/backup/directory'
*/
class BackupStatement extends MaintenanceStatement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'TABLE' => 1,
'NO_WRITE_TO_BINLOG' => 2,
'LOCAL' => 3,
'TO' => [
4,
'var',
],
];
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\FunctionCall;
use PhpMyAdmin\SqlParser\Statement;
use function implode;
/**
* `CALL` statement.
*
* CALL sp_name([parameter[,...]])
*
* or
*
* CALL sp_name[()]
*/
class CallStatement extends Statement
{
/**
* The name of the function and its parameters.
*
* @var FunctionCall|null
*/
public $call;
/**
* Build statement for CALL.
*
* @return string
*/
public function build()
{
return 'CALL ' . $this->call->name . '('
. ($this->call->parameters ? implode(',', $this->call->parameters->raw) : '') . ')';
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
/**
* `CHECK` statement.
*
* CHECK TABLE tbl_name [, tbl_name] ... [option] ...
*/
class CheckStatement extends MaintenanceStatement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'TABLE' => 1,
'FOR UPGRADE' => 2,
'QUICK' => 3,
'FAST' => 4,
'MEDIUM' => 5,
'EXTENDED' => 6,
'CHANGED' => 7,
];
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
/**
* `CHECKSUM` statement.
*
* CHECKSUM TABLE tbl_name [, tbl_name] ... [ QUICK | EXTENDED ]
*/
class ChecksumStatement extends MaintenanceStatement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'TABLE' => 1,
'QUICK' => 2,
'EXTENDED' => 3,
];
}

View File

@@ -0,0 +1,797 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\ArrayObj;
use PhpMyAdmin\SqlParser\Components\CreateDefinition;
use PhpMyAdmin\SqlParser\Components\DataType;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\ParameterDefinition;
use PhpMyAdmin\SqlParser\Components\PartitionDefinition;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function is_array;
use function trim;
/**
* `CREATE` statement.
*/
class CreateStatement extends Statement
{
/**
* Options for `CREATE` statements.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
// CREATE TABLE
'TEMPORARY' => 1,
// CREATE VIEW
'OR REPLACE' => 2,
'ALGORITHM' => [
3,
'var=',
],
// `DEFINER` is also used for `CREATE FUNCTION / PROCEDURE`
'DEFINER' => [
4,
'expr=',
],
// Used in `CREATE VIEW`
'SQL SECURITY' => [
5,
'var',
],
'DATABASE' => 6,
'EVENT' => 6,
'FUNCTION' => 6,
'INDEX' => 6,
'UNIQUE INDEX' => 6,
'FULLTEXT INDEX' => 6,
'SPATIAL INDEX' => 6,
'PROCEDURE' => 6,
'SERVER' => 6,
'TABLE' => 6,
'TABLESPACE' => 6,
'TRIGGER' => 6,
'USER' => 6,
'VIEW' => 6,
'SCHEMA' => 6,
// CREATE TABLE
'IF NOT EXISTS' => 7,
];
/**
* 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=',
],
'DEFAULT COLLATE' => [
2,
'var=',
],
'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' => [
2,
'var=',
],
'AVG_ROW_LENGTH' => [
3,
'var',
],
'CHARACTER SET' => [
4,
'var=',
],
'CHARSET' => [
4,
'var=',
],
'DEFAULT CHARACTER SET' => [
4,
'var=',
],
'DEFAULT CHARSET' => [
4,
'var=',
],
'CHECKSUM' => [
5,
'var',
],
'DEFAULT COLLATE' => [
6,
'var=',
],
'COLLATE' => [
6,
'var=',
],
'COMMENT' => [
7,
'var=',
],
'CONNECTION' => [
8,
'var',
],
'DATA DIRECTORY' => [
9,
'var',
],
'DELAY_KEY_WRITE' => [
10,
'var',
],
'INDEX DIRECTORY' => [
11,
'var',
],
'INSERT_METHOD' => [
12,
'var',
],
'KEY_BLOCK_SIZE' => [
13,
'var',
],
'MAX_ROWS' => [
14,
'var',
],
'MIN_ROWS' => [
15,
'var',
],
'PACK_KEYS' => [
16,
'var',
],
'PASSWORD' => [
17,
'var',
],
'ROW_FORMAT' => [
18,
'var',
],
'TABLESPACE' => [
19,
'var',
],
'STORAGE' => [
20,
'var',
],
'UNION' => [
21,
'var',
],
'PAGE_COMPRESSED' => [
22,
'var',
],
'PAGE_COMPRESSION_LEVEL' => [
23,
'var',
],
];
/**
* All function options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $FUNC_OPTIONS = [
'NOT' => [
2,
'var',
],
'FUNCTION' => [
3,
'var=',
],
'PROCEDURE' => [
3,
'var=',
],
'CONTAINS SQL' => 4,
'NO SQL' => 4,
'READS SQL DATA' => 4,
'MODIFIES SQL DATA' => 4,
'SQL SECURITY' => [
6,
'var',
],
'LANGUAGE' => [
7,
'var',
],
'COMMENT' => [
8,
'var',
],
'CREATE' => 1,
'DETERMINISTIC' => 2,
];
/**
* All trigger options.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $TRIGGER_OPTIONS = [
'BEFORE' => 1,
'AFTER' => 1,
'INSERT' => 2,
'UPDATE' => 2,
'DELETE' => 2,
];
/**
* The name of the entity that is created.
*
* Used by all `CREATE` statements.
*
* @var Expression|null
*/
public $name;
/**
* The options of the entity (table, procedure, function, etc.).
*
* Used by `CREATE TABLE`, `CREATE FUNCTION` and `CREATE PROCEDURE`.
*
* @see static::$TABLE_OPTIONS
* @see static::$FUNC_OPTIONS
* @see static::$TRIGGER_OPTIONS
*
* @var OptionsArray|null
*/
public $entityOptions;
/**
* If `CREATE TABLE`, a list of columns and keys.
* If `CREATE VIEW`, a list of columns.
*
* Used by `CREATE TABLE` and `CREATE VIEW`.
*
* @var CreateDefinition[]|ArrayObj|null
*/
public $fields;
/**
* If `CREATE TABLE WITH`.
* If `CREATE TABLE AS WITH`.
* If `CREATE VIEW AS WITH`.
*
* Used by `CREATE TABLE`, `CREATE VIEW`
*
* @var WithStatement|null
*/
public $with;
/**
* If `CREATE TABLE ... SELECT`.
* If `CREATE VIEW AS ` ... SELECT`.
*
* Used by `CREATE TABLE`, `CREATE VIEW`
*
* @var SelectStatement|null
*/
public $select;
/**
* If `CREATE TABLE ... LIKE`.
*
* Used by `CREATE TABLE`
*
* @var Expression|null
*/
public $like;
/**
* Expression used for partitioning.
*
* @var string|null
*/
public $partitionBy;
/**
* The number of partitions.
*
* @var int|null
*/
public $partitionsNum;
/**
* Expression used for subpartitioning.
*
* @var string|null
*/
public $subpartitionBy;
/**
* The number of subpartitions.
*
* @var int|null
*/
public $subpartitionsNum;
/**
* The partition of the new table.
*
* @var PartitionDefinition[]|null
*/
public $partitions;
/**
* If `CREATE TRIGGER` the name of the table.
*
* Used by `CREATE TRIGGER`.
*
* @var Expression|null
*/
public $table;
/**
* The return data type of this routine.
*
* Used by `CREATE FUNCTION`.
*
* @var DataType|null
*/
public $return;
/**
* The parameters of this routine.
*
* Used by `CREATE FUNCTION` and `CREATE PROCEDURE`.
*
* @var ParameterDefinition[]|null
*/
public $parameters;
/**
* The body of this function or procedure.
* For views, it is the select statement that creates the view.
* Used by `CREATE FUNCTION`, `CREATE PROCEDURE` and `CREATE VIEW`.
*
* @var Token[]|string
*/
public $body = [];
/**
* @return string
*/
public function build()
{
$fields = '';
if (! empty($this->fields)) {
if (is_array($this->fields)) {
$fields = CreateDefinition::build($this->fields) . ' ';
} elseif ($this->fields instanceof ArrayObj) {
$fields = ArrayObj::build($this->fields);
}
}
if ($this->options->has('DATABASE') || $this->options->has('SCHEMA')) {
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. OptionsArray::build($this->entityOptions);
}
if ($this->options->has('TABLE')) {
if ($this->select !== null) {
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. $this->select->build();
}
if ($this->like !== null) {
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' LIKE '
. Expression::build($this->like);
}
if ($this->with !== null) {
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. $this->with->build();
}
$partition = '';
if (! empty($this->partitionBy)) {
$partition .= "\nPARTITION BY " . $this->partitionBy;
}
if (! empty($this->partitionsNum)) {
$partition .= "\nPARTITIONS " . $this->partitionsNum;
}
if (! empty($this->subpartitionBy)) {
$partition .= "\nSUBPARTITION BY " . $this->subpartitionBy;
}
if (! empty($this->subpartitionsNum)) {
$partition .= "\nSUBPARTITIONS " . $this->subpartitionsNum;
}
if (! empty($this->partitions)) {
$partition .= "\n" . PartitionDefinition::build($this->partitions);
}
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. $fields
. OptionsArray::build($this->entityOptions)
. $partition;
} elseif ($this->options->has('VIEW')) {
$builtStatement = '';
if ($this->select !== null) {
$builtStatement = $this->select->build();
} elseif ($this->with !== null) {
$builtStatement = $this->with->build();
}
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. $fields . ' AS ' . $builtStatement
. (! empty($this->body) ? TokensList::build($this->body) : '') . ' '
. OptionsArray::build($this->entityOptions);
} elseif ($this->options->has('TRIGGER')) {
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. OptionsArray::build($this->entityOptions) . ' '
. 'ON ' . Expression::build($this->table) . ' '
. 'FOR EACH ROW ' . TokensList::build($this->body);
} elseif ($this->options->has('PROCEDURE') || $this->options->has('FUNCTION')) {
$tmp = '';
if ($this->options->has('FUNCTION')) {
$tmp = 'RETURNS ' . DataType::build($this->return);
}
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. ParameterDefinition::build($this->parameters) . ' '
. $tmp . ' ' . OptionsArray::build($this->entityOptions) . ' '
. TokensList::build($this->body);
}
return 'CREATE '
. OptionsArray::build($this->options) . ' '
. Expression::build($this->name) . ' '
. TokensList::build($this->body);
}
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
++$list->idx; // Skipping `CREATE`.
// Parsing options.
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx; // Skipping last option.
$isDatabase = $this->options->has('DATABASE') || $this->options->has('SCHEMA');
$fieldName = $isDatabase ? 'database' : 'table';
// Parsing the field name.
$this->name = Expression::parse(
$parser,
$list,
[
'parseField' => $fieldName,
'breakOnAlias' => true,
]
);
if (! isset($this->name) || ($this->name === '')) {
$parser->error('The name of the entity was expected.', $list->tokens[$list->idx]);
} else {
++$list->idx; // Skipping field.
}
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
$nextidx = $list->idx + 1;
while ($nextidx < $list->count && $list->tokens[$nextidx]->type === Token::TYPE_WHITESPACE) {
++$nextidx;
}
if ($isDatabase) {
$this->entityOptions = OptionsArray::parse($parser, $list, static::$DB_OPTIONS);
} elseif ($this->options->has('TABLE')) {
if (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'SELECT')) {
/* CREATE TABLE ... SELECT */
$this->select = new SelectStatement($parser, $list);
} elseif ($token->type === Token::TYPE_KEYWORD && ($token->keyword === 'WITH')) {
/* CREATE TABLE WITH */
$this->with = new WithStatement($parser, $list);
} elseif (
($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'AS')
&& ($list->tokens[$nextidx]->type === Token::TYPE_KEYWORD)
) {
if ($list->tokens[$nextidx]->value === 'SELECT') {
/* CREATE TABLE ... AS SELECT */
$list->idx = $nextidx;
$this->select = new SelectStatement($parser, $list);
} elseif ($list->tokens[$nextidx]->value === 'WITH') {
/* CREATE TABLE WITH */
$list->idx = $nextidx;
$this->with = new WithStatement($parser, $list);
}
} elseif ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'LIKE') {
/* CREATE TABLE `new_tbl` LIKE 'orig_tbl' */
$list->idx = $nextidx;
$this->like = Expression::parse(
$parser,
$list,
[
'parseField' => 'table',
'breakOnAlias' => true,
]
);
// The 'LIKE' keyword was found, but no table_name was found next to it
if ($this->like === null) {
$parser->error('A table name was expected.', $list->tokens[$list->idx]);
}
} else {
$this->fields = CreateDefinition::parse($parser, $list);
if (empty($this->fields)) {
$parser->error('At least one column definition was expected.', $list->tokens[$list->idx]);
}
++$list->idx;
$this->entityOptions = OptionsArray::parse($parser, $list, static::$TABLE_OPTIONS);
/**
* The field that is being filled (`partitionBy` or
* `subpartitionBy`).
*
* @var string
*/
$field = null;
/**
* The number of brackets. `false` means no bracket was found
* previously. At least one bracket is required to validate the
* expression.
*
* @var int|bool
*/
$brackets = false;
/*
* Handles partitions.
*/
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;
}
if (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITION BY')) {
$field = 'partitionBy';
$brackets = false;
} elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'SUBPARTITION BY')) {
$field = 'subpartitionBy';
$brackets = false;
} elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITIONS')) {
$token = $list->getNextOfType(Token::TYPE_NUMBER);
--$list->idx; // `getNextOfType` also advances one position.
$this->partitionsNum = $token->value;
} elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'SUBPARTITIONS')) {
$token = $list->getNextOfType(Token::TYPE_NUMBER);
--$list->idx; // `getNextOfType` also advances one position.
$this->subpartitionsNum = $token->value;
} elseif (! empty($field)) {
/*
* Handling the content of `PARTITION BY` and `SUBPARTITION BY`.
*/
// Counting brackets.
if ($token->type === Token::TYPE_OPERATOR) {
if ($token->value === '(') {
// This is used instead of `++$brackets` because,
// initially, `$brackets` is `false` cannot be
// incremented.
$brackets += 1;
} elseif ($token->value === ')') {
--$brackets;
}
}
// Building the expression used for partitioning.
$this->$field .= $token->type === Token::TYPE_WHITESPACE ? ' ' : $token->token;
// Last bracket was read, the expression ended.
// Comparing with `0` and not `false`, because `false` means
// that no bracket was found and at least one must is
// required.
if ($brackets === 0) {
$this->$field = trim($this->$field);
$field = null;
}
} elseif (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
if (! empty($this->partitionBy)) {
$this->partitions = ArrayObj::parse(
$parser,
$list,
['type' => 'PhpMyAdmin\\SqlParser\\Components\\PartitionDefinition']
);
}
break;
}
}
}
} elseif ($this->options->has('PROCEDURE') || $this->options->has('FUNCTION')) {
$this->parameters = ParameterDefinition::parse($parser, $list);
if ($this->options->has('FUNCTION')) {
$prevToken = $token;
$token = $list->getNextOfType(Token::TYPE_KEYWORD);
if ($token === null || $token->keyword !== 'RETURNS') {
$parser->error('A "RETURNS" keyword was expected.', $token ?? $prevToken);
} else {
++$list->idx;
$this->return = DataType::parse($parser, $list);
}
}
++$list->idx;
$this->entityOptions = OptionsArray::parse($parser, $list, static::$FUNC_OPTIONS);
++$list->idx;
for (; $list->idx < $list->count; ++$list->idx) {
$token = $list->tokens[$list->idx];
$this->body[] = $token;
}
} elseif ($this->options->has('VIEW')) {
/** @var Token $token */
$token = $list->getNext(); // Skipping whitespaces and comments.
// Parsing columns list.
if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
--$list->idx; // getNext() also goes forward one field.
$this->fields = ArrayObj::parse($parser, $list);
++$list->idx; // Skipping last token from the array.
$list->getNext();
}
// Parsing the SELECT expression if the view started with it.
if (
$token->type === Token::TYPE_KEYWORD
&& $token->keyword === 'AS'
&& $list->tokens[$nextidx]->type === Token::TYPE_KEYWORD
) {
if ($list->tokens[$nextidx]->value === 'SELECT') {
$list->idx = $nextidx;
$this->select = new SelectStatement($parser, $list);
++$list->idx; // Skipping last token from the select.
} elseif ($list->tokens[$nextidx]->value === 'WITH') {
++$list->idx;
$this->with = new WithStatement($parser, $list);
}
}
// Parsing all other tokens
for (; $list->idx < $list->count; ++$list->idx) {
$token = $list->tokens[$list->idx];
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
$this->body[] = $token;
}
} elseif ($this->options->has('TRIGGER')) {
// Parsing the time and the event.
$this->entityOptions = OptionsArray::parse($parser, $list, static::$TRIGGER_OPTIONS);
++$list->idx;
$list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ON');
++$list->idx; // Skipping `ON`.
// Parsing the name of the table.
$this->table = Expression::parse(
$parser,
$list,
[
'parseField' => 'table',
'breakOnAlias' => true,
]
);
++$list->idx;
$list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'FOR EACH ROW');
++$list->idx; // Skipping `FOR EACH ROW`.
for (; $list->idx < $list->count; ++$list->idx) {
$token = $list->tokens[$list->idx];
$this->body[] = $token;
}
} else {
for (; $list->idx < $list->count; ++$list->idx) {
$token = $list->tokens[$list->idx];
if ($token->type === Token::TYPE_DELIMITER) {
break;
}
$this->body[] = $token;
}
}
}
}

View File

@@ -0,0 +1,374 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\ArrayObj;
use PhpMyAdmin\SqlParser\Components\Condition;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\ExpressionArray;
use PhpMyAdmin\SqlParser\Components\JoinKeyword;
use PhpMyAdmin\SqlParser\Components\Limit;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\OrderKeyword;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function count;
use function stripos;
use function strlen;
/**
* `DELETE` statement.
*
* DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM tbl_name
* [PARTITION (partition_name,...)]
* [WHERE where_condition]
* [ORDER BY ...]
* [LIMIT row_count]
*
* Multi-table syntax
*
* DELETE [LOW_PRIORITY] [QUICK] [IGNORE]
* tbl_name[.*] [, tbl_name[.*]] ...
* FROM table_references
* [WHERE where_condition]
*
* OR
*
* DELETE [LOW_PRIORITY] [QUICK] [IGNORE]
* FROM tbl_name[.*] [, tbl_name[.*]] ...
* USING table_references
* [WHERE where_condition]
*/
class DeleteStatement extends Statement
{
/**
* Options for `DELETE` statements.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'LOW_PRIORITY' => 1,
'QUICK' => 2,
'IGNORE' => 3,
];
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array<string, array<int, int|string>>
* @psalm-var array<string, array{non-empty-string, (1|2|3)}>
*/
public static $CLAUSES = [
'DELETE' => [
'DELETE',
2,
],
// Used for options.
'_OPTIONS' => [
'_OPTIONS',
1,
],
'FROM' => [
'FROM',
3,
],
'PARTITION' => [
'PARTITION',
3,
],
'USING' => [
'USING',
3,
],
'WHERE' => [
'WHERE',
3,
],
'ORDER BY' => [
'ORDER BY',
3,
],
'LIMIT' => [
'LIMIT',
3,
],
];
/**
* Table(s) used as sources for this statement.
*
* @var Expression[]|null
*/
public $from;
/**
* Joins.
*
* @var JoinKeyword[]|null
*/
public $join;
/**
* Tables used as sources for this statement.
*
* @var Expression[]|null
*/
public $using;
/**
* Columns used in this statement.
*
* @var Expression[]|null
*/
public $columns;
/**
* Partitions used as source for this statement.
*
* @var ArrayObj|null
*/
public $partition;
/**
* Conditions used for filtering each row of the result set.
*
* @var Condition[]|null
*/
public $where;
/**
* Specifies the order of the rows in the result set.
*
* @var OrderKeyword[]|null
*/
public $order;
/**
* Conditions used for limiting the size of the result set.
*
* @var Limit|null
*/
public $limit;
/**
* @return string
*/
public function build()
{
$ret = 'DELETE ' . OptionsArray::build($this->options);
if ($this->columns !== null && count($this->columns) > 0) {
$ret .= ' ' . ExpressionArray::build($this->columns);
}
if ($this->from !== null && count($this->from) > 0) {
$ret .= ' FROM ' . ExpressionArray::build($this->from);
}
if ($this->join !== null && count($this->join) > 0) {
$ret .= ' ' . JoinKeyword::build($this->join);
}
if ($this->using !== null && count($this->using) > 0) {
$ret .= ' USING ' . ExpressionArray::build($this->using);
}
if ($this->where !== null && count($this->where) > 0) {
$ret .= ' WHERE ' . Condition::build($this->where);
}
if ($this->order !== null && count($this->order) > 0) {
$ret .= ' ORDER BY ' . ExpressionArray::build($this->order);
}
if ($this->limit !== null && strlen((string) $this->limit) > 0) {
$ret .= ' LIMIT ' . Limit::build($this->limit);
}
return $ret;
}
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
++$list->idx; // Skipping `DELETE`.
// parse any options if provided
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx;
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------------------[ FROM ]----------------------------------> 2
* 0 ------------------------------[ table[.*] ]--------------------------------> 1
* 1 ---------------------------------[ FROM ]----------------------------------> 2
* 2 --------------------------------[ USING ]----------------------------------> 3
* 2 --------------------------------[ WHERE ]----------------------------------> 4
* 2 --------------------------------[ ORDER ]----------------------------------> 5
* 2 --------------------------------[ LIMIT ]----------------------------------> 6
*
* @var int
*/
$state = 0;
/**
* If the query is multi-table or not.
*
* @var bool
*/
$multiTable = 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;
}
if ($state === 0) {
if ($token->type === Token::TYPE_KEYWORD) {
if ($token->keyword !== 'FROM') {
$parser->error('Unexpected keyword.', $token);
break;
}
++$list->idx; // Skip 'FROM'
$this->from = ExpressionArray::parse($parser, $list);
$state = 2;
} else {
$this->columns = ExpressionArray::parse($parser, $list);
$state = 1;
}
} elseif ($state === 1) {
if ($token->type !== Token::TYPE_KEYWORD) {
$parser->error('Unexpected token.', $token);
break;
}
if ($token->keyword !== 'FROM') {
$parser->error('Unexpected keyword.', $token);
break;
}
++$list->idx; // Skip 'FROM'
$this->from = ExpressionArray::parse($parser, $list);
$state = 2;
} elseif ($state === 2) {
if ($token->type === Token::TYPE_KEYWORD) {
if (stripos($token->keyword, 'JOIN') !== false) {
++$list->idx;
$this->join = JoinKeyword::parse($parser, $list);
// remain in state = 2
} else {
switch ($token->keyword) {
case 'USING':
++$list->idx; // Skip 'USING'
$this->using = ExpressionArray::parse($parser, $list);
$state = 3;
$multiTable = true;
break;
case 'WHERE':
++$list->idx; // Skip 'WHERE'
$this->where = Condition::parse($parser, $list);
$state = 4;
break;
case 'ORDER BY':
++$list->idx; // Skip 'ORDER BY'
$this->order = OrderKeyword::parse($parser, $list);
$state = 5;
break;
case 'LIMIT':
++$list->idx; // Skip 'LIMIT'
$this->limit = Limit::parse($parser, $list);
$state = 6;
break;
default:
$parser->error('Unexpected keyword.', $token);
break 2;
}
}
}
} elseif ($state === 3) {
if ($token->type !== Token::TYPE_KEYWORD) {
$parser->error('Unexpected token.', $token);
break;
}
if ($token->keyword !== 'WHERE') {
$parser->error('Unexpected keyword.', $token);
break;
}
++$list->idx; // Skip 'WHERE'
$this->where = Condition::parse($parser, $list);
$state = 4;
} elseif ($state === 4) {
if ($multiTable === true && $token->type === Token::TYPE_KEYWORD) {
$parser->error('This type of clause is not valid in Multi-table queries.', $token);
break;
}
if ($token->type === Token::TYPE_KEYWORD) {
switch ($token->keyword) {
case 'ORDER BY':
++$list->idx; // Skip 'ORDER BY'
$this->order = OrderKeyword::parse($parser, $list);
$state = 5;
break;
case 'LIMIT':
++$list->idx; // Skip 'LIMIT'
$this->limit = Limit::parse($parser, $list);
$state = 6;
break;
default:
$parser->error('Unexpected keyword.', $token);
break 2;
}
}
} elseif ($state === 5) {
if ($token->type === Token::TYPE_KEYWORD) {
if ($token->keyword !== 'LIMIT') {
$parser->error('Unexpected keyword.', $token);
break;
}
++$list->idx; // Skip 'LIMIT'
$this->limit = Limit::parse($parser, $list);
$state = 6;
}
}
}
if ($state >= 2) {
foreach ($this->from as $fromExpr) {
$fromExpr->database = $fromExpr->table;
$fromExpr->table = $fromExpr->column;
$fromExpr->column = null;
}
}
--$list->idx;
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Statement;
/**
* `DROP` statement.
*/
class DropStatement extends Statement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'DATABASE' => 1,
'EVENT' => 1,
'FUNCTION' => 1,
'INDEX' => 1,
'LOGFILE' => 1,
'PROCEDURE' => 1,
'SCHEMA' => 1,
'SERVER' => 1,
'TABLE' => 1,
'VIEW' => 1,
'TABLESPACE' => 1,
'TRIGGER' => 1,
'USER' => 1,
'TEMPORARY' => 2,
'IF EXISTS' => 3,
];
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array<string, array<int, int|string>>
* @psalm-var array<string, array{non-empty-string, (1|2|3)}>
*/
public static $CLAUSES = [
'DROP' => [
'DROP',
2,
],
// Used for options.
'_OPTIONS' => [
'_OPTIONS',
1,
],
// Used for select expressions.
'DROP_' => [
'DROP',
1,
],
'ON' => [
'ON',
3,
],
];
/**
* Dropped elements.
*
* @var Expression[]|null
*/
public $fields;
/**
* Table of the dropped index.
*
* @var Expression|null
*/
public $table;
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function array_slice;
use function count;
/**
* `EXPLAIN` statement.
*/
class ExplainStatement extends Statement
{
/**
* Options for `EXPLAIN` statements.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'EXTENDED' => 1,
'PARTITIONS' => 1,
'FORMAT' => [
1,
'var',
],
];
/**
* The parser of the statement to be explained
*
* @var Parser|null
*/
public $bodyParser = null;
/**
* The statement alias, could be any of the following:
* - {EXPLAIN | DESCRIBE | DESC}
* - {EXPLAIN | DESCRIBE | DESC} ANALYZE
* - ANALYZE
*
* @var string
*/
public $statementAlias;
/**
* The connection identifier, if used.
*
* @var int|null
*/
public $connectionId = null;
/**
* The explained table's name, if used.
*
* @var string|null
*/
public $explainedTable = null;
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 -------------------[ EXPLAIN/EXPLAIN ANALYZE/ANALYZE ]-----------------------> 1
*
* 1 ------------------------------[ OPTIONS ]------------------------------------> 2
*
* 2 --------------[ tablename / STATEMENT / FOR CONNECTION ]---------------------> 2
*
* @var int
*/
$state = 0;
/**
* To Differentiate between ANALYZE / EXPLAIN / EXPLAIN ANALYZE
* 0 -> ANALYZE ( used by mariaDB https://mariadb.com/kb/en/analyze-statement)
* 1 -> {EXPLAIN | DESCRIBE | DESC}
* 2 -> {EXPLAIN | DESCRIBE | DESC} ANALYZE
*/
$miniState = 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) {
if ($token->keyword === 'ANALYZE' && $miniState === 0) {
$state = 1;
$this->statementAlias = 'ANALYZE';
} elseif (
$token->keyword === 'EXPLAIN'
|| $token->keyword === 'DESC'
|| $token->keyword === 'DESCRIBE'
) {
$miniState = 1;
$this->statementAlias = $token->keyword;
$lastIdx = $list->idx;
$nextKeyword = $list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ANALYZE');
if ($nextKeyword && $nextKeyword->keyword !== null) {
$miniState = 2;
$this->statementAlias .= ' ANALYZE';
} else {
$list->idx = $lastIdx;
}
$state = 1;
}
} elseif ($state === 1) {
// Parsing options.
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
$state = 2;
} elseif ($state === 2) {
$currIdx = $list->idx;
$list->idx++; // Ignore the current token
$nextToken = $list->getNext();
$list->idx = $currIdx;
if ($token->keyword === 'FOR' && $nextToken->keyword === 'CONNECTION') {
$list->idx++; // Ignore the current token
$list->getNext(); // CONNECTION
$nextToken = $list->getNext(); // Identifier
$this->connectionId = $nextToken->value;
break;
}
// To support EXPLAIN tablename
if ($token->type === Token::TYPE_NONE) {
$this->explainedTable = $token->value;
break;
}
if (
$token->keyword !== 'SELECT'
&& $token->keyword !== 'INSERT'
&& $token->keyword !== 'UPDATE'
&& $token->keyword !== 'DELETE'
) {
$parser->error('Unexpected token.', $token);
break;
}
// Index of the last parsed token by default would be the last token in the $list, because we're
// assuming that all remaining tokens at state 2, are related to the to-be-explained statement.
$idxOfLastParsedToken = $list->count - 1;
$subList = new TokensList(array_slice($list->tokens, $list->idx));
$this->bodyParser = new Parser($subList);
if (count($this->bodyParser->errors)) {
foreach ($this->bodyParser->errors as $error) {
$parser->errors[] = $error;
}
break;
}
$list->idx = $idxOfLastParsedToken;
break;
}
}
}
public function build(): string
{
$str = $this->statementAlias;
if (count($this->options->options)) {
$str .= ' ';
}
$str .= OptionsArray::build($this->options) . ' ';
if ($this->bodyParser) {
foreach ($this->bodyParser->statements as $statement) {
$str .= $statement->build();
}
} elseif ($this->connectionId) {
$str .= 'FOR CONNECTION ' . $this->connectionId;
} elseif ($this->explainedTable) {
$str .= $this->explainedTable;
}
return $str;
}
}

View File

@@ -0,0 +1,254 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Array2d;
use PhpMyAdmin\SqlParser\Components\ArrayObj;
use PhpMyAdmin\SqlParser\Components\IntoKeyword;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\SetOperation;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function count;
use function strlen;
use function trim;
/**
* `INSERT` statement.
*
* INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
* [INTO] tbl_name
* [PARTITION (partition_name,...)]
* [(col_name,...)]
* {VALUES | VALUE} ({expr | DEFAULT},...),(...),...
* [ ON DUPLICATE KEY UPDATE
* col_name=expr
* [, col_name=expr] ... ]
*
* or
*
* INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE]
* [INTO] tbl_name
* [PARTITION (partition_name,...)]
* SET col_name={expr | DEFAULT}, ...
* [ ON DUPLICATE KEY UPDATE
* col_name=expr
* [, col_name=expr] ... ]
*
* or
*
* INSERT [LOW_PRIORITY | HIGH_PRIORITY] [IGNORE]
* [INTO] tbl_name
* [PARTITION (partition_name,...)]
* [(col_name,...)]
* SELECT ...
* [ ON DUPLICATE KEY UPDATE
* col_name=expr
* [, col_name=expr] ... ]
*/
class InsertStatement extends Statement
{
/**
* Options for `INSERT` statements.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'LOW_PRIORITY' => 1,
'DELAYED' => 2,
'HIGH_PRIORITY' => 3,
'IGNORE' => 4,
];
/**
* Tables used as target for this statement.
*
* @var IntoKeyword|null
*/
public $into;
/**
* Values to be inserted.
*
* @var ArrayObj[]|null
*/
public $values;
/**
* If SET clause is present
* holds the SetOperation.
*
* @var SetOperation[]|null
*/
public $set;
/**
* If SELECT clause is present
* holds the SelectStatement.
*
* @var SelectStatement|null
*/
public $select;
/**
* If WITH CTE is present
* holds the WithStatement.
*
* @var WithStatement|null
*/
public $with;
/**
* If ON DUPLICATE KEY UPDATE clause is present
* holds the SetOperation.
*
* @var SetOperation[]|null
*/
public $onDuplicateSet;
/**
* @return string
*/
public function build()
{
$ret = 'INSERT ' . $this->options;
$ret = trim($ret) . ' INTO ' . $this->into;
if ($this->values !== null && count($this->values) > 0) {
$ret .= ' VALUES ' . Array2d::build($this->values);
} elseif ($this->set !== null && count($this->set) > 0) {
$ret .= ' SET ' . SetOperation::build($this->set);
} elseif ($this->select !== null && strlen((string) $this->select) > 0) {
$ret .= ' ' . $this->select->build();
}
if ($this->onDuplicateSet !== null && count($this->onDuplicateSet) > 0) {
$ret .= ' ON DUPLICATE KEY UPDATE ' . SetOperation::build($this->onDuplicateSet);
}
return $ret;
}
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
++$list->idx; // Skipping `INSERT`.
// parse any options if provided
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx;
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------------------[ INTO ]----------------------------------> 1
*
* 1 -------------------------[ VALUES/VALUE/SET/SELECT ]-----------------------> 2
*
* 2 -------------------------[ ON DUPLICATE KEY UPDATE ]-----------------------> 3
*
* @var int
*/
$state = 0;
/**
* For keeping track of semi-states on encountering
* ON DUPLICATE KEY UPDATE ...
*/
$miniState = 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_KEYWORD && $token->keyword !== 'INTO') {
$parser->error('Unexpected keyword.', $token);
break;
}
++$list->idx;
$this->into = IntoKeyword::parse(
$parser,
$list,
['fromInsert' => true]
);
$state = 1;
} elseif ($state === 1) {
if ($token->type !== Token::TYPE_KEYWORD) {
$parser->error('Unexpected token.', $token);
break;
}
if ($token->keyword === 'VALUE' || $token->keyword === 'VALUES') {
++$list->idx; // skip VALUES
$this->values = Array2d::parse($parser, $list);
} elseif ($token->keyword === 'SET') {
++$list->idx; // skip SET
$this->set = SetOperation::parse($parser, $list);
} elseif ($token->keyword === 'SELECT') {
$this->select = new SelectStatement($parser, $list);
} elseif ($token->keyword === 'WITH') {
$this->with = new WithStatement($parser, $list);
} else {
$parser->error('Unexpected keyword.', $token);
break;
}
$state = 2;
$miniState = 1;
} elseif ($state === 2) {
$lastCount = $miniState;
if ($miniState === 1 && $token->keyword === 'ON') {
++$miniState;
} elseif ($miniState === 2 && $token->keyword === 'DUPLICATE') {
++$miniState;
} elseif ($miniState === 3 && $token->keyword === 'KEY') {
++$miniState;
} elseif ($miniState === 4 && $token->keyword === 'UPDATE') {
++$miniState;
}
if ($lastCount === $miniState) {
$parser->error('Unexpected token.', $token);
break;
}
if ($miniState === 5) {
++$list->idx;
$this->onDuplicateSet = SetOperation::parse($parser, $list);
$state = 3;
}
}
}
--$list->idx;
}
}

View File

@@ -0,0 +1,410 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\ArrayObj;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\ExpressionArray;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\SetOperation;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function count;
use function strlen;
use function trim;
/**
* `LOAD` statement.
*
* LOAD DATA [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name'
* [REPLACE | IGNORE]
* INTO TABLE tbl_name
* [PARTITION (partition_name,...)]
* [CHARACTER SET charset_name]
* [{FIELDS | COLUMNS}
* [TERMINATED BY 'string']
* [[OPTIONALLY] ENCLOSED BY 'char']
* [ESCAPED BY 'char']
* ]
* [LINES
* [STARTING BY 'string']
* [TERMINATED BY 'string']
* ]
* [IGNORE number {LINES | ROWS}]
* [(col_name_or_user_var,...)]
* [SET col_name = expr,...]
*/
class LoadStatement extends Statement
{
/**
* Options for `LOAD` statements and their slot ID.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'LOW_PRIORITY' => 1,
'CONCURRENT' => 1,
'LOCAL' => 2,
];
/**
* FIELDS/COLUMNS Options for `LOAD DATA...INFILE` 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 `LOAD DATA...INFILE` 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',
],
];
/**
* File name being used to load data.
*
* @var Expression|null
*/
public $file_name;
/**
* Table used as destination for this statement.
*
* @var Expression|null
*/
public $table;
/**
* Partitions used as source for this statement.
*
* @var ArrayObj|null
*/
public $partition;
/**
* Character set used in this statement.
*
* @var Expression|null
*/
public $charset_name;
/**
* Options for FIELDS/COLUMNS keyword.
*
* @see static::$FIELDS_OPTIONS
*
* @var OptionsArray|null
*/
public $fields_options;
/**
* Whether to use `FIELDS` or `COLUMNS` while building.
*
* @var string|null
*/
public $fields_keyword;
/**
* Options for OPTIONS keyword.
*
* @see static::$LINES_OPTIONS
*
* @var OptionsArray|null
*/
public $lines_options;
/**
* Column names or user variables.
*
* @var Expression[]|null
*/
public $col_name_or_user_var;
/**
* SET clause's updated values(optional).
*
* @var SetOperation[]|null
*/
public $set;
/**
* Ignore 'number' LINES/ROWS.
*
* @var Expression|null
*/
public $ignore_number;
/**
* REPLACE/IGNORE Keyword.
*
* @var string|null
*/
public $replace_ignore;
/**
* LINES/ROWS Keyword.
*
* @var string|null
*/
public $lines_rows;
/**
* @return string
*/
public function build()
{
$ret = 'LOAD DATA ' . $this->options
. ' INFILE ' . $this->file_name;
if ($this->replace_ignore !== null) {
$ret .= ' ' . trim($this->replace_ignore);
}
$ret .= ' INTO TABLE ' . $this->table;
if ($this->partition !== null && strlen((string) $this->partition) > 0) {
$ret .= ' PARTITION ' . ArrayObj::build($this->partition);
}
if ($this->charset_name !== null) {
$ret .= ' CHARACTER SET ' . $this->charset_name;
}
if ($this->fields_keyword !== null) {
$ret .= ' ' . $this->fields_keyword . ' ' . $this->fields_options;
}
if ($this->lines_options !== null && strlen((string) $this->lines_options) > 0) {
$ret .= ' LINES ' . $this->lines_options;
}
if ($this->ignore_number !== null) {
$ret .= ' IGNORE ' . $this->ignore_number . ' ' . $this->lines_rows;
}
if ($this->col_name_or_user_var !== null && count($this->col_name_or_user_var) > 0) {
$ret .= ' ' . ExpressionArray::build($this->col_name_or_user_var);
}
if ($this->set !== null && count($this->set) > 0) {
$ret .= ' SET ' . SetOperation::build($this->set);
}
return $ret;
}
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
++$list->idx; // Skipping `LOAD DATA`.
// parse any options if provided
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx;
/**
* The state of the parser.
*
* @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_KEYWORD && $token->keyword !== 'INFILE') {
$parser->error('Unexpected keyword.', $token);
break;
}
if ($token->type !== Token::TYPE_KEYWORD) {
$parser->error('Unexpected token.', $token);
break;
}
++$list->idx;
$this->file_name = Expression::parse(
$parser,
$list,
['parseField' => 'file']
);
$state = 1;
} elseif ($state === 1) {
if ($token->type === Token::TYPE_KEYWORD) {
if ($token->keyword === 'REPLACE' || $token->keyword === 'IGNORE') {
$this->replace_ignore = trim($token->keyword);
} elseif ($token->keyword === 'INTO') {
$state = 2;
}
}
} elseif ($state === 2) {
if ($token->type !== Token::TYPE_KEYWORD || $token->keyword !== 'TABLE') {
$parser->error('Unexpected token.', $token);
break;
}
++$list->idx;
$this->table = Expression::parse($parser, $list, ['parseField' => 'table']);
$state = 3;
} elseif ($state >= 3 && $state <= 7) {
if ($token->type === Token::TYPE_KEYWORD) {
$newState = $this->parseKeywordsAccordingToState($parser, $list, $state);
if ($newState === $state) {
// Avoid infinite loop
break;
}
} elseif ($token->type === Token::TYPE_OPERATOR && $token->token === '(') {
$this->col_name_or_user_var
= ExpressionArray::parse($parser, $list);
$state = 7;
} else {
$parser->error('Unexpected token.', $token);
break;
}
}
}
--$list->idx;
}
/**
* @param Parser $parser The parser
* @param TokensList $list A token list
* @param string $keyword The keyword
*/
public function parseFileOptions(Parser $parser, TokensList $list, $keyword = 'FIELDS'): void
{
++$list->idx;
if ($keyword === 'FIELDS' || $keyword === 'COLUMNS') {
// parse field options
$this->fields_options = OptionsArray::parse($parser, $list, static::$FIELDS_OPTIONS);
$this->fields_keyword = $keyword;
} else {
// parse line options
$this->lines_options = OptionsArray::parse($parser, $list, static::$LINES_OPTIONS);
}
}
/**
* @param Parser $parser
* @param TokensList $list
* @param int $state
*
* @return int
*/
public function parseKeywordsAccordingToState($parser, $list, $state)
{
$token = $list->tokens[$list->idx];
switch ($state) {
case 3:
if ($token->keyword === 'PARTITION') {
++$list->idx;
$this->partition = ArrayObj::parse($parser, $list);
return 4;
}
// no break
case 4:
if ($token->keyword === 'CHARACTER SET') {
++$list->idx;
$this->charset_name = Expression::parse($parser, $list);
return 5;
}
// no break
case 5:
if ($token->keyword === 'FIELDS' || $token->keyword === 'COLUMNS' || $token->keyword === 'LINES') {
$this->parseFileOptions($parser, $list, $token->value);
return 6;
}
// no break
case 6:
if ($token->keyword === 'IGNORE') {
++$list->idx;
$this->ignore_number = Expression::parse($parser, $list);
$nextToken = $list->getNextOfType(Token::TYPE_KEYWORD);
if (
$nextToken->type === Token::TYPE_KEYWORD
&& (($nextToken->keyword === 'LINES')
|| ($nextToken->keyword === 'ROWS'))
) {
$this->lines_rows = $nextToken->token;
}
return 7;
}
// no break
case 7:
if ($token->keyword === 'SET') {
++$list->idx;
$this->set = SetOperation::parse($parser, $list);
return 8;
}
// no break
default:
}
return $state;
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\LockExpression;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function trim;
/**
* `LOCK` statement.
*/
class LockStatement extends Statement
{
/**
* Tables with their Lock expressions.
*
* @var LockExpression[]
*/
public $locked = [];
/**
* Whether it's a LOCK statement
* if false, it's an UNLOCK statement
*
* @var bool
*/
public $isLock = true;
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
if ($list->tokens[$list->idx]->value === 'UNLOCK') {
// this is in fact an UNLOCK statement
$this->isLock = false;
}
++$list->idx; // Skipping `LOCK`.
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------- [ TABLES ] -----------------> 1
* 1 -------------- [ lock_expr ] ----------------> 2
* 2 ------------------ [ , ] --------------------> 1
*
* @var int
*/
$state = 0;
/**
* Previous parsed token
*/
$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) {
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) {
if ($token->keyword !== 'TABLES') {
$parser->error('Unexpected keyword.', $token);
break;
}
$state = 1;
continue;
}
$parser->error('Unexpected token.', $token);
break;
}
if ($state === 1) {
if (! $this->isLock) {
// UNLOCK statement should not have any more tokens
$parser->error('Unexpected token.', $token);
break;
}
$this->locked[] = LockExpression::parse($parser, $list);
$state = 2;
} elseif ($state === 2) {
if ($token->value === ',') {
// move over to parsing next lock expression
$state = 1;
}
}
$prevToken = $token;
}
if ($state === 2 || $prevToken === null) {
return;
}
$parser->error('Unexpected end of LOCK statement.', $prevToken);
}
/**
* @return string
*/
public function build()
{
return trim(($this->isLock ? 'LOCK' : 'UNLOCK')
. ' TABLES ' . LockExpression::build($this->locked));
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
/**
* Maintenance statement.
*
* They follow the syntax:
* STMT [some options] tbl_name [, tbl_name] ... [some more options]
*/
class MaintenanceStatement extends Statement
{
/**
* Tables maintained.
*
* @var Expression[]|null
*/
public $tables;
/**
* Function called after the token was processed.
*
* Parses the additional options from the end.
*
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
* @param Token $token the token that is being parsed
*
* @return void
*/
public function after(Parser $parser, TokensList $list, Token $token)
{
// [some options] is going to be parsed first.
//
// There is a parser specified in `Parser::$KEYWORD_PARSERS`
// which parses the name of the tables.
//
// Finally, we parse here [some more options] and that's all.
++$list->idx;
$this->options->merge(
OptionsArray::parse(
$parser,
$list,
static::$OPTIONS
)
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
/**
* Not implemented (yet) statements.
*
* The `after` function makes the parser jump straight to the first delimiter.
*/
class NotImplementedStatement extends Statement
{
/**
* The part of the statement that can't be parsed.
*
* @var Token[]
*/
public $unknown = [];
/**
* @return string
*/
public function build()
{
// Building the parsed part of the query (if any).
$query = parent::build() . ' ';
// Rebuilding the unknown part from tokens.
foreach ($this->unknown as $token) {
$query .= $token->token;
}
return $query;
}
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
for (; $list->idx < $list->count; ++$list->idx) {
if ($list->tokens[$list->idx]->type === Token::TYPE_DELIMITER) {
break;
}
$this->unknown[] = $list->tokens[$list->idx];
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Statement;
/**
* `OPTIMIZE` statement.
*
* OPTIMIZE [NO_WRITE_TO_BINLOG | LOCAL] TABLE
* tbl_name [, tbl_name] ...
*/
class OptimizeStatement extends Statement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'TABLE' => 1,
'NO_WRITE_TO_BINLOG' => 2,
'LOCAL' => 3,
];
/**
* Optimized tables.
*
* @var Expression[]|null
*/
public $tables;
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function in_array;
use function trim;
/**
* `PURGE` statement.
*
* PURGE { BINARY | MASTER } LOGS
* { TO 'log_name' | BEFORE datetime_expr }
*/
class PurgeStatement extends Statement
{
/**
* The type of logs
*
* @var string|null
*/
public $log_type;
/**
* The end option of this query.
*
* @var string|null
*/
public $end_option;
/**
* The end expr of this query.
*
* @var string|null
*/
public $end_expr;
/**
* @return string
*/
public function build()
{
$ret = 'PURGE ' . $this->log_type . ' LOGS '
. ($this->end_option !== null ? ($this->end_option . ' ' . $this->end_expr) : '');
return trim($ret);
}
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*/
public function parse(Parser $parser, TokensList $list)
{
++$list->idx; // Skipping `PURGE`.
/**
* The state of the parser.
*
* @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) {
break;
}
// Skipping whitespaces and comments.
if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
continue;
}
switch ($state) {
case 0:
// parse `{ BINARY | MASTER }`
$this->log_type = self::parseExpectedKeyword($parser, $token, ['BINARY', 'MASTER']);
break;
case 1:
// parse `LOGS`
self::parseExpectedKeyword($parser, $token, ['LOGS']);
break;
case 2:
// parse `{ TO | BEFORE }`
$this->end_option = self::parseExpectedKeyword($parser, $token, ['TO', 'BEFORE']);
break;
case 3:
// parse `expr`
$this->end_expr = Expression::parse($parser, $list, []);
break;
default:
$parser->error('Unexpected token.', $token);
break;
}
$state++;
$prevToken = $token;
}
// Only one possible end state
if ($state === 4) {
return;
}
$parser->error('Unexpected token.', $prevToken);
}
/**
* Parse expected keyword (or throw relevant error)
*
* @param Parser $parser the instance that requests parsing
* @param Token $token token to be parsed
* @param string[] $expectedKeywords array of possibly expected keywords at this point
*
* @return mixed|null
*/
private static function parseExpectedKeyword($parser, $token, $expectedKeywords)
{
if ($token->type === Token::TYPE_KEYWORD) {
if (in_array($token->keyword, $expectedKeywords)) {
return $token->keyword;
}
$parser->error('Unexpected keyword', $token);
} else {
$parser->error('Unexpected token.', $token);
}
return null;
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\RenameOperation;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
/**
* `RENAME` statement.
*
* RENAME TABLE tbl_name TO new_tbl_name
* [, tbl_name2 TO new_tbl_name2] ...
*/
class RenameStatement extends Statement
{
/**
* The old and new names of the tables.
*
* @var RenameOperation[]|null
*/
public $renames;
/**
* Function called before the token is processed.
*
* Skips the `TABLE` keyword after `RENAME`.
*
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
* @param Token $token the token that is being parsed
*
* @return void
*/
public function before(Parser $parser, TokensList $list, Token $token)
{
if (($token->type !== Token::TYPE_KEYWORD) || ($token->keyword !== 'RENAME')) {
return;
}
// Checking if it is the beginning of the query.
$list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'TABLE');
}
/**
* @return string
*/
public function build()
{
return 'RENAME TABLE ' . RenameOperation::build($this->renames);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
/**
* `REPAIR` statement.
*
* REPAIR [NO_WRITE_TO_BINLOG | LOCAL] TABLE
* tbl_name [, tbl_name] ...
* [QUICK] [EXTENDED] [USE_FRM]
*/
class RepairStatement extends MaintenanceStatement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'TABLE' => 1,
'NO_WRITE_TO_BINLOG' => 2,
'LOCAL' => 3,
'QUICK' => 4,
'EXTENDED' => 5,
'USE_FRM' => 6,
];
}

View File

@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Array2d;
use PhpMyAdmin\SqlParser\Components\IntoKeyword;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\SetOperation;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use function count;
use function strlen;
use function trim;
/**
* `REPLACE` statement.
*
* REPLACE [LOW_PRIORITY | DELAYED]
* [INTO] tbl_name [(col_name,...)]
* {VALUES | VALUE} ({expr | DEFAULT},...),(...),...
*
* or
*
* REPLACE [LOW_PRIORITY | DELAYED]
* [INTO] tbl_name
* SET col_name={expr | DEFAULT}, ...
*
* or
*
* REPLACE [LOW_PRIORITY | DELAYED]
* [INTO] tbl_name
* [PARTITION (partition_name,...)]
* [(col_name,...)]
* SELECT ...
*/
class ReplaceStatement extends Statement
{
/**
* Options for `REPLACE` statements and their slot ID.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'LOW_PRIORITY' => 1,
'DELAYED' => 1,
];
/**
* Tables used as target for this statement.
*
* @var IntoKeyword|null
*/
public $into;
/**
* Values to be replaced.
*
* @var Array2d|null
*/
public $values;
/**
* If SET clause is present
* holds the SetOperation.
*
* @var SetOperation[]|null
*/
public $set;
/**
* If SELECT clause is present
* holds the SelectStatement.
*
* @var SelectStatement|null
*/
public $select;
/**
* @return string
*/
public function build()
{
$ret = 'REPLACE ' . $this->options;
$ret = trim($ret) . ' INTO ' . $this->into;
if ($this->values !== null && count($this->values) > 0) {
$ret .= ' VALUES ' . Array2d::build($this->values);
} elseif ($this->set !== null && count($this->set) > 0) {
$ret .= ' SET ' . SetOperation::build($this->set);
} elseif ($this->select !== null && strlen((string) $this->select) > 0) {
$ret .= ' ' . $this->select->build();
}
return $ret;
}
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*
* @return void
*/
public function parse(Parser $parser, TokensList $list)
{
++$list->idx; // Skipping `REPLACE`.
// parse any options if provided
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx;
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------------------------[ INTO ]----------------------------------> 1
*
* 1 -------------------------[ VALUES/VALUE/SET/SELECT ]-----------------------> 2
*
* @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_KEYWORD && $token->keyword !== 'INTO') {
$parser->error('Unexpected keyword.', $token);
break;
}
++$list->idx;
$this->into = IntoKeyword::parse(
$parser,
$list,
['fromReplace' => true]
);
$state = 1;
} elseif ($state === 1) {
if ($token->type !== Token::TYPE_KEYWORD) {
$parser->error('Unexpected token.', $token);
break;
}
if ($token->keyword === 'VALUE' || $token->keyword === 'VALUES') {
++$list->idx; // skip VALUES
$this->values = Array2d::parse($parser, $list);
} elseif ($token->keyword === 'SET') {
++$list->idx; // skip SET
$this->set = SetOperation::parse($parser, $list);
} elseif ($token->keyword === 'SELECT') {
$this->select = new SelectStatement($parser, $list);
} else {
$parser->error('Unexpected keyword.', $token);
break;
}
$state = 2;
}
}
--$list->idx;
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
/**
* `RESTORE` statement.
*
* RESTORE TABLE tbl_name [, tbl_name] ... FROM '/path/to/backup/directory'
*/
class RestoreStatement extends MaintenanceStatement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'TABLE' => 1,
'FROM' => [
2,
'var',
],
];
}

View File

@@ -0,0 +1,353 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\ArrayObj;
use PhpMyAdmin\SqlParser\Components\Condition;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\FunctionCall;
use PhpMyAdmin\SqlParser\Components\GroupKeyword;
use PhpMyAdmin\SqlParser\Components\IndexHint;
use PhpMyAdmin\SqlParser\Components\IntoKeyword;
use PhpMyAdmin\SqlParser\Components\JoinKeyword;
use PhpMyAdmin\SqlParser\Components\Limit;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\OrderKeyword;
use PhpMyAdmin\SqlParser\Statement;
/**
* `SELECT` statement.
*
* SELECT
* [ALL | DISTINCT | DISTINCTROW ]
* [HIGH_PRIORITY]
* [MAX_STATEMENT_TIME = N]
* [STRAIGHT_JOIN]
* [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
* [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
* select_expr [, select_expr ...]
* [FROM table_references
* [PARTITION partition_list]
* [WHERE where_condition]
* [GROUP BY {col_name | expr | position}
* [ASC | DESC], ... [WITH ROLLUP]]
* [HAVING where_condition]
* [ORDER BY {col_name | expr | position}
* [ASC | DESC], ...]
* [LIMIT {[offset,] row_count | row_count OFFSET offset}]
* [PROCEDURE procedure_name(argument_list)]
* [INTO OUTFILE 'file_name'
* [CHARACTER SET charset_name]
* export_options
* | INTO DUMPFILE 'file_name'
* | INTO var_name [, var_name]]
* [FOR UPDATE | LOCK IN SHARE MODE]]
*/
class SelectStatement extends Statement
{
/**
* Options for `SELECT` statements and their slot ID.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'ALL' => 1,
'DISTINCT' => 1,
'DISTINCTROW' => 1,
'HIGH_PRIORITY' => 2,
'MAX_STATEMENT_TIME' => [
3,
'var=',
],
'STRAIGHT_JOIN' => 4,
'SQL_SMALL_RESULT' => 5,
'SQL_BIG_RESULT' => 6,
'SQL_BUFFER_RESULT' => 7,
'SQL_CACHE' => 8,
'SQL_NO_CACHE' => 8,
'SQL_CALC_FOUND_ROWS' => 9,
];
/**
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $END_OPTIONS = [
'FOR UPDATE' => 1,
'LOCK IN SHARE MODE' => 1,
];
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array<string, array<int, int|string>>
* @psalm-var array<string, array{non-empty-string, (1|2|3)}>
*/
public static $CLAUSES = [
'SELECT' => [
'SELECT',
2,
],
// Used for options.
'_OPTIONS' => [
'_OPTIONS',
1,
],
// Used for selected expressions.
'_SELECT' => [
'SELECT',
1,
],
'INTO' => [
'INTO',
3,
],
'FROM' => [
'FROM',
3,
],
'FORCE' => [
'FORCE',
1,
],
'USE' => [
'USE',
1,
],
'IGNORE' => [
'IGNORE',
3,
],
'PARTITION' => [
'PARTITION',
3,
],
'JOIN' => [
'JOIN',
1,
],
'FULL JOIN' => [
'FULL JOIN',
1,
],
'INNER JOIN' => [
'INNER JOIN',
1,
],
'LEFT JOIN' => [
'LEFT JOIN',
1,
],
'LEFT OUTER JOIN' => [
'LEFT OUTER JOIN',
1,
],
'RIGHT JOIN' => [
'RIGHT JOIN',
1,
],
'RIGHT OUTER JOIN' => [
'RIGHT OUTER JOIN',
1,
],
'NATURAL JOIN' => [
'NATURAL JOIN',
1,
],
'NATURAL LEFT JOIN' => [
'NATURAL LEFT JOIN',
1,
],
'NATURAL RIGHT JOIN' => [
'NATURAL RIGHT JOIN',
1,
],
'NATURAL LEFT OUTER JOIN' => [
'NATURAL LEFT OUTER JOIN',
1,
],
'NATURAL RIGHT OUTER JOIN' => [
'NATURAL RIGHT JOIN',
1,
],
'WHERE' => [
'WHERE',
3,
],
'GROUP BY' => [
'GROUP BY',
3,
],
'HAVING' => [
'HAVING',
3,
],
'ORDER BY' => [
'ORDER BY',
3,
],
'LIMIT' => [
'LIMIT',
3,
],
'PROCEDURE' => [
'PROCEDURE',
3,
],
'UNION' => [
'UNION',
1,
],
'EXCEPT' => [
'EXCEPT',
1,
],
'INTERSECT' => [
'INTERSECT',
1,
],
'_END_OPTIONS' => [
'_END_OPTIONS',
1,
],
// These are available only when `UNION` is present.
// 'ORDER BY' => ['ORDER BY', 3],
// 'LIMIT' => ['LIMIT', 3],
];
/**
* Expressions that are being selected by this statement.
*
* @var Expression[]
*/
public $expr = [];
/**
* Tables used as sources for this statement.
*
* @var Expression[]
*/
public $from = [];
/**
* Index hints
*
* @var IndexHint[]|null
*/
public $index_hints;
/**
* Partitions used as source for this statement.
*
* @var ArrayObj|null
*/
public $partition;
/**
* Conditions used for filtering each row of the result set.
*
* @var Condition[]|null
*/
public $where;
/**
* Conditions used for grouping the result set.
*
* @var GroupKeyword[]|null
*/
public $group;
/**
* Conditions used for filtering the result set.
*
* @var Condition[]|null
*/
public $having;
/**
* Specifies the order of the rows in the result set.
*
* @var OrderKeyword[]|null
*/
public $order;
/**
* Conditions used for limiting the size of the result set.
*
* @var Limit|null
*/
public $limit;
/**
* Procedure that should process the data in the result set.
*
* @var FunctionCall|null
*/
public $procedure;
/**
* Destination of this result set.
*
* @var IntoKeyword|null
*/
public $into;
/**
* Joins.
*
* @var JoinKeyword[]|null
*/
public $join;
/**
* Unions.
*
* @var SelectStatement[]
*/
public $union = [];
/**
* The end options of this query.
*
* @see static::$END_OPTIONS
*
* @var OptionsArray|null
*/
public $end_options;
/**
* Gets the clauses of this statement.
*
* @return array<string, array<int, int|string>>
* @psalm-return array<string, array{non-empty-string, (1|2|3)}>
*/
public function getClauses()
{
// This is a cheap fix for `SELECT` statements that contain `UNION`.
// The `ORDER BY` and `LIMIT` clauses should be at the end of the
// statement.
if (! empty($this->union)) {
$clauses = static::$CLAUSES;
unset($clauses['ORDER BY'], $clauses['LIMIT']);
$clauses['ORDER BY'] = [
'ORDER BY',
3,
];
$clauses['LIMIT'] = [
'LIMIT',
3,
];
return $clauses;
}
return static::$CLAUSES;
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\SetOperation;
use PhpMyAdmin\SqlParser\Statement;
use function trim;
/**
* `SET` statement.
*/
class SetStatement extends Statement
{
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array<string, array<int, int|string>>
* @psalm-var array<string, array{non-empty-string, (1|2|3)}>
*/
public static $CLAUSES = [
'SET' => [
'SET',
3,
],
'_END_OPTIONS' => [
'_END_OPTIONS',
1,
],
];
/**
* Possible exceptions in SET statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'CHARSET' => [
3,
'var',
],
'CHARACTER SET' => [
3,
'var',
],
'NAMES' => [
3,
'var',
],
'PASSWORD' => [
3,
'expr',
],
'SESSION' => 3,
'GLOBAL' => 3,
'PERSIST' => 3,
'PERSIST_ONLY' => 3,
'@@SESSION' => 3,
'@@GLOBAL' => 3,
'@@PERSIST' => 3,
'@@PERSIST_ONLY' => 3,
];
/**
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $END_OPTIONS = [
'COLLATE' => [
1,
'var',
],
'DEFAULT' => 1,
];
/**
* Options used in current statement.
*
* @var OptionsArray[]|null
*/
public $options;
/**
* The end options of this query.
*
* @see static::$END_OPTIONS
*
* @var OptionsArray|null
*/
public $end_options;
/**
* The updated values.
*
* @var SetOperation[]|null
*/
public $set;
/**
* @return string
*/
public function build()
{
$ret = 'SET ' . OptionsArray::build($this->options)
. ' ' . SetOperation::build($this->set)
. ' ' . OptionsArray::build($this->end_options);
return trim($ret);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
/**
* `SHOW` statement.
*/
class ShowStatement extends NotImplementedStatement
{
/**
* Options of this statement.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'CREATE' => 1,
'AUTHORS' => 2,
'BINARY' => 2,
'BINLOG' => 2,
'CHARACTER' => 2,
'CODE' => 2,
'COLLATION' => 2,
'COLUMNS' => 2,
'CONTRIBUTORS' => 2,
'DATABASE' => 2,
'DATABASES' => 2,
'ENGINE' => 2,
'ENGINES' => 2,
'ERRORS' => 2,
'EVENT' => 2,
'EVENTS' => 2,
'FUNCTION' => 2,
'GRANTS' => 2,
'HOSTS' => 2,
'INDEX' => 2,
'INNODB' => 2,
'LOGS' => 2,
'MASTER' => 2,
'OPEN' => 2,
'PLUGINS' => 2,
'PRIVILEGES' => 2,
'PROCEDURE' => 2,
'PROCESSLIST' => 2,
'PROFILE' => 2,
'PROFILES' => 2,
'SCHEDULER' => 2,
'SET' => 2,
'SLAVE' => 2,
'STATUS' => 2,
'TABLE' => 2,
'TABLES' => 2,
'TRIGGER' => 2,
'TRIGGERS' => 2,
'VARIABLES' => 2,
'VIEW' => 2,
'WARNINGS' => 2,
];
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\TokensList;
/**
* Transaction statement.
*/
class TransactionStatement extends Statement
{
/**
* START TRANSACTION and BEGIN.
*/
public const TYPE_BEGIN = 1;
/**
* COMMIT and ROLLBACK.
*/
public const TYPE_END = 2;
/**
* The type of this query.
*
* @var int|null
*/
public $type;
/**
* The list of statements in this transaction.
*
* @var Statement[]|null
*/
public $statements;
/**
* The ending transaction statement which may be a `COMMIT` or a `ROLLBACK`.
*
* @var TransactionStatement|null
*/
public $end;
/**
* Options for this query.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'START TRANSACTION' => 1,
'BEGIN' => 1,
'COMMIT' => 1,
'ROLLBACK' => 1,
'WITH CONSISTENT SNAPSHOT' => 2,
'WORK' => 2,
'AND NO CHAIN' => 3,
'AND CHAIN' => 3,
'RELEASE' => 4,
'NO RELEASE' => 4,
];
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*
* @return void
*/
public function parse(Parser $parser, TokensList $list)
{
parent::parse($parser, $list);
// Checks the type of this query.
if ($this->options->has('START TRANSACTION') || $this->options->has('BEGIN')) {
$this->type = self::TYPE_BEGIN;
} elseif ($this->options->has('COMMIT') || $this->options->has('ROLLBACK')) {
$this->type = self::TYPE_END;
}
}
/**
* @return string
*/
public function build()
{
$ret = OptionsArray::build($this->options);
if ($this->type === self::TYPE_BEGIN) {
foreach ($this->statements as $statement) {
/*
* @var SelectStatement $statement
*/
$ret .= ';' . $statement->build();
}
$ret .= ';' . $this->end->build();
}
return $ret;
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Statement;
/**
* `TRUNCATE` statement.
*/
class TruncateStatement extends Statement
{
/**
* Options for `TRUNCATE` statements.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = ['TABLE' => 1];
/**
* The name of the truncated table.
*
* @var Expression|null
*/
public $table;
/**
* Special build method for truncate statement as Statement::build would return empty string.
*
* @return string
*/
public function build()
{
return 'TRUNCATE TABLE ' . $this->table . ';';
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Condition;
use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\Limit;
use PhpMyAdmin\SqlParser\Components\OrderKeyword;
use PhpMyAdmin\SqlParser\Components\SetOperation;
use PhpMyAdmin\SqlParser\Statement;
/**
* `UPDATE` statement.
*
* UPDATE [LOW_PRIORITY] [IGNORE] table_reference
* SET col_name1={expr1|DEFAULT} [, col_name2={expr2|DEFAULT}] ...
* [WHERE where_condition]
* [ORDER BY ...]
* [LIMIT row_count]
*
* or
*
* UPDATE [LOW_PRIORITY] [IGNORE] table_references
* SET col_name1={expr1|DEFAULT} [, col_name2={expr2|DEFAULT}] ...
* [WHERE where_condition]
*/
class UpdateStatement extends Statement
{
/**
* Options for `UPDATE` statements and their slot ID.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'LOW_PRIORITY' => 1,
'IGNORE' => 2,
];
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array<string, array<int, int|string>>
* @psalm-var array<string, array{non-empty-string, (1|2|3)}>
*/
public static $CLAUSES = [
'UPDATE' => [
'UPDATE',
2,
],
// Used for options.
'_OPTIONS' => [
'_OPTIONS',
1,
],
// Used for updated tables.
'_UPDATE' => [
'UPDATE',
1,
],
'SET' => [
'SET',
3,
],
'WHERE' => [
'WHERE',
3,
],
'ORDER BY' => [
'ORDER BY',
3,
],
'LIMIT' => [
'LIMIT',
3,
],
];
/**
* Tables used as sources for this statement.
*
* @var Expression[]|null
*/
public $tables;
/**
* The updated values.
*
* @var SetOperation[]|null
*/
public $set;
/**
* Conditions used for filtering each row of the result set.
*
* @var Condition[]|null
*/
public $where;
/**
* Specifies the order of the rows in the result set.
*
* @var OrderKeyword[]|null
*/
public $order;
/**
* Conditions used for limiting the size of the result set.
*
* @var Limit|null
*/
public $limit;
}

View File

@@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\SqlParser\Statements;
use PhpMyAdmin\SqlParser\Components\Array2d;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Components\WithKeyword;
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;
use PhpMyAdmin\SqlParser\Translator;
use function array_slice;
use function count;
/**
* `WITH` statement.
* WITH [RECURSIVE] query_name [ (column_name [,...]) ] AS (SELECT ...) [, ...]
*/
final class WithStatement extends Statement
{
/**
* Options for `WITH` statements and their slot ID.
*
* @var array<string, int|array<int, int|string>>
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = ['RECURSIVE' => 1];
/**
* The clauses of this statement, in order.
*
* @see Statement::$CLAUSES
*
* @var array<string, array<int, int|string>>
* @psalm-var array<string, array{non-empty-string, (1|2|3)}>
*/
public static $CLAUSES = [
'WITH' => [
'WITH',
2,
],
// Used for options.
'_OPTIONS' => [
'_OPTIONS',
1,
],
'AS' => [
'AS',
2,
],
];
/** @var WithKeyword[] */
public $withers = [];
/**
* holds the CTE parser.
*
* @var Parser|null
*/
public $cteStatementParser;
/**
* @param Parser $parser the instance that requests parsing
* @param TokensList $list the list of tokens to be parsed
*
* @return void
*/
public function parse(Parser $parser, TokensList $list)
{
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 ---------------- [ name ] -----------------> 1
*
* 1 ------------------ [ ( ] ------------------> 2
*
* 2 ------------------ [ AS ] -----------------> 3
*
* 3 ------------------ [ ( ] ------------------> 4
*
* 4 ------------------ [ , ] ------------------> 1
*
* 4 ----- [ SELECT/UPDATE/DELETE/INSERT ] -----> 5
*
* @var int
*/
$state = 0;
$wither = null;
++$list->idx; // Skipping `WITH`.
// parse any options if provided
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx;
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_NONE) {
$parser->error('The name of the CTE was expected.', $token);
break;
}
$wither = $token->value;
$this->withers[$wither] = new WithKeyword($wither);
$state = 1;
} elseif ($state === 1) {
if ($token->type === Token::TYPE_OPERATOR && $token->value === '(') {
$this->withers[$wither]->columns = Array2d::parse($parser, $list);
$state = 2;
} elseif ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'AS') {
$state = 3;
} else {
$parser->error('Unexpected token.', $token);
break;
}
} elseif ($state === 2) {
if (! ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'AS')) {
$parser->error('AS keyword was expected.', $token);
break;
}
$state = 3;
} elseif ($state === 3) {
$idxBeforeGetNext = $list->idx;
$list->idx++; // Ignore the current token
$nextKeyword = $list->getNext();
if (! ($token->value === '(' && ($nextKeyword && $nextKeyword->value === 'SELECT'))) {
$parser->error('Subquery of the CTE was expected.', $token);
$list->idx = $idxBeforeGetNext;
break;
}
// Restore the index
$list->idx = $idxBeforeGetNext;
++$list->idx;
$subList = $this->getSubTokenList($list);
if ($subList instanceof ParserException) {
$parser->errors[] = $subList;
break;
}
$subParser = new Parser($subList);
if (count($subParser->errors)) {
foreach ($subParser->errors as $error) {
$parser->errors[] = $error;
}
break;
}
$this->withers[$wither]->statement = $subParser;
$state = 4;
} elseif ($state === 4) {
if ($token->value === ',') {
// There's another WITH expression to parse, go back to state=0
$state = 0;
continue;
}
if (
$token->type === Token::TYPE_KEYWORD && (
$token->value === 'SELECT'
|| $token->value === 'INSERT'
|| $token->value === 'UPDATE'
|| $token->value === 'DELETE'
)
) {
$state = 5;
--$list->idx;
continue;
}
$parser->error('An expression was expected.', $token);
break;
} elseif ($state === 5) {
/**
* We need to parse all of the remaining tokens becuase mostly, they are only the CTE expression
* which's mostly is SELECT, or INSERT, UPDATE, or delete statement.
* e.g: INSERT .. ( SELECT 1 ) SELECT col1 FROM cte ON DUPLICATE KEY UPDATE col_name = 3.
* The issue is that, `ON DUPLICATE KEY UPDATE col_name = 3` is related to the main INSERT query
* not the cte expression (SELECT col1 FROM cte) we need to determine the end of the expression
* to parse `ON DUPLICATE KEY UPDATE` from the InsertStatement parser instead.
*/
// Index of the last parsed token by default would be the last token in the $list, because we're
// assuming that all remaining tokens at state 4, are related to the expression.
$idxOfLastParsedToken = $list->count - 1;
// Index before search to be able to restore the index.
$idxBeforeSearch = $list->idx;
// Length of expression tokens is null by default, in order for the $subList to start
// from $list->idx to the end of the $list.
$lengthOfExpressionTokens = null;
if ($list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ON')) {
// (-1) because getNextOfTypeAndValue returned ON and increased the index.
$idxOfOn = $list->idx - 1;
// We want to make sure that it's `ON DUPLICATE KEY UPDATE`
$dubplicateToken = $list->getNext();
$keyToken = $list->getNext();
$updateToken = $list->getNext();
if (
$dubplicateToken && $dubplicateToken->keyword === 'DUPLICATE'
&& ($keyToken && $keyToken->keyword === 'KEY')
&& ($updateToken && $updateToken->keyword === 'UPDATE')
) {
// Index of the last parsed token will be the token before the ON Keyword
$idxOfLastParsedToken = $idxOfOn - 1;
// The length of the expression tokens would be the difference
// between the first unrelated token `ON` and the idx
// before skipping the CTE tokens.
$lengthOfExpressionTokens = $idxOfOn - $idxBeforeSearch;
}
}
// Restore the index
$list->idx = $idxBeforeSearch;
$subList = new TokensList(array_slice($list->tokens, $list->idx, $lengthOfExpressionTokens));
$subParser = new Parser($subList);
if (count($subParser->errors)) {
foreach ($subParser->errors as $error) {
$parser->errors[] = $error;
}
break;
}
$this->cteStatementParser = $subParser;
$list->idx = $idxOfLastParsedToken;
break;
}
}
// 5 is the only valid end state
if ($state !== 5) {
/**
* Token parsed at this moment.
*/
$token = $list->tokens[$list->idx];
$parser->error('Unexpected end of the WITH CTE.', $token);
}
--$list->idx;
}
/**
* {@inheritdoc}
*/
public function build()
{
$str = 'WITH ';
foreach ($this->withers as $wither) {
$str .= $str === 'WITH ' ? '' : ', ';
$str .= WithKeyword::build($wither);
}
$str .= ' ';
if ($this->cteStatementParser) {
foreach ($this->cteStatementParser->statements as $statement) {
$str .= $statement->build();
}
}
return $str;
}
/**
* Get tokens within the WITH expression to use them in another parser
*
* @return ParserException|TokensList
*/
private function getSubTokenList(TokensList $list)
{
$idx = $list->idx;
$token = $list->tokens[$list->idx];
$openParenthesis = 0;
while ($list->idx < $list->count) {
if ($token->value === '(') {
++$openParenthesis;
} elseif ($token->value === ')') {
if (--$openParenthesis === -1) {
break;
}
}
++$list->idx;
if (! isset($list->tokens[$list->idx])) {
break;
}
$token = $list->tokens[$list->idx];
}
// performance improvement: return the error to avoid a try/catch in the loop
if ($list->idx === $list->count) {
--$list->idx;
return new ParserException(
Translator::gettext('A closing bracket was expected.'),
$token
);
}
$length = $list->idx - $idx;
return new TokensList(array_slice($list->tokens, $idx, $length), $length);
}
}