Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #556 - Implementing and refactoring the KILL statement parser #565

Merged
merged 4 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,6 @@ class Parser extends Core
'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
'field' => 'join',
],
'KILL' => [
'class' => 'PhpMyAdmin\\SqlParser\\Components\\Expression',
williamdes marked this conversation as resolved.
Show resolved Hide resolved
'field' => 'processListId',
],
'LEFT JOIN' => [
'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
'field' => 'join',
Expand Down
166 changes: 150 additions & 16 deletions src/Statements/KillStatement.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@

namespace PhpMyAdmin\SqlParser\Statements;

use PhpMyAdmin\SqlParser\Components\Expression;
use PhpMyAdmin\SqlParser\Components\OptionsArray;
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
use PhpMyAdmin\SqlParser\Parser;
use PhpMyAdmin\SqlParser\Statement;
use PhpMyAdmin\SqlParser\Token;
use PhpMyAdmin\SqlParser\TokensList;

use function trim;
use function array_slice;
use function is_int;

/**
* `KILL` statement.
*
* KILL [CONNECTION | QUERY] processlist_id
/** KILL [HARD|SOFT]
* {
* {CONNECTION|QUERY} id |
* QUERY ID query_id | USER user_name
* }
*/
class KillStatement extends Statement
{
Expand All @@ -24,20 +29,149 @@ class KillStatement extends Statement
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
*/
public static $OPTIONS = [
'CONNECTION' => 1,
'QUERY' => 1,
'HARD' => 1,
'SOFT' => 1,
'CONNECTION' => 2,
'QUERY' => 2,
'USER' => 2,
];

/** @var Expression|null */
public $processListId = null;
/**
* Holds the identifier if explicitly set
*
* @psalm-var Statement|int|null
*/
public $identifier = null;

/**
* Whether MariaDB ID keyword is used or not.
*
* @var bool
*/
public $idKeywordUsed = false;

public function build(): string
/**
* Whether parenthesis used around the identifier or not
*
* @var bool
*/
public $parenthesisUsed = false;

/** @throws ParserException */
public function parse(Parser $parser, TokensList $list): void
{
$option = $this->options === null || $this->options->isEmpty()
? ''
: ' ' . OptionsArray::build($this->options);
$expression = $this->processListId === null ? '' : ' ' . Expression::build($this->processListId);
/**
* The state of the parser.
*
* Below are the states of the parser.
*
* 0 --------------------- [ OPTIONS PARSED ] --------------------------> 0
*
* 0 -------------------- [ number ] -----------------------------------> 2
*
* 0 -------------------- [ ( ] ----------------------------------------> 3
*
* 0 -------------------- [ QUERY ID ] ---------------------------------> 0
*
* 3 -------------------- [ number ] -----------------------------------> 3
*
* 3 -------------------- [ SELECT STATEMENT ] -------------------------> 2
*
* 3 -------------------- [ ) ] ----------------------------------------> 2
*
* 2 ----------------------------------------------------------> Final state
*/
$state = 0;

++$list->idx; // Skipping `KILL`.
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
++$list->idx;
for (; $list->idx < $list->count; ++$list->idx) {
$token = $list->tokens[$list->idx];

if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
continue;
}

switch ($state) {
case 0:
$currIdx = $list->idx;
$prev = $list->getPreviousOfType(Token::TYPE_KEYWORD);
$list->idx = $currIdx;
if ($token->type === Token::TYPE_NUMBER && is_int($token->value)) {
$this->identifier = $token->value;
$state = 2;
} elseif ($token->type === Token::TYPE_OPERATOR && $token->value === '(') {
$this->parenthesisUsed = true;
$state = 3;
} elseif ($prev && $token->value === 'ID' && $prev->value === 'QUERY') {
$this->idKeywordUsed = true;
$state = 0;
} else {
$parser->error('Unexpected token.', $token);
break 2;
}

break;

case 3:
if ($token->type === Token::TYPE_KEYWORD && $token->value === 'SELECT') {
$subList = new TokensList(array_slice($list->tokens, $list->idx - 1));
$subParser = new Parser($subList);
if ($subParser->errors !== []) {
foreach ($subParser->errors as $error) {
$parser->errors[] = $error;
}

break;
}

$this->identifier = $subParser->statements[0];
$state = 2;
} elseif ($token->type === Token::TYPE_OPERATOR && $token->value === ')') {
$state = 2;
} elseif ($token->type === Token::TYPE_NUMBER && is_int($token->value)) {
$this->identifier = $token->value;
$state = 3;
} else {
$parser->error('Unexpected token.', $token);
break 2;
}

break;
}
}

if ($state !== 2) {
$token = $list->tokens[$list->idx];
$parser->error('Unexpected end of the KILL statement.', $token);
}

--$list->idx;
}

/**
* {@inheritdoc}
*/
public function build()
{
$ret = 'KILL';

if ($this->options !== null && $this->options->options !== []) {
$ret .= ' ' . OptionsArray::build($this->options);
}

if ($this->idKeywordUsed) {
$ret .= ' ID';
}

$identifier = (string) $this->identifier;
if ($this->parenthesisUsed) {
$ret .= ' (' . $identifier . ')';
} else {
$ret .= ' ' . $identifier;
}

return trim('KILL' . $option . $expression);
return $ret;
}
}
36 changes: 22 additions & 14 deletions tests/Parser/KillStatementTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,29 @@

class KillStatementTest extends TestCase
{
/**
* @dataProvider killProvider
*/
/** @dataProvider killProvider */
public function testKill(string $test): void
{
$this->runParserTest($test);
}

/**
* @return string[][]
*/
public function killProvider(): array
/** @return string[][] */
public static function killProvider(): array
{
return [
['parser/parseKill'],
['parser/parseKill2'],
['parser/parseKill3'],
['parser/parseKillConnection'],
['parser/parseKillQuery'],
['parser/parseKillErr1'],
['parser/parseKillErr2'],
['parser/parseKillErr3'],
['parser/parseKillErr4'],
];
}

/**
* @dataProvider buildKillProvider
*/
/** @dataProvider buildKillProvider */
public function testBuildKill(string $sql): void
{
$parser = new Parser($sql);
Expand All @@ -47,13 +47,21 @@ public function testBuildKill(string $sql): void
* @return array<int, array<int, string>>
* @psalm-return list<list<string>>
*/
public function buildKillProvider(): array
public static function buildKillProvider(): array
{
return [
['KILL (SELECT 3 + 4)'],
['KILL QUERY 3'],
['KILL CONNECTION 3'],
['KILL'],
['KILL QUERY 4'],
['KILL CONNECTION 5'],
['KILL 6'],
['KILL QUERY (SELECT 7)'],
['KILL SOFT QUERY (SELECT 8)'],
['KILL HARD 9'],
['KILL USER 10'],
['KILL SOFT (SELECT 1)'],
['KILL (2)'],
['KILL QUERY ID (2)'],
['KILL QUERY ID (SELECT ID FROM INFORMATION_SCHEMA.PROCESSLIST LIMIT 0, 1)'],
];
}
}
15 changes: 4 additions & 11 deletions tests/data/parser/parseKill.out
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,15 @@
"statements": [
{
"@type": "PhpMyAdmin\\SqlParser\\Statements\\KillStatement",
"processListId": {
"@type": "PhpMyAdmin\\SqlParser\\Components\\Expression",
"database": null,
"table": null,
"column": null,
"expr": "1",
"alias": null,
"function": null,
"subquery": null
},
"identifier": 1,
"idKeywordUsed": false,
"parenthesisUsed": false,
"options": {
"@type": "PhpMyAdmin\\SqlParser\\Components\\OptionsArray",
"options": []
},
"first": 0,
"last": 2
"last": 3
}
],
"brackets": 0,
Expand Down
1 change: 1 addition & 0 deletions tests/data/parser/parseKill2.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KILL (SELECT 3 + 4)
Loading
Loading