424 lines
11 KiB
PHP
424 lines
11 KiB
PHP
<?php
|
|
|
|
/**
|
|
* `LOAD` statement.
|
|
*/
|
|
|
|
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;
|
|
|
|
/**
|
|
* `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,...]
|
|
*
|
|
*
|
|
* @category Statements
|
|
*
|
|
* @license https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
|
|
*/
|
|
class LoadStatement extends Statement
|
|
{
|
|
/**
|
|
* Options for `LOAD` statements and their slot ID.
|
|
*
|
|
* @var array
|
|
*/
|
|
public static $OPTIONS = array(
|
|
'LOW_PRIORITY' => 1,
|
|
'CONCURRENT' => 1,
|
|
'LOCAL' => 2
|
|
);
|
|
|
|
/**
|
|
* FIELDS/COLUMNS Options for `LOAD DATA...INFILE` statements.
|
|
*
|
|
* @var array
|
|
*/
|
|
public static $FIELDS_OPTIONS = array(
|
|
'TERMINATED BY' => array(
|
|
1,
|
|
'expr',
|
|
),
|
|
'OPTIONALLY' => 2,
|
|
'ENCLOSED BY' => array(
|
|
3,
|
|
'expr',
|
|
),
|
|
'ESCAPED BY' => array(
|
|
4,
|
|
'expr',
|
|
)
|
|
);
|
|
|
|
/**
|
|
* LINES Options for `LOAD DATA...INFILE` statements.
|
|
*
|
|
* @var array
|
|
*/
|
|
public static $LINES_OPTIONS = array(
|
|
'STARTING BY' => array(
|
|
1,
|
|
'expr',
|
|
),
|
|
'TERMINATED BY' => array(
|
|
2,
|
|
'expr',
|
|
)
|
|
);
|
|
|
|
/**
|
|
* File name being used to load data.
|
|
*
|
|
* @var Expression
|
|
*/
|
|
public $file_name;
|
|
|
|
/**
|
|
* Table used as destination for this statement.
|
|
*
|
|
* @var Expression
|
|
*/
|
|
public $table;
|
|
|
|
/**
|
|
* Partitions used as source for this statement.
|
|
*
|
|
* @var ArrayObj
|
|
*/
|
|
public $partition;
|
|
|
|
/**
|
|
* Character set used in this statement.
|
|
*
|
|
* @var Expression
|
|
*/
|
|
public $charset_name;
|
|
|
|
/**
|
|
* Options for FIELDS/COLUMNS keyword.
|
|
*
|
|
* @var OptionsArray
|
|
*
|
|
* @see static::$FIELDS_OPTIONS
|
|
*/
|
|
public $fields_options;
|
|
|
|
/**
|
|
* Whether to use `FIELDS` or `COLUMNS` while building.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $fields_keyword;
|
|
|
|
/**
|
|
* Options for OPTIONS keyword.
|
|
*
|
|
* @var OptionsArray
|
|
*
|
|
* @see static::$LINES_OPTIONS
|
|
*/
|
|
public $lines_options;
|
|
|
|
/**
|
|
* Column names or user variables.
|
|
*
|
|
* @var Expression[]
|
|
*/
|
|
public $col_name_or_user_var;
|
|
|
|
/**
|
|
* SET clause's updated values(optional).
|
|
*
|
|
* @var SetOperation[]
|
|
*/
|
|
public $set;
|
|
|
|
/**
|
|
* Ignore 'number' LINES/ROWS.
|
|
*
|
|
* @var Expression
|
|
*/
|
|
public $ignore_number;
|
|
|
|
/**
|
|
* REPLACE/IGNORE Keyword.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $replace_ignore;
|
|
|
|
/**
|
|
* LINES/ROWS Keyword.
|
|
*
|
|
* @var string
|
|
*/
|
|
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($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($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.
|
|
*
|
|
* @var Token
|
|
*/
|
|
$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;
|
|
} elseif ($token->type !== Token::TYPE_KEYWORD) {
|
|
$parser->error('Unexpected token.', $token);
|
|
break;
|
|
}
|
|
|
|
++$list->idx;
|
|
$this->file_name = Expression::parse(
|
|
$parser,
|
|
$list,
|
|
array('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'
|
|
) {
|
|
++$list->idx;
|
|
$this->table = Expression::parse($parser, $list, array('parseField' => 'table'));
|
|
$state = 3;
|
|
} else {
|
|
$parser->error('Unexpected token.', $token);
|
|
break;
|
|
}
|
|
} 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;
|
|
}
|
|
|
|
public function parseFileOptions(Parser $parser, TokensList $list, $keyword = 'FIELDS')
|
|
{
|
|
++$list->idx;
|
|
|
|
if ($keyword === 'FIELDS' || $keyword === 'COLUMNS') {
|
|
// parse field options
|
|
$this->fields_options = OptionsArray::parse(
|
|
$parser,
|
|
$list,
|
|
static::$FIELDS_OPTIONS
|
|
);
|
|
|
|
$this->fields_keyword = $keyword;
|
|
} else {
|
|
// parse line options
|
|
$this->lines_options = OptionsArray::parse(
|
|
$parser,
|
|
$list,
|
|
static::$LINES_OPTIONS
|
|
);
|
|
}
|
|
}
|
|
|
|
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);
|
|
$state = 4;
|
|
|
|
return $state;
|
|
}
|
|
// no break
|
|
case 4:
|
|
if ($token->keyword === 'CHARACTER SET') {
|
|
++$list->idx;
|
|
$this->charset_name = Expression::parse($parser, $list);
|
|
$state = 5;
|
|
|
|
return $state;
|
|
}
|
|
// no break
|
|
case 5:
|
|
if ($token->keyword === 'FIELDS'
|
|
|| $token->keyword === 'COLUMNS'
|
|
|| $token->keyword === 'LINES'
|
|
) {
|
|
$this->parseFileOptions($parser, $list, $token->value);
|
|
$state = 6;
|
|
|
|
return $state;
|
|
}
|
|
// 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;
|
|
}
|
|
$state = 7;
|
|
|
|
return $state;
|
|
}
|
|
// no break
|
|
case 7:
|
|
if ($token->keyword === 'SET') {
|
|
++$list->idx;
|
|
$this->set = SetOperation::parse($parser, $list);
|
|
$state = 8;
|
|
|
|
return $state;
|
|
}
|
|
// no break
|
|
default:
|
|
}
|
|
|
|
return $state;
|
|
}
|
|
}
|