Skip to content

Commit

Permalink
feat: Implementing and refactoring the KILL statement parser
Browse files Browse the repository at this point in the history
Signed-off-by: Fawzi Abdulfattah <[email protected]>
  • Loading branch information
iifawzi committed Jul 11, 2024
1 parent 06b5a52 commit c5033af
Show file tree
Hide file tree
Showing 18 changed files with 961 additions and 69 deletions.
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',
'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.
*
* @psalm-var bool
*/
public $IDKeywordUsed = false;

public function build(): string
/**
* Whether parenthesis used around the identifier or not
*
* @psalm-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

0 comments on commit c5033af

Please sign in to comment.