Skip to content

Commit afe9173

Browse files
committed
feat: Implementing and refactoring the KILL statement parser
Signed-off-by: Fawzi Abdulfattah <[email protected]>
1 parent 06b5a52 commit afe9173

19 files changed

+967
-69
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ phpstan.neon
1515
# Infection
1616
infection.json
1717
infection.log
18+
**/.DS_Store

src/Parser.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,6 @@ class Parser extends Core
211211
'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
212212
'field' => 'join',
213213
],
214-
'KILL' => [
215-
'class' => 'PhpMyAdmin\\SqlParser\\Components\\Expression',
216-
'field' => 'processListId',
217-
],
218214
'LEFT JOIN' => [
219215
'class' => 'PhpMyAdmin\\SqlParser\\Components\\JoinKeyword',
220216
'field' => 'join',

src/Statements/KillStatement.php

Lines changed: 155 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@
44

55
namespace PhpMyAdmin\SqlParser\Statements;
66

7-
use PhpMyAdmin\SqlParser\Components\Expression;
87
use PhpMyAdmin\SqlParser\Components\OptionsArray;
8+
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
9+
use PhpMyAdmin\SqlParser\Parser;
910
use PhpMyAdmin\SqlParser\Statement;
11+
use PhpMyAdmin\SqlParser\Token;
12+
use PhpMyAdmin\SqlParser\TokensList;
1013

11-
use function trim;
14+
use function array_slice;
15+
use function count;
16+
use function is_int;
1217

13-
/**
14-
* `KILL` statement.
15-
*
16-
* KILL [CONNECTION | QUERY] processlist_id
18+
/** KILL [HARD|SOFT]
19+
* {
20+
* {CONNECTION|QUERY} id |
21+
* QUERY ID query_id | USER user_name
22+
* }
1723
*/
1824
class KillStatement extends Statement
1925
{
@@ -24,20 +30,153 @@ class KillStatement extends Statement
2430
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
2531
*/
2632
public static $OPTIONS = [
27-
'CONNECTION' => 1,
28-
'QUERY' => 1,
33+
'HARD' => 1,
34+
'SOFT' => 1,
35+
'CONNECTION' => 2,
36+
'QUERY' => 2,
37+
'USER' => 2,
2938
];
3039

31-
/** @var Expression|null */
32-
public $processListId = null;
40+
/**
41+
* Holds the identifier if explicitly set
42+
*
43+
* @psalm-var Statement|int|null
44+
*/
45+
public $identifier = null;
46+
47+
/**
48+
* Whether MariaDB ID keyword is used or not.
49+
*
50+
* @psalm-var bool
51+
*/
52+
public $IDKeywordUsed = false;
53+
54+
/**
55+
* Whether parenthesis used around the identifier or not
56+
*
57+
* @psalm-var bool
58+
*/
59+
public $parenthesisUsed = false;
60+
61+
/** @throws ParserException */
62+
public function parse(Parser $parser, TokensList $list): void
63+
{
64+
/**
65+
* The state of the parser.
66+
*
67+
* Below are the states of the parser.
68+
*
69+
* 0 --------------------- [ OPTIONS PARSED ] --------------------------> 0
70+
*
71+
* 0 -------------------- [ number ] -----------------------------------> 2
72+
*
73+
* 0 -------------------- [ ( ] ----------------------------------------> 3
74+
*
75+
* 0 -------------------- [ QUERY ID ] ---------------------------------> 0
76+
*
77+
* 3 -------------------- [ number ] -----------------------------------> 3
78+
*
79+
* 3 -------------------- [ SELECT STATEMENT ] -------------------------> 2
80+
*
81+
* 3 -------------------- [ ) ] ----------------------------------------> 2
82+
*
83+
* 2 ----------------------------------------------------------> Final state
84+
*/
85+
$state = 0;
86+
87+
++$list->idx; // Skipping `KILL`.
88+
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
89+
++$list->idx;
90+
for (; $list->idx < $list->count; ++$list->idx) {
91+
$token = $list->tokens[$list->idx];
92+
93+
if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
94+
continue;
95+
}
3396

34-
public function build(): string
97+
switch ($state) {
98+
case 0:
99+
$currIdx = $list->idx;
100+
$prev = $list->getPreviousOfType(Token::TYPE_KEYWORD);
101+
$list->idx = $currIdx;
102+
if ($token->type === Token::TYPE_NUMBER && is_int($token->value)) {
103+
$this->identifier = $token->value;
104+
$state = 2;
105+
} elseif ($token->type === Token::TYPE_OPERATOR && $token->value === '(') {
106+
$this->parenthesisUsed = true;
107+
$state = 3;
108+
} elseif ($prev && $token->value === 'ID' && $prev->value === 'QUERY') {
109+
$this->IDKeywordUsed = true;
110+
$state = 0;
111+
} else {
112+
$parser->error('Unexpected token.', $token);
113+
break 2;
114+
}
115+
116+
break;
117+
118+
case 3:
119+
if ($token->type === Token::TYPE_KEYWORD && $token->value === 'SELECT') {
120+
$subList = new TokensList(array_slice($list->tokens, $list->idx - 1));
121+
$subParser = new Parser($subList);
122+
if (count($subParser->errors)) {
123+
foreach ($subParser->errors as $error) {
124+
$parser->errors[] = $error;
125+
}
126+
127+
break;
128+
}
129+
130+
$this->identifier = $subParser->statements[0];
131+
$state = 2;
132+
} elseif ($token->type === Token::TYPE_OPERATOR && $token->value === ')') {
133+
$state = 2;
134+
} elseif ($token->type === Token::TYPE_NUMBER && is_int($token->value)) {
135+
$this->identifier = $token->value;
136+
$state = 3;
137+
} else {
138+
$parser->error('Unexpected token.', $token);
139+
break 2;
140+
}
141+
142+
break;
143+
}
144+
}
145+
146+
if ($state !== 2) {
147+
$token = $list->tokens[$list->idx];
148+
$parser->error('Unexpected end of the KILL statement.', $token);
149+
}
150+
151+
--$list->idx;
152+
}
153+
154+
/**
155+
* {@inheritdoc}
156+
*/
157+
public function build()
35158
{
36-
$option = $this->options === null || $this->options->isEmpty()
37-
? ''
38-
: ' ' . OptionsArray::build($this->options);
39-
$expression = $this->processListId === null ? '' : ' ' . Expression::build($this->processListId);
159+
$ret = 'KILL';
160+
161+
if ($this->options && count($this->options->options) > 0) {
162+
$ret .= ' ' . OptionsArray::build($this->options);
163+
}
164+
165+
if ($this->IDKeywordUsed) {
166+
$ret .= ' ID';
167+
}
168+
169+
$builtIdentifier = (string) $this->identifier;
170+
if ($this->identifier instanceof Statement) {
171+
$builtIdentifier = $this->identifier->build();
172+
}
173+
174+
if ($this->parenthesisUsed) {
175+
$ret .= ' (' . $builtIdentifier . ')';
176+
} elseif ($this->identifier !== null) {
177+
$ret .= ' ' . $builtIdentifier;
178+
}
40179

41-
return trim('KILL' . $option . $expression);
180+
return $ret;
42181
}
43182
}

tests/Parser/KillStatementTest.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,29 @@
1010

1111
class KillStatementTest extends TestCase
1212
{
13-
/**
14-
* @dataProvider killProvider
15-
*/
13+
/** @dataProvider killProvider */
1614
public function testKill(string $test): void
1715
{
1816
$this->runParserTest($test);
1917
}
2018

21-
/**
22-
* @return string[][]
23-
*/
24-
public function killProvider(): array
19+
/** @return string[][] */
20+
public static function killProvider(): array
2521
{
2622
return [
2723
['parser/parseKill'],
24+
['parser/parseKill2'],
25+
['parser/parseKill3'],
2826
['parser/parseKillConnection'],
2927
['parser/parseKillQuery'],
28+
['parser/parseKillErr1'],
29+
['parser/parseKillErr2'],
30+
['parser/parseKillErr3'],
31+
['parser/parseKillErr4'],
3032
];
3133
}
3234

33-
/**
34-
* @dataProvider buildKillProvider
35-
*/
35+
/** @dataProvider buildKillProvider */
3636
public function testBuildKill(string $sql): void
3737
{
3838
$parser = new Parser($sql);
@@ -47,13 +47,21 @@ public function testBuildKill(string $sql): void
4747
* @return array<int, array<int, string>>
4848
* @psalm-return list<list<string>>
4949
*/
50-
public function buildKillProvider(): array
50+
public static function buildKillProvider(): array
5151
{
5252
return [
5353
['KILL (SELECT 3 + 4)'],
54-
['KILL QUERY 3'],
55-
['KILL CONNECTION 3'],
56-
['KILL'],
54+
['KILL QUERY 4'],
55+
['KILL CONNECTION 5'],
56+
['KILL 6'],
57+
['KILL QUERY (SELECT 7)'],
58+
['KILL SOFT QUERY (SELECT 8)'],
59+
['KILL HARD 9'],
60+
['KILL USER 10'],
61+
['KILL SOFT (SELECT 1)'],
62+
['KILL (2)'],
63+
['KILL QUERY ID (2)'],
64+
['KILL QUERY ID (SELECT ID FROM INFORMATION_SCHEMA.PROCESSLIST LIMIT 0, 1)'],
5765
];
5866
}
5967
}

tests/data/parser/parseKill.out

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,15 @@
6161
"statements": [
6262
{
6363
"@type": "PhpMyAdmin\\SqlParser\\Statements\\KillStatement",
64-
"processListId": {
65-
"@type": "PhpMyAdmin\\SqlParser\\Components\\Expression",
66-
"database": null,
67-
"table": null,
68-
"column": null,
69-
"expr": "1",
70-
"alias": null,
71-
"function": null,
72-
"subquery": null
73-
},
64+
"identifier": 1,
65+
"IDKeywordUsed": false,
66+
"parenthesisUsed": false,
7467
"options": {
7568
"@type": "PhpMyAdmin\\SqlParser\\Components\\OptionsArray",
7669
"options": []
7770
},
7871
"first": 0,
79-
"last": 2
72+
"last": 3
8073
}
8174
],
8275
"brackets": 0,

tests/data/parser/parseKill2.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
KILL (SELECT 3 + 4)

0 commit comments

Comments
 (0)