phpmyadmin /
sql-parser
| 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
Loading history...
|
|||
| 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
Loading history...
|
|||
| 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 |