Skip to content

Commit 158de19

Browse files
committed
Merge #389 - Implement the EXPLAIN Parser
Pull-request: #389 Ref: phpmyadmin/phpmyadmin#11867 Signed-off-by: William Desportes <williamdes@wdes.fr>
2 parents 6b91b64 + 5689daf commit 158de19

23 files changed

Lines changed: 7210 additions & 91 deletions

src/Statements/ExplainStatement.php

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,204 @@
44

55
namespace PhpMyAdmin\SqlParser\Statements;
66

7+
use PhpMyAdmin\SqlParser\Components\OptionsArray;
8+
use PhpMyAdmin\SqlParser\Parser;
9+
use PhpMyAdmin\SqlParser\Statement;
10+
use PhpMyAdmin\SqlParser\Token;
11+
use PhpMyAdmin\SqlParser\TokensList;
12+
13+
use function array_slice;
14+
use function count;
15+
716
/**
817
* `EXPLAIN` statement.
918
*/
10-
class ExplainStatement extends NotImplementedStatement
19+
class ExplainStatement extends Statement
1120
{
21+
/**
22+
* Options for `EXPLAIN` statements.
23+
*
24+
* @var array<string, int|array<int, int|string>>
25+
* @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
26+
*/
27+
public static $OPTIONS = [
28+
29+
'EXTENDED' => 1,
30+
'PARTITIONS' => 1,
31+
'FORMAT' => [
32+
1,
33+
'var',
34+
],
35+
];
36+
37+
/**
38+
* The parser of the statement to be explained
39+
*
40+
* @var Parser|null
41+
*/
42+
public $bodyParser = null;
43+
44+
/**
45+
* The statement alias, could be any of the following:
46+
* - {EXPLAIN | DESCRIBE | DESC}
47+
* - {EXPLAIN | DESCRIBE | DESC} ANALYZE
48+
* - ANALYZE
49+
*
50+
* @var string
51+
*/
52+
public $statemenetAlias;
53+
54+
/**
55+
* The connection identifier, if used.
56+
*
57+
* @var int|null
58+
*/
59+
public $connectionId = null;
60+
61+
/**
62+
* The explained table's name, if used.
63+
*
64+
* @var string|null
65+
*/
66+
public $explainedTable = null;
67+
68+
/**
69+
* @param Parser $parser the instance that requests parsing
70+
* @param TokensList $list the list of tokens to be parsed
71+
*/
72+
public function parse(Parser $parser, TokensList $list)
73+
{
74+
/**
75+
* The state of the parser.
76+
*
77+
* Below are the states of the parser.
78+
*
79+
* 0 -------------------[ EXPLAIN/EXPLAIN ANALYZE/ANALYZE ]-----------------------> 1
80+
*
81+
* 1 ------------------------------[ OPTIONS ]------------------------------------> 2
82+
*
83+
* 2 --------------[ tablename / STATEMENT / FOR CONNECTION ]---------------------> 2
84+
*
85+
* @var int
86+
*/
87+
$state = 0;
88+
89+
/**
90+
* To Differentiate between ANALYZE / EXPLAIN / EXPLAIN ANALYZE
91+
* 0 -> ANALYZE ( used by mariaDB https://mariadb.com/kb/en/analyze-statement)
92+
* 1 -> {EXPLAIN | DESCRIBE | DESC}
93+
* 2 -> {EXPLAIN | DESCRIBE | DESC} ANALYZE
94+
*/
95+
$miniState = 0;
96+
97+
for (; $list->idx < $list->count; ++$list->idx) {
98+
/**
99+
* Token parsed at this moment.
100+
*/
101+
$token = $list->tokens[$list->idx];
102+
103+
// Skipping whitespaces and comments.
104+
if ($token->type === Token::TYPE_WHITESPACE || $token->type === Token::TYPE_COMMENT) {
105+
continue;
106+
}
107+
108+
if ($state === 0) {
109+
if ($token->keyword === 'ANALYZE' && $miniState === 0) {
110+
$state = 1;
111+
$this->statemenetAlias = 'ANALYZE';
112+
} elseif (
113+
$token->keyword === 'EXPLAIN'
114+
|| $token->keyword === 'DESC'
115+
|| $token->keyword === 'DESCRIBE'
116+
) {
117+
$miniState = 1;
118+
$this->statemenetAlias = $token->keyword;
119+
120+
$lastIdx = $list->idx;
121+
$nextKeyword = $list->getNextOfTypeAndValue(Token::TYPE_KEYWORD, 'ANALYZE');
122+
if ($nextKeyword && $nextKeyword->keyword !== null) {
123+
$miniState = 2;
124+
$this->statemenetAlias .= ' ANALYZE';
125+
} else {
126+
$list->idx = $lastIdx;
127+
}
128+
129+
$state = 1;
130+
}
131+
} elseif ($state === 1) {
132+
// Parsing options.
133+
$this->options = OptionsArray::parse($parser, $list, static::$OPTIONS);
134+
$state = 2;
135+
} elseif ($state === 2) {
136+
$currIdx = $list->idx;
137+
$currToken = $list->getNext();
138+
$nextToken = $list->getNext();
139+
$list->idx = $currIdx;
140+
141+
if ($token->keyword === 'FOR' && $nextToken->keyword === 'CONNECTION') {
142+
$forToken = $list->getNext(); // FOR
143+
$connectionToken = $list->getNext(); // CONNECTION
144+
$nextToken = $list->getNext(); // Identifier
145+
$this->connectionId = $nextToken->value;
146+
break;
147+
}
148+
149+
// To support EXPLAIN tablename
150+
if ($token->type === Token::TYPE_NONE) {
151+
$this->explainedTable = $token->value;
152+
break;
153+
}
154+
155+
if (
156+
$token->keyword !== 'SELECT'
157+
&& $token->keyword !== 'INSERT'
158+
&& $token->keyword !== 'UPDATE'
159+
&& $token->keyword !== 'DELETE'
160+
) {
161+
$parser->error('Unexpected token.', $token);
162+
break;
163+
}
164+
165+
// Index of the last parsed token by default would be the last token in the $list, because we're
166+
// assuming that all remaining tokens at state 2, are related to the to-be-explained statement.
167+
$idxOfLastParsedToken = $list->count - 1;
168+
$subList = new TokensList(array_slice($list->tokens, $list->idx));
169+
170+
$this->bodyParser = new Parser($subList);
171+
if (count($this->bodyParser->errors)) {
172+
foreach ($this->bodyParser->errors as $error) {
173+
$parser->errors[] = $error;
174+
}
175+
176+
break;
177+
}
178+
179+
$list->idx = $idxOfLastParsedToken;
180+
break;
181+
}
182+
}
183+
}
184+
185+
public function build(): string
186+
{
187+
$str = $this->statemenetAlias;
188+
189+
if (count($this->options->options)) {
190+
$str .= ' ';
191+
}
192+
193+
$str .= OptionsArray::build($this->options) . ' ';
194+
195+
if ($this->bodyParser) {
196+
foreach ($this->bodyParser->statements as $statement) {
197+
$str .= $statement->build();
198+
}
199+
} elseif ($this->connectionId) {
200+
$str .= 'FOR CONNECTION ' . $this->connectionId;
201+
} elseif ($this->explainedTable) {
202+
$str .= $this->explainedTable;
203+
}
204+
205+
return $str;
206+
}
12207
}

tests/Builder/ExplainStatementTest.php

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,68 @@
99

1010
class ExplainStatementTest extends TestCase
1111
{
12-
public function testBuilderView(): void
12+
public function testBuilder(): void
1313
{
14+
/* Assertion 1 */
1415
$query = 'EXPLAIN SELECT * FROM test;';
16+
$parser = new Parser($query);
17+
$stmt = $parser->statements[0];
18+
$this->assertEquals(
19+
'EXPLAIN SELECT * FROM test',
20+
$stmt->build()
21+
);
22+
23+
/* Assertion 2 */
24+
$query = 'EXPLAIN ANALYZE SELECT * FROM tablename;';
25+
$parser = new Parser($query);
26+
$stmt = $parser->statements[0];
27+
$this->assertEquals(
28+
'EXPLAIN ANALYZE SELECT * FROM tablename',
29+
$stmt->build()
30+
);
31+
32+
/* Assertion 3 */
33+
$query = 'DESC ANALYZE SELECT * FROM tablename;';
34+
$parser = new Parser($query);
35+
$stmt = $parser->statements[0];
36+
$this->assertEquals(
37+
'DESC ANALYZE SELECT * FROM tablename',
38+
$stmt->build()
39+
);
1540

41+
/* Assertion 4 */
42+
$query = 'ANALYZE SELECT * FROM tablename;';
1643
$parser = new Parser($query);
1744
$stmt = $parser->statements[0];
45+
$this->assertEquals(
46+
'ANALYZE SELECT * FROM tablename',
47+
$stmt->build()
48+
);
49+
50+
/* Assertion 5 */
51+
$query = 'DESCRIBE tablename;';
52+
$parser = new Parser($query);
53+
$stmt = $parser->statements[0];
54+
$this->assertEquals(
55+
'DESCRIBE tablename',
56+
$stmt->build()
57+
);
1858

59+
/* Assertion 6 */
60+
$query = 'DESC FOR CONNECTION 458';
61+
$parser = new Parser($query);
62+
$stmt = $parser->statements[0];
63+
$this->assertEquals(
64+
'DESC FOR CONNECTION 458',
65+
$stmt->build()
66+
);
67+
68+
/* Assertion 6 */
69+
$query = 'EXPLAIN FORMAT=TREE SELECT * FROM db;';
70+
$parser = new Parser($query);
71+
$stmt = $parser->statements[0];
1972
$this->assertEquals(
20-
' EXPLAIN SELECT * FROM test',
73+
'EXPLAIN FORMAT=TREE SELECT * FROM db',
2174
$stmt->build()
2275
);
2376
}

tests/Misc/BugsTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public function bugProvider(): array
2929
['bugs/pma11800'],
3030
['bugs/pma11836'],
3131
['bugs/pma11843'],
32-
['bugs/pma11867'],
3332
['bugs/pma11879'],
3433
];
3534
}

tests/Parser/ExplainStatementTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ public function explainProvider(): array
2424
return [
2525
['parser/parseExplain'],
2626
['parser/parseExplain1'],
27+
['parser/parseExplain2'],
28+
['parser/parseExplain3'],
29+
['parser/parseExplain4'],
30+
['parser/parseExplainErr'],
31+
['parser/parseExplainErr1'],
32+
['parser/parseExplainErr2'],
33+
['parser/parseExplainErr3'],
34+
['parser/parseExplainErr4'],
2735
];
2836
}
2937
}

tests/data/bugs/pma11867.in

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)