Skip to content

Commit 03f0532

Browse files
committed
Merge #565 - Fix #556 - Implementing and refactoring the KILL statement parser
Pull-request: #565 Fixes: #556
2 parents f6d0619 + 5a7aec8 commit 03f0532

18 files changed

+958
-64
lines changed

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: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@
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 is_int;
1216

13-
/**
14-
* `KILL` statement.
15-
*
16-
* KILL [CONNECTION | QUERY] processlist_id
17+
/** KILL [HARD|SOFT]
18+
* {
19+
* {CONNECTION|QUERY} id |
20+
* QUERY ID query_id | USER user_name
21+
* }
1722
*/
1823
class KillStatement extends Statement
1924
{
@@ -24,20 +29,149 @@ class KillStatement extends Statement
2429
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
2530
*/
2631
public static $OPTIONS = [
27-
'CONNECTION' => 1,
28-
'QUERY' => 1,
32+
'HARD' => 1,
33+
'SOFT' => 1,
34+
'CONNECTION' => 2,
35+
'QUERY' => 2,
36+
'USER' => 2,
2937
];
3038

31-
/** @var Expression|null */
32-
public $processListId = null;
39+
/**
40+
* Holds the identifier if explicitly set
41+
*
42+
* @psalm-var Statement|int|null
43+
*/
44+
public $identifier = null;
45+
46+
/**
47+
* Whether MariaDB ID keyword is used or not.
48+
*
49+
* @var bool
50+
*/
51+
public $idKeywordUsed = false;
3352

34-
public function build(): string
53+
/**
54+
* Whether parenthesis used around the identifier or not
55+
*
56+
* @var bool
57+
*/
58+
public $parenthesisUsed = false;
59+
60+
/** @throws ParserException */
61+
public function parse(Parser $parser, TokensList $list): void
3562
{
36-
$option = $this->options === null || $this->options->isEmpty()
37-
? ''
38-
: ' ' . OptionsArray::build($this->options);
39-
$expression = $this->processListId === null ? '' : ' ' . Expression::build($this->processListId);
63+
/**
64+
* The state of the parser.
65+
*
66+
* Below are the states of the parser.
67+
*
68+
* 0 --------------------- [ OPTIONS PARSED ] --------------------------> 0
69+
*
70+
* 0 -------------------- [ number ] -----------------------------------> 2
71+
*
72+
* 0 -------------------- [ ( ] ----------------------------------------> 3
73+
*
74+
* 0 -------------------- [ QUERY ID ] ---------------------------------> 0
75+
*
76+
* 3 -------------------- [ number ] -----------------------------------> 3
77+
*
78+
* 3 -------------------- [ SELECT STATEMENT ] -------------------------> 2
79+
*
80+
* 3 -------------------- [ ) ] ----------------------------------------> 2
81+
*
82+
* 2 ----------------------------------------------------------> Final state
83+
*/
84+
$state = 0;
85+
86+
++$list->idx; // Skipping `KILL`.
87+
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
88+
++$list->idx;
89+
for (; $list->idx < $list->count; ++$list->idx) {
90+
$token = $list->tokens[$list->idx];
91+
92+
if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
93+
continue;
94+
}
95+
96+
switch ($state) {
97+
case 0:
98+
$currIdx = $list->idx;
99+
$prev = $list->getPreviousOfType(Token::TYPE_KEYWORD);
100+
$list->idx = $currIdx;
101+
if ($token->type === Token::TYPE_NUMBER && is_int($token->value)) {
102+
$this->identifier = $token->value;
103+
$state = 2;
104+
} elseif ($token->type === Token::TYPE_OPERATOR && $token->value === '(') {
105+
$this->parenthesisUsed = true;
106+
$state = 3;
107+
} elseif ($prev && $token->value === 'ID' && $prev->value === 'QUERY') {
108+
$this->idKeywordUsed = true;
109+
$state = 0;
110+
} else {
111+
$parser->error('Unexpected token.', $token);
112+
break 2;
113+
}
114+
115+
break;
116+
117+
case 3:
118+
if ($token->type === Token::TYPE_KEYWORD && $token->value === 'SELECT') {
119+
$subList = new TokensList(array_slice($list->tokens, $list->idx - 1));
120+
$subParser = new Parser($subList);
121+
if ($subParser->errors !== []) {
122+
foreach ($subParser->errors as $error) {
123+
$parser->errors[] = $error;
124+
}
125+
126+
break;
127+
}
128+
129+
$this->identifier = $subParser->statements[0];
130+
$state = 2;
131+
} elseif ($token->type === Token::TYPE_OPERATOR && $token->value === ')') {
132+
$state = 2;
133+
} elseif ($token->type === Token::TYPE_NUMBER && is_int($token->value)) {
134+
$this->identifier = $token->value;
135+
$state = 3;
136+
} else {
137+
$parser->error('Unexpected token.', $token);
138+
break 2;
139+
}
140+
141+
break;
142+
}
143+
}
144+
145+
if ($state !== 2) {
146+
$token = $list->tokens[$list->idx];
147+
$parser->error('Unexpected end of the KILL statement.', $token);
148+
}
149+
150+
--$list->idx;
151+
}
152+
153+
/**
154+
* {@inheritdoc}
155+
*/
156+
public function build()
157+
{
158+
$ret = 'KILL';
159+
160+
if ($this->options !== null && $this->options->options !== []) {
161+
$ret .= ' ' . OptionsArray::build($this->options);
162+
}
163+
164+
if ($this->idKeywordUsed) {
165+
$ret .= ' ID';
166+
}
167+
168+
$identifier = (string) $this->identifier;
169+
if ($this->parenthesisUsed) {
170+
$ret .= ' (' . $identifier . ')';
171+
} else {
172+
$ret .= ' ' . $identifier;
173+
}
40174

41-
return trim('KILL' . $option . $expression);
175+
return $ret;
42176
}
43177
}

tests/Parser/KillStatementTest.php

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
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);
@@ -25,14 +23,18 @@ public static function killProvider(): array
2523
{
2624
return [
2725
['parser/parseKill'],
26+
['parser/parseKill2'],
27+
['parser/parseKill3'],
2828
['parser/parseKillConnection'],
2929
['parser/parseKillQuery'],
30+
['parser/parseKillErr1'],
31+
['parser/parseKillErr2'],
32+
['parser/parseKillErr3'],
33+
['parser/parseKillErr4'],
3034
];
3135
}
3236

33-
/**
34-
* @dataProvider buildKillProvider
35-
*/
37+
/** @dataProvider buildKillProvider */
3638
public function testBuildKill(string $sql): void
3739
{
3840
$parser = new Parser($sql);
@@ -51,9 +53,17 @@ public static function buildKillProvider(): array
5153
{
5254
return [
5355
['KILL (SELECT 3 + 4)'],
54-
['KILL QUERY 3'],
55-
['KILL CONNECTION 3'],
56-
['KILL'],
56+
['KILL QUERY 4'],
57+
['KILL CONNECTION 5'],
58+
['KILL 6'],
59+
['KILL QUERY (SELECT 7)'],
60+
['KILL SOFT QUERY (SELECT 8)'],
61+
['KILL HARD 9'],
62+
['KILL USER 10'],
63+
['KILL SOFT (SELECT 1)'],
64+
['KILL (2)'],
65+
['KILL QUERY ID (2)'],
66+
['KILL QUERY ID (SELECT ID FROM INFORMATION_SCHEMA.PROCESSLIST LIMIT 0, 1)'],
5767
];
5868
}
5969
}

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)