1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace PhpMyAdmin\SqlParser\Statements; |
||
6 | |||
7 | use PhpMyAdmin\SqlParser\Context; |
||
8 | use PhpMyAdmin\SqlParser\Parser; |
||
9 | use PhpMyAdmin\SqlParser\Parsers\OptionsArrays; |
||
10 | use PhpMyAdmin\SqlParser\Statement; |
||
11 | use PhpMyAdmin\SqlParser\TokensList; |
||
12 | use PhpMyAdmin\SqlParser\TokenType; |
||
13 | |||
14 | use function array_slice; |
||
15 | |||
16 | /** |
||
17 | * `EXPLAIN` statement. |
||
18 | */ |
||
19 | class ExplainStatement extends Statement |
||
20 | { |
||
21 | /** |
||
22 | * Options for `EXPLAIN` statements. |
||
23 | */ |
||
24 | private const OPTIONS = [ |
||
25 | |||
26 | 'EXTENDED' => 1, |
||
27 | 'PARTITIONS' => 1, |
||
28 | 'FORMAT' => [ |
||
29 | 1, |
||
30 | 'var', |
||
31 | ], |
||
32 | ]; |
||
33 | |||
34 | /** |
||
35 | * The parser of the statement to be explained |
||
36 | */ |
||
37 | public Parser|null $bodyParser = null; |
||
38 | |||
39 | /** |
||
40 | * The statement alias, could be any of the following: |
||
41 | * - {EXPLAIN | DESCRIBE | DESC} |
||
42 | * - {EXPLAIN | DESCRIBE | DESC} ANALYZE |
||
43 | * - ANALYZE |
||
44 | */ |
||
45 | public string $statementAlias; |
||
46 | |||
47 | /** |
||
48 | * The connection identifier, if used. |
||
49 | */ |
||
50 | public int|null $connectionId = null; |
||
51 | |||
52 | /** |
||
53 | * The explained database for the table's name, if used. |
||
54 | */ |
||
55 | public string|null $explainedDatabase = null; |
||
56 | |||
57 | /** |
||
58 | * The explained table's name, if used. |
||
59 | */ |
||
60 | public string|null $explainedTable = null; |
||
61 | |||
62 | /** |
||
63 | * The explained column's name, if used. |
||
64 | */ |
||
65 | public string|null $explainedColumn = null; |
||
66 | |||
67 | /** |
||
68 | * @param Parser $parser the instance that requests parsing |
||
69 | * @param TokensList $list the list of tokens to be parsed |
||
70 | */ |
||
71 | 44 | public function parse(Parser $parser, TokensList $list): void |
|
72 | { |
||
73 | /** |
||
74 | * The state of the parser. |
||
75 | * |
||
76 | * Below are the states of the parser. |
||
77 | * |
||
78 | * 0 -------------------[ EXPLAIN/EXPLAIN ANALYZE/ANALYZE ]-----------------------> 1 |
||
79 | * |
||
80 | * 0 ------------------------[ EXPLAIN/DESC/DESCRIBE ]----------------------------> 3 |
||
81 | * |
||
82 | * 1 ------------------------------[ OPTIONS ]------------------------------------> 2 |
||
83 | * |
||
84 | * 2 --------------[ tablename / STATEMENT / FOR CONNECTION ]---------------------> 2 |
||
85 | * |
||
86 | * 3 -----------------------------[ tablename ]-----------------------------------> 3 |
||
87 | */ |
||
88 | 44 | $state = 0; |
|
89 | |||
90 | /** |
||
91 | * To Differentiate between ANALYZE / EXPLAIN / EXPLAIN ANALYZE |
||
92 | * 0 -> ANALYZE ( used by mariaDB https://mariadb.com/kb/en/analyze-statement) |
||
93 | * 1 -> {EXPLAIN | DESCRIBE | DESC} |
||
94 | * 2 -> {EXPLAIN | DESCRIBE | DESC} ANALYZE |
||
95 | */ |
||
96 | 44 | $miniState = 0; |
|
97 | |||
98 | 44 | for (; $list->idx < $list->count; ++$list->idx) { |
|
99 | /** |
||
100 | * Token parsed at this moment. |
||
101 | */ |
||
102 | 44 | $token = $list->tokens[$list->idx]; |
|
103 | |||
104 | // End of statement. |
||
105 | 44 | if ($token->type === TokenType::Delimiter) { |
|
106 | 20 | --$list->idx; // Back up one token, no real reasons to document |
|
107 | 20 | break; |
|
108 | } |
||
109 | |||
110 | // Skipping whitespaces and comments. |
||
111 | 44 | if ($token->type === TokenType::Whitespace || $token->type === TokenType::Comment) { |
|
112 | 38 | continue; |
|
113 | } |
||
114 | |||
115 | 44 | if ($state === 0) { |
|
116 | 44 | if ($token->keyword === 'ANALYZE' && $miniState === 0) { |
|
117 | 10 | $state = 1; |
|
118 | 10 | $this->statementAlias = 'ANALYZE'; |
|
119 | } elseif ( |
||
120 | 36 | $token->keyword === 'EXPLAIN' |
|
121 | 20 | || $token->keyword === 'DESC' |
|
122 | 36 | || $token->keyword === 'DESCRIBE' |
|
123 | ) { |
||
124 | 36 | $this->statementAlias = $token->keyword; |
|
125 | |||
126 | 36 | $lastIdx = $list->idx; |
|
127 | 36 | $list->idx++; // Ignore the current token |
|
128 | 36 | $nextKeyword = $list->getNextOfType(TokenType::Keyword); |
|
129 | 36 | $list->idx = $lastIdx; |
|
130 | |||
131 | // There is no other keyword, we must be describing a table |
||
132 | 36 | if ($nextKeyword === null) { |
|
133 | 18 | $state = 3; |
|
134 | 18 | continue; |
|
135 | } |
||
136 | |||
137 | 20 | $miniState = 1; |
|
138 | |||
139 | 20 | $lastIdx = $list->idx; |
|
140 | 20 | $nextKeyword = $list->getNextOfTypeAndValue(TokenType::Keyword, 'ANALYZE'); |
|
141 | 20 | if ($nextKeyword && $nextKeyword->keyword !== null) { |
|
142 | 6 | $miniState = 2; |
|
143 | 6 | $this->statementAlias .= ' ANALYZE'; |
|
144 | } else { |
||
145 | 16 | $list->idx = $lastIdx; |
|
146 | } |
||
147 | |||
148 | 20 | $state = 1; |
|
149 | } |
||
150 | 40 | } elseif ($state === 1) { |
|
151 | // Parsing options. |
||
152 | 26 | $this->options = OptionsArrays::parse($parser, $list, self::OPTIONS); |
|
153 | 26 | $state = 2; |
|
154 | 40 | } elseif ($state === 2) { |
|
155 | 26 | $currIdx = $list->idx; |
|
156 | 26 | $list->idx++; // Ignore the current token |
|
157 | 26 | $nextToken = $list->getNext(); |
|
158 | 26 | $list->idx = $currIdx; |
|
159 | |||
160 | 26 | if ($token->keyword === 'FOR' && $nextToken->keyword === 'CONNECTION') { |
|
161 | 2 | $list->idx++; // Ignore the current token |
|
162 | 2 | $list->getNext(); // CONNECTION |
|
163 | 2 | $nextToken = $list->getNext(); // Identifier |
|
164 | 2 | $this->connectionId = $nextToken->value; |
|
165 | 2 | break; |
|
166 | } |
||
167 | |||
168 | if ( |
||
169 | 26 | $token->keyword !== 'SELECT' |
|
170 | 26 | && $token->keyword !== 'TABLE' |
|
171 | 26 | && $token->keyword !== 'INSERT' |
|
172 | 26 | && $token->keyword !== 'REPLACE' |
|
173 | 26 | && $token->keyword !== 'UPDATE' |
|
174 | 26 | && $token->keyword !== 'DELETE' |
|
175 | ) { |
||
176 | 6 | $parser->error('Unexpected token.', $token); |
|
177 | 6 | break; |
|
178 | } |
||
179 | |||
180 | // Index of the last parsed token by default would be the last token in the $list, because we're |
||
181 | // assuming that all remaining tokens at state 2, are related to the to-be-explained statement. |
||
182 | 20 | $idxOfLastParsedToken = $list->count - 1; |
|
183 | 20 | $subList = new TokensList(array_slice($list->tokens, $list->idx)); |
|
184 | |||
185 | 20 | $this->bodyParser = new Parser($subList); |
|
186 | 20 | if ($this->bodyParser->errors !== []) { |
|
187 | 4 | foreach ($this->bodyParser->errors as $error) { |
|
188 | 4 | $parser->errors[] = $error; |
|
189 | } |
||
190 | |||
191 | 4 | break; |
|
192 | } |
||
193 | |||
194 | 16 | $list->idx = $idxOfLastParsedToken; |
|
195 | 16 | break; |
|
196 | 16 | } elseif ($state === 3) { |
|
197 | 16 | if (($token->type === TokenType::Operator) && ($token->value === '.')) { |
|
198 | 4 | continue; |
|
199 | } |
||
200 | |||
201 | 16 | if ($this->explainedDatabase === null) { |
|
202 | 16 | $lastIdx = $list->idx; |
|
203 | 16 | $nextDot = $list->getNextOfTypeAndValue(TokenType::Operator, '.'); |
|
204 | 16 | $list->idx = $lastIdx; |
|
205 | 16 | if ($nextDot !== null) {// We found a dot, so it must be a db.table name format |
|
206 | 4 | $this->explainedDatabase = $token->value; |
|
207 | 4 | continue; |
|
208 | } |
||
209 | } |
||
210 | |||
211 | 16 | if ($this->explainedTable === null) { |
|
212 | 16 | $this->explainedTable = $token->value; |
|
213 | 16 | continue; |
|
214 | } |
||
215 | |||
216 | 10 | if ($this->explainedColumn === null && $token->value !== null) { |
|
217 | 10 | $this->explainedColumn = (string) $token->value; |
|
218 | } |
||
219 | } |
||
220 | } |
||
221 | |||
222 | 44 | if ($state !== 3 || $this->explainedTable !== null) { |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
223 | 42 | return; |
|
224 | } |
||
225 | |||
226 | // We reached end of the state 3 and no table name was found |
||
227 | /** Token parsed at this moment. */ |
||
228 | 2 | $token = $list->tokens[$list->idx]; |
|
229 | 2 | $parser->error('Expected a table name.', $token); |
|
230 | } |
||
231 | |||
232 | 2 | public function build(): string |
|
233 | { |
||
234 | 2 | $str = $this->statementAlias; |
|
235 | |||
236 | 2 | if ($this->options !== null) { |
|
237 | 2 | if ($this->options->options !== []) { |
|
238 | 2 | $str .= ' '; |
|
239 | } |
||
240 | |||
241 | 2 | $str .= $this->options->build() . ' '; |
|
242 | } |
||
243 | |||
244 | 2 | if ($this->options === null) { |
|
245 | 2 | $str .= ' '; |
|
246 | } |
||
247 | |||
248 | 2 | if ($this->bodyParser) { |
|
249 | 2 | foreach ($this->bodyParser->statements as $statement) { |
|
250 | 2 | $str .= $statement->build(); |
|
251 | } |
||
252 | 2 | } elseif ($this->connectionId) { |
|
0 ignored issues
–
show
The expression
$this->connectionId of type integer|null is loosely compared to true ; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||
253 | 2 | $str .= 'FOR CONNECTION ' . $this->connectionId; |
|
254 | } |
||
255 | |||
256 | 2 | if ($this->explainedDatabase !== null && $this->explainedTable !== null) { |
|
257 | 2 | $str .= Context::escape($this->explainedDatabase) . '.' . Context::escape($this->explainedTable); |
|
258 | 2 | } elseif ($this->explainedTable !== null) { |
|
259 | 2 | $str .= Context::escape($this->explainedTable); |
|
260 | } |
||
261 | |||
262 | 2 | if ($this->explainedColumn !== null) { |
|
263 | 2 | $str .= ' ' . Context::escape($this->explainedColumn); |
|
264 | } |
||
265 | |||
266 | 2 | return $str; |
|
267 | } |
||
268 | } |
||
269 |