<?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;
}
}