phpmyadmin /
sql-parser
| 1 | <?php |
||
| 2 | |||
| 3 | declare(strict_types=1); |
||
| 4 | |||
| 5 | namespace PhpMyAdmin\SqlParser; |
||
| 6 | |||
| 7 | use Exception; |
||
| 8 | use PhpMyAdmin\SqlParser\Exceptions\ParserException; |
||
| 9 | use PhpMyAdmin\SqlParser\Statements\SelectStatement; |
||
| 10 | use PhpMyAdmin\SqlParser\Statements\TransactionStatement; |
||
| 11 | |||
| 12 | use function is_string; |
||
| 13 | use function strtoupper; |
||
| 14 | |||
| 15 | /** |
||
| 16 | * Defines the parser of the library. |
||
| 17 | * |
||
| 18 | * This is one of the most important components, along with the lexer. |
||
| 19 | * |
||
| 20 | * Takes multiple tokens (contained in a Lexer instance) as input and builds a parse tree. |
||
| 21 | */ |
||
| 22 | class Parser |
||
| 23 | { |
||
| 24 | /** |
||
| 25 | * Whether errors should throw exceptions or just be stored. |
||
| 26 | */ |
||
| 27 | private bool $strict = false; |
||
| 28 | |||
| 29 | /** |
||
| 30 | * List of errors that occurred during lexing. |
||
| 31 | * |
||
| 32 | * Usually, the lexing does not stop once an error occurred because that |
||
| 33 | * error might be false positive or a partial result (even a bad one) |
||
| 34 | * might be needed. |
||
| 35 | * |
||
| 36 | * @var Exception[] |
||
| 37 | */ |
||
| 38 | public array $errors = []; |
||
| 39 | |||
| 40 | /** |
||
| 41 | * Array of classes that are used in parsing the SQL statements. |
||
| 42 | * |
||
| 43 | * @psalm-var array<string, class-string<Statement>|''> |
||
| 44 | */ |
||
| 45 | public const STATEMENT_PARSERS = [ |
||
| 46 | // MySQL Utility Statements |
||
| 47 | 'DESCRIBE' => Statements\ExplainStatement::class, |
||
| 48 | 'DESC' => Statements\ExplainStatement::class, |
||
| 49 | 'EXPLAIN' => Statements\ExplainStatement::class, |
||
| 50 | 'FLUSH' => '', |
||
| 51 | 'GRANT' => '', |
||
| 52 | 'HELP' => '', |
||
| 53 | 'SET PASSWORD' => '', |
||
| 54 | 'STATUS' => '', |
||
| 55 | 'USE' => '', |
||
| 56 | |||
| 57 | // Table Maintenance Statements |
||
| 58 | // https://dev.mysql.com/doc/refman/5.7/en/table-maintenance-sql.html |
||
| 59 | 'ANALYZE' => Statements\AnalyzeStatement::class, |
||
| 60 | 'BACKUP' => Statements\BackupStatement::class, |
||
| 61 | 'CHECK' => Statements\CheckStatement::class, |
||
| 62 | 'CHECKSUM' => Statements\ChecksumStatement::class, |
||
| 63 | 'OPTIMIZE' => Statements\OptimizeStatement::class, |
||
| 64 | 'REPAIR' => Statements\RepairStatement::class, |
||
| 65 | 'RESTORE' => Statements\RestoreStatement::class, |
||
| 66 | |||
| 67 | // Database Administration Statements |
||
| 68 | // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-server-administration.html |
||
| 69 | 'SET' => Statements\SetStatement::class, |
||
| 70 | 'SHOW' => Statements\ShowStatement::class, |
||
| 71 | |||
| 72 | // Data Definition Statements. |
||
| 73 | // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-definition.html |
||
| 74 | 'ALTER' => Statements\AlterStatement::class, |
||
| 75 | 'CREATE' => Statements\CreateStatement::class, |
||
| 76 | 'DROP' => Statements\DropStatement::class, |
||
| 77 | 'RENAME' => Statements\RenameStatement::class, |
||
| 78 | 'TRUNCATE' => Statements\TruncateStatement::class, |
||
| 79 | |||
| 80 | // Data Manipulation Statements. |
||
| 81 | // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-data-manipulation.html |
||
| 82 | 'CALL' => Statements\CallStatement::class, |
||
| 83 | 'DELETE' => Statements\DeleteStatement::class, |
||
| 84 | 'DO' => '', |
||
| 85 | 'HANDLER' => '', |
||
| 86 | 'INSERT' => Statements\InsertStatement::class, |
||
| 87 | 'LOAD DATA' => Statements\LoadStatement::class, |
||
| 88 | 'REPLACE' => Statements\ReplaceStatement::class, |
||
| 89 | 'SELECT' => Statements\SelectStatement::class, |
||
| 90 | 'UPDATE' => Statements\UpdateStatement::class, |
||
| 91 | 'WITH' => Statements\WithStatement::class, |
||
| 92 | |||
| 93 | // Prepared Statements. |
||
| 94 | // https://dev.mysql.com/doc/refman/5.7/en/sql-syntax-prepared-statements.html |
||
| 95 | 'DEALLOCATE' => '', |
||
| 96 | 'EXECUTE' => '', |
||
| 97 | 'PREPARE' => '', |
||
| 98 | |||
| 99 | // Transactional and Locking Statements |
||
| 100 | // https://dev.mysql.com/doc/refman/5.7/en/commit.html |
||
| 101 | 'BEGIN' => Statements\TransactionStatement::class, |
||
| 102 | 'COMMIT' => Statements\TransactionStatement::class, |
||
| 103 | 'ROLLBACK' => Statements\TransactionStatement::class, |
||
| 104 | 'START TRANSACTION' => Statements\TransactionStatement::class, |
||
| 105 | |||
| 106 | 'PURGE' => Statements\PurgeStatement::class, |
||
| 107 | |||
| 108 | // Lock statements |
||
| 109 | // https://dev.mysql.com/doc/refman/5.7/en/lock-tables.html |
||
| 110 | 'LOCK' => Statements\LockStatement::class, |
||
| 111 | 'UNLOCK' => Statements\LockStatement::class, |
||
| 112 | ]; |
||
| 113 | |||
| 114 | /** |
||
| 115 | * Array of classes that are used in parsing SQL components. |
||
| 116 | */ |
||
| 117 | public const KEYWORD_PARSERS = [ |
||
| 118 | // This is not a proper keyword and was added here to help the |
||
| 119 | // builder. |
||
| 120 | '_OPTIONS' => [ |
||
| 121 | 'class' => Parsers\OptionsArrays::class, |
||
| 122 | 'field' => 'options', |
||
| 123 | ], |
||
| 124 | '_END_OPTIONS' => [ |
||
| 125 | 'class' => Parsers\OptionsArrays::class, |
||
| 126 | 'field' => 'endOptions', |
||
| 127 | ], |
||
| 128 | '_GROUP_OPTIONS' => [ |
||
| 129 | 'class' => Parsers\OptionsArrays::class, |
||
| 130 | 'field' => 'groupOptions', |
||
| 131 | ], |
||
| 132 | |||
| 133 | 'INTERSECT' => [ |
||
| 134 | 'class' => Parsers\UnionKeywords::class, |
||
| 135 | 'field' => 'union', |
||
| 136 | ], |
||
| 137 | 'EXCEPT' => [ |
||
| 138 | 'class' => Parsers\UnionKeywords::class, |
||
| 139 | 'field' => 'union', |
||
| 140 | ], |
||
| 141 | 'UNION' => [ |
||
| 142 | 'class' => Parsers\UnionKeywords::class, |
||
| 143 | 'field' => 'union', |
||
| 144 | ], |
||
| 145 | 'UNION ALL' => [ |
||
| 146 | 'class' => Parsers\UnionKeywords::class, |
||
| 147 | 'field' => 'union', |
||
| 148 | ], |
||
| 149 | 'UNION DISTINCT' => [ |
||
| 150 | 'class' => Parsers\UnionKeywords::class, |
||
| 151 | 'field' => 'union', |
||
| 152 | ], |
||
| 153 | |||
| 154 | // Actual clause parsers. |
||
| 155 | 'ALTER' => [ |
||
| 156 | 'class' => Parsers\Expressions::class, |
||
| 157 | 'field' => 'table', |
||
| 158 | 'options' => ['parseField' => 'table'], |
||
| 159 | ], |
||
| 160 | 'ANALYZE' => [ |
||
| 161 | 'class' => Parsers\ExpressionArray::class, |
||
| 162 | 'field' => 'tables', |
||
| 163 | 'options' => ['parseField' => 'table'], |
||
| 164 | ], |
||
| 165 | 'BACKUP' => [ |
||
| 166 | 'class' => Parsers\ExpressionArray::class, |
||
| 167 | 'field' => 'tables', |
||
| 168 | 'options' => ['parseField' => 'table'], |
||
| 169 | ], |
||
| 170 | 'CALL' => [ |
||
| 171 | 'class' => Parsers\FunctionCalls::class, |
||
| 172 | 'field' => 'call', |
||
| 173 | ], |
||
| 174 | 'CHECK' => [ |
||
| 175 | 'class' => Parsers\ExpressionArray::class, |
||
| 176 | 'field' => 'tables', |
||
| 177 | 'options' => ['parseField' => 'table'], |
||
| 178 | ], |
||
| 179 | 'CHECKSUM' => [ |
||
| 180 | 'class' => Parsers\ExpressionArray::class, |
||
| 181 | 'field' => 'tables', |
||
| 182 | 'options' => ['parseField' => 'table'], |
||
| 183 | ], |
||
| 184 | 'CROSS JOIN' => [ |
||
| 185 | 'class' => Parsers\JoinKeywords::class, |
||
| 186 | 'field' => 'join', |
||
| 187 | ], |
||
| 188 | 'DROP' => [ |
||
| 189 | 'class' => Parsers\ExpressionArray::class, |
||
| 190 | 'field' => 'fields', |
||
| 191 | 'options' => ['parseField' => 'table'], |
||
| 192 | ], |
||
| 193 | 'FORCE' => [ |
||
| 194 | 'class' => Parsers\IndexHints::class, |
||
| 195 | 'field' => 'indexHints', |
||
| 196 | ], |
||
| 197 | 'FROM' => [ |
||
| 198 | 'class' => Parsers\ExpressionArray::class, |
||
| 199 | 'field' => 'from', |
||
| 200 | 'options' => ['field' => 'table'], |
||
| 201 | ], |
||
| 202 | 'GROUP BY' => [ |
||
| 203 | 'class' => Parsers\GroupKeywords::class, |
||
| 204 | 'field' => 'group', |
||
| 205 | ], |
||
| 206 | 'HAVING' => [ |
||
| 207 | 'class' => Parsers\Conditions::class, |
||
| 208 | 'field' => 'having', |
||
| 209 | ], |
||
| 210 | 'IGNORE' => [ |
||
| 211 | 'class' => Parsers\IndexHints::class, |
||
| 212 | 'field' => 'indexHints', |
||
| 213 | ], |
||
| 214 | 'INTO' => [ |
||
| 215 | 'class' => Parsers\IntoKeywords::class, |
||
| 216 | 'field' => 'into', |
||
| 217 | ], |
||
| 218 | 'JOIN' => [ |
||
| 219 | 'class' => Parsers\JoinKeywords::class, |
||
| 220 | 'field' => 'join', |
||
| 221 | ], |
||
| 222 | 'LEFT JOIN' => [ |
||
| 223 | 'class' => Parsers\JoinKeywords::class, |
||
| 224 | 'field' => 'join', |
||
| 225 | ], |
||
| 226 | 'LEFT OUTER JOIN' => [ |
||
| 227 | 'class' => Parsers\JoinKeywords::class, |
||
| 228 | 'field' => 'join', |
||
| 229 | ], |
||
| 230 | 'ON' => [ |
||
| 231 | 'class' => Parsers\Expressions::class, |
||
| 232 | 'field' => 'table', |
||
| 233 | 'options' => ['parseField' => 'table'], |
||
| 234 | ], |
||
| 235 | 'RIGHT JOIN' => [ |
||
| 236 | 'class' => Parsers\JoinKeywords::class, |
||
| 237 | 'field' => 'join', |
||
| 238 | ], |
||
| 239 | 'RIGHT OUTER JOIN' => [ |
||
| 240 | 'class' => Parsers\JoinKeywords::class, |
||
| 241 | 'field' => 'join', |
||
| 242 | ], |
||
| 243 | 'INNER JOIN' => [ |
||
| 244 | 'class' => Parsers\JoinKeywords::class, |
||
| 245 | 'field' => 'join', |
||
| 246 | ], |
||
| 247 | 'FULL JOIN' => [ |
||
| 248 | 'class' => Parsers\JoinKeywords::class, |
||
| 249 | 'field' => 'join', |
||
| 250 | ], |
||
| 251 | 'FULL OUTER JOIN' => [ |
||
| 252 | 'class' => Parsers\JoinKeywords::class, |
||
| 253 | 'field' => 'join', |
||
| 254 | ], |
||
| 255 | 'NATURAL JOIN' => [ |
||
| 256 | 'class' => Parsers\JoinKeywords::class, |
||
| 257 | 'field' => 'join', |
||
| 258 | ], |
||
| 259 | 'NATURAL LEFT JOIN' => [ |
||
| 260 | 'class' => Parsers\JoinKeywords::class, |
||
| 261 | 'field' => 'join', |
||
| 262 | ], |
||
| 263 | 'NATURAL RIGHT JOIN' => [ |
||
| 264 | 'class' => Parsers\JoinKeywords::class, |
||
| 265 | 'field' => 'join', |
||
| 266 | ], |
||
| 267 | 'NATURAL LEFT OUTER JOIN' => [ |
||
| 268 | 'class' => Parsers\JoinKeywords::class, |
||
| 269 | 'field' => 'join', |
||
| 270 | ], |
||
| 271 | 'NATURAL RIGHT OUTER JOIN' => [ |
||
| 272 | 'class' => Parsers\JoinKeywords::class, |
||
| 273 | 'field' => 'join', |
||
| 274 | ], |
||
| 275 | 'STRAIGHT_JOIN' => [ |
||
| 276 | 'class' => Parsers\JoinKeywords::class, |
||
| 277 | 'field' => 'join', |
||
| 278 | ], |
||
| 279 | 'LIMIT' => [ |
||
| 280 | 'class' => Parsers\Limits::class, |
||
| 281 | 'field' => 'limit', |
||
| 282 | ], |
||
| 283 | 'OPTIMIZE' => [ |
||
| 284 | 'class' => Parsers\ExpressionArray::class, |
||
| 285 | 'field' => 'tables', |
||
| 286 | 'options' => ['parseField' => 'table'], |
||
| 287 | ], |
||
| 288 | 'ORDER BY' => [ |
||
| 289 | 'class' => Parsers\OrderKeywords::class, |
||
| 290 | 'field' => 'order', |
||
| 291 | ], |
||
| 292 | 'PARTITION' => [ |
||
| 293 | 'class' => Parsers\ArrayObjs::class, |
||
| 294 | 'field' => 'partition', |
||
| 295 | ], |
||
| 296 | 'PROCEDURE' => [ |
||
| 297 | 'class' => Parsers\FunctionCalls::class, |
||
| 298 | 'field' => 'procedure', |
||
| 299 | ], |
||
| 300 | 'RENAME' => [ |
||
| 301 | 'class' => Parsers\RenameOperations::class, |
||
| 302 | 'field' => 'renames', |
||
| 303 | ], |
||
| 304 | 'REPAIR' => [ |
||
| 305 | 'class' => Parsers\ExpressionArray::class, |
||
| 306 | 'field' => 'tables', |
||
| 307 | 'options' => ['parseField' => 'table'], |
||
| 308 | ], |
||
| 309 | 'RESTORE' => [ |
||
| 310 | 'class' => Parsers\ExpressionArray::class, |
||
| 311 | 'field' => 'tables', |
||
| 312 | 'options' => ['parseField' => 'table'], |
||
| 313 | ], |
||
| 314 | 'SET' => [ |
||
| 315 | 'class' => Parsers\SetOperations::class, |
||
| 316 | 'field' => 'set', |
||
| 317 | ], |
||
| 318 | 'SELECT' => [ |
||
| 319 | 'class' => Parsers\ExpressionArray::class, |
||
| 320 | 'field' => 'expr', |
||
| 321 | ], |
||
| 322 | 'TRUNCATE' => [ |
||
| 323 | 'class' => Parsers\Expressions::class, |
||
| 324 | 'field' => 'table', |
||
| 325 | 'options' => ['parseField' => 'table'], |
||
| 326 | ], |
||
| 327 | 'UPDATE' => [ |
||
| 328 | 'class' => Parsers\ExpressionArray::class, |
||
| 329 | 'field' => 'tables', |
||
| 330 | 'options' => ['parseField' => 'table'], |
||
| 331 | ], |
||
| 332 | 'USE' => [ |
||
| 333 | 'class' => Parsers\IndexHints::class, |
||
| 334 | 'field' => 'indexHints', |
||
| 335 | ], |
||
| 336 | 'VALUE' => [ |
||
| 337 | 'class' => Parsers\Array2d::class, |
||
| 338 | 'field' => 'values', |
||
| 339 | ], |
||
| 340 | 'VALUES' => [ |
||
| 341 | 'class' => Parsers\Array2d::class, |
||
| 342 | 'field' => 'values', |
||
| 343 | ], |
||
| 344 | 'WHERE' => [ |
||
| 345 | 'class' => Parsers\Conditions::class, |
||
| 346 | 'field' => 'where', |
||
| 347 | ], |
||
| 348 | ]; |
||
| 349 | |||
| 350 | /** |
||
| 351 | * The list of tokens that are parsed. |
||
| 352 | */ |
||
| 353 | public TokensList|null $list = null; |
||
| 354 | |||
| 355 | /** |
||
| 356 | * List of statements parsed. |
||
| 357 | * |
||
| 358 | * @var Statement[] |
||
| 359 | */ |
||
| 360 | public array $statements = []; |
||
| 361 | |||
| 362 | /** |
||
| 363 | * The number of opened brackets. |
||
| 364 | */ |
||
| 365 | public int $brackets = 0; |
||
| 366 | |||
| 367 | /** |
||
| 368 | * @param string|UtfString|TokensList|null $list the list of tokens to be parsed |
||
| 369 | * @param bool $strict whether strict mode should be enabled or not |
||
| 370 | */ |
||
| 371 | 1378 | public function __construct(string|UtfString|TokensList|null $list = null, bool $strict = false) |
|
| 372 | { |
||
| 373 | 1378 | if (Context::$keywords === []) { |
|
| 374 | Context::load(); |
||
| 375 | } |
||
| 376 | |||
| 377 | 1378 | if (is_string($list) || ($list instanceof UtfString)) { |
|
| 378 | 350 | $lexer = new Lexer($list, $strict); |
|
| 379 | 350 | $this->list = $lexer->list; |
|
| 380 | 1036 | } elseif ($list instanceof TokensList) { |
|
| 381 | 866 | $this->list = $list; |
|
| 382 | } |
||
| 383 | |||
| 384 | 1378 | $this->strict = $strict; |
|
| 385 | |||
| 386 | 1378 | if ($list === null) { |
|
| 387 | 170 | return; |
|
| 388 | } |
||
| 389 | |||
| 390 | 1208 | $this->parse(); |
|
| 391 | } |
||
| 392 | |||
| 393 | /** |
||
| 394 | * Builds the parse trees. |
||
| 395 | * |
||
| 396 | * @throws ParserException |
||
| 397 | */ |
||
| 398 | 1208 | public function parse(): void |
|
| 399 | { |
||
| 400 | /** |
||
| 401 | * Last transaction. |
||
| 402 | */ |
||
| 403 | 1208 | $lastTransaction = null; |
|
| 404 | |||
| 405 | /** |
||
| 406 | * Last parsed statement. |
||
| 407 | */ |
||
| 408 | 1208 | $lastStatement = null; |
|
| 409 | |||
| 410 | /** |
||
| 411 | * Union's type or false for no union. |
||
| 412 | */ |
||
| 413 | 1208 | $unionType = false; |
|
| 414 | |||
| 415 | /** |
||
| 416 | * The index of the last token from the last statement. |
||
| 417 | */ |
||
| 418 | 1208 | $prevLastIdx = -1; |
|
| 419 | |||
| 420 | /** |
||
| 421 | * The list of tokens. |
||
| 422 | */ |
||
| 423 | 1208 | $list = &$this->list; |
|
| 424 | |||
| 425 | 1208 | for (; $list->idx < $list->count; ++$list->idx) { |
|
| 426 | /** |
||
| 427 | * Token parsed at this moment. |
||
| 428 | */ |
||
| 429 | 1204 | $token = $list->tokens[$list->idx]; |
|
| 430 | |||
| 431 | // `DELIMITER` is not an actual statement and it requires |
||
| 432 | // special handling. |
||
| 433 | 1204 | if (($token->type === TokenType::None) && (strtoupper($token->token) === 'DELIMITER')) { |
|
| 434 | // Skipping to the end of this statement. |
||
| 435 | 24 | $list->getNextOfType(TokenType::Delimiter); |
|
| 436 | 24 | $prevLastIdx = $list->idx; |
|
| 437 | 24 | continue; |
|
| 438 | } |
||
| 439 | |||
| 440 | // Counting the brackets around statements. |
||
| 441 | 1204 | if ($token->value === '(') { |
|
| 442 | 24 | ++$this->brackets; |
|
| 443 | 24 | continue; |
|
| 444 | } |
||
| 445 | |||
| 446 | // Statements can start with keywords only. |
||
| 447 | // Comments, whitespaces, etc. are ignored. |
||
| 448 | 1204 | if ($token->type !== TokenType::Keyword) { |
|
| 449 | if ( |
||
| 450 | 994 | ($token->type !== TokenType::Comment) |
|
| 451 | 994 | && ($token->type !== TokenType::Whitespace) |
|
| 452 | 994 | && ($token->type !== TokenType::Operator) // `(` and `)` |
|
| 453 | 994 | && ($token->type !== TokenType::Delimiter) |
|
| 454 | ) { |
||
| 455 | 42 | $this->error('Unexpected beginning of statement.', $token); |
|
| 456 | } |
||
| 457 | |||
| 458 | 994 | continue; |
|
| 459 | } |
||
| 460 | |||
| 461 | if ( |
||
| 462 | 1200 | ($token->keyword === 'UNION') || |
|
| 463 | 1200 | ($token->keyword === 'UNION ALL') || |
|
| 464 | 1200 | ($token->keyword === 'UNION DISTINCT') || |
|
| 465 | 1200 | ($token->keyword === 'EXCEPT') || |
|
| 466 | 1200 | ($token->keyword === 'INTERSECT') |
|
| 467 | ) { |
||
| 468 | 38 | $unionType = $token->keyword; |
|
| 469 | 38 | continue; |
|
| 470 | } |
||
| 471 | |||
| 472 | 1200 | $lastIdx = $list->idx; |
|
| 473 | 1200 | $statementName = null; |
|
| 474 | |||
| 475 | 1200 | if ($token->keyword === 'ANALYZE') { |
|
| 476 | 16 | ++$list->idx; // Skip ANALYZE |
|
| 477 | |||
| 478 | 16 | $first = $list->getNextOfType(TokenType::Keyword); |
|
| 479 | 16 | $second = $list->getNextOfType(TokenType::Keyword); |
|
| 480 | |||
| 481 | // ANALYZE keyword can be an indication of two cases: |
||
| 482 | // 1 - ANALYZE TABLE statements, in both MariaDB and MySQL |
||
| 483 | // 2 - Explain statement, in case of MariaDB https://mariadb.com/kb/en/explain-analyze/ |
||
| 484 | // We need to point case 2 to use the EXPLAIN Parser. |
||
| 485 | 16 | $statementName = 'EXPLAIN'; |
|
| 486 | 16 | if (($first && $first->keyword === 'TABLE') || ($second && $second->keyword === 'TABLE')) { |
|
| 487 | 6 | $statementName = 'ANALYZE'; |
|
| 488 | } |
||
| 489 | |||
| 490 | 16 | $list->idx = $lastIdx; |
|
| 491 | 1190 | } elseif (empty(self::STATEMENT_PARSERS[$token->keyword])) { |
|
| 492 | // Checking if it is a known statement that can be parsed. |
||
| 493 | 80 | if (! isset(self::STATEMENT_PARSERS[$token->keyword])) { |
|
| 494 | // A statement is considered recognized if the parser |
||
| 495 | // is aware that it is a statement, but it does not have |
||
| 496 | // a parser for it yet. |
||
| 497 | 80 | $this->error('Unrecognized statement type.', $token); |
|
| 498 | } |
||
| 499 | |||
| 500 | // Skipping to the end of this statement. |
||
| 501 | 80 | $list->getNextOfType(TokenType::Delimiter); |
|
| 502 | 80 | $prevLastIdx = $list->idx; |
|
| 503 | 80 | continue; |
|
| 504 | } |
||
| 505 | |||
| 506 | /** |
||
| 507 | * The name of the class that is used for parsing. |
||
| 508 | */ |
||
| 509 | 1196 | $class = self::STATEMENT_PARSERS[$statementName ?? $token->keyword]; |
|
| 510 | |||
| 511 | /** |
||
| 512 | * Processed statement. |
||
| 513 | */ |
||
| 514 | 1196 | $statement = new $class($this, $this->list); |
|
| 515 | |||
| 516 | // The first token that is a part of this token is the next token |
||
| 517 | // unprocessed by the previous statement. |
||
| 518 | // There might be brackets around statements and this shouldn't |
||
| 519 | // affect the parser |
||
| 520 | 1196 | $statement->first = $prevLastIdx + 1; |
|
| 521 | |||
| 522 | // Storing the index of the last token parsed and updating the old |
||
| 523 | // index. |
||
| 524 | 1196 | $statement->last = $list->idx; |
|
| 525 | 1196 | $prevLastIdx = $list->idx; |
|
| 526 | |||
| 527 | // Handles unions. |
||
| 528 | if ( |
||
| 529 | 1196 | ! empty($unionType) |
|
| 530 | 1196 | && ($lastStatement instanceof SelectStatement) |
|
| 531 | 1196 | && ($statement instanceof SelectStatement) |
|
| 532 | ) { |
||
| 533 | /* |
||
| 534 | * This SELECT statement. |
||
| 535 | * |
||
| 536 | * @var SelectStatement $statement |
||
| 537 | */ |
||
| 538 | |||
| 539 | /* |
||
| 540 | * Last SELECT statement. |
||
| 541 | * |
||
| 542 | * @var SelectStatement $lastStatement |
||
| 543 | */ |
||
| 544 | 38 | $lastStatement->union[] = [ |
|
| 545 | 38 | $unionType, |
|
| 546 | 38 | $statement, |
|
| 547 | 38 | ]; |
|
| 548 | |||
| 549 | // if there are no no delimiting brackets, the `ORDER` and |
||
| 550 | // `LIMIT` keywords actually belong to the first statement. |
||
| 551 | 38 | $lastStatement->order = $statement->order; |
|
| 552 | 38 | $lastStatement->limit = $statement->limit; |
|
| 553 | 38 | $statement->order = []; |
|
| 554 | 38 | $statement->limit = null; |
|
| 555 | |||
| 556 | // The statement actually ends where the last statement in |
||
| 557 | // union ends. |
||
| 558 | 38 | $lastStatement->last = $statement->last; |
|
| 559 | |||
| 560 | 38 | $unionType = false; |
|
| 561 | |||
| 562 | // Validate clause order |
||
| 563 | 38 | $statement->validateClauseOrder($this, $list); |
|
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 564 | 38 | continue; |
|
| 565 | } |
||
| 566 | |||
| 567 | // Handles transactions. |
||
| 568 | 1196 | if ($statement instanceof TransactionStatement) { |
|
| 569 | /* |
||
| 570 | * @var TransactionStatement |
||
| 571 | */ |
||
| 572 | 26 | if ($statement->type === TransactionStatement::TYPE_BEGIN) { |
|
| 573 | 22 | $lastTransaction = $statement; |
|
| 574 | 22 | $this->statements[] = $statement; |
|
| 575 | 22 | } elseif ($statement->type === TransactionStatement::TYPE_END) { |
|
| 576 | 20 | if ($lastTransaction === null) { |
|
| 577 | // Even though an error occurred, the query is being |
||
| 578 | // saved. |
||
| 579 | 6 | $this->statements[] = $statement; |
|
| 580 | 6 | $this->error('No transaction was previously started.', $token); |
|
| 581 | } else { |
||
| 582 | 18 | $lastTransaction->end = $statement; |
|
| 583 | } |
||
| 584 | |||
| 585 | 20 | $lastTransaction = null; |
|
| 586 | } |
||
| 587 | |||
| 588 | // Validate clause order |
||
| 589 | 26 | $statement->validateClauseOrder($this, $list); |
|
| 590 | 26 | continue; |
|
| 591 | } |
||
| 592 | |||
| 593 | // Validate clause order |
||
| 594 | 1194 | $statement->validateClauseOrder($this, $list); |
|
| 595 | |||
| 596 | // Finally, storing the statement. |
||
| 597 | 1194 | if ($lastTransaction !== null) { |
|
| 598 | 22 | $lastTransaction->statements[] = $statement; |
|
| 599 | } else { |
||
| 600 | 1178 | $this->statements[] = $statement; |
|
| 601 | } |
||
| 602 | |||
| 603 | 1194 | $lastStatement = $statement; |
|
| 604 | } |
||
| 605 | } |
||
| 606 | |||
| 607 | /** |
||
| 608 | * Creates a new error log. |
||
| 609 | * |
||
| 610 | * @param string $msg the error message |
||
| 611 | * @param Token $token the token that produced the error |
||
| 612 | * @param int $code the code of the error |
||
| 613 | * |
||
| 614 | * @throws ParserException throws the exception, if strict mode is enabled. |
||
| 615 | */ |
||
| 616 | 316 | public function error(string $msg, Token|null $token = null, int $code = 0): void |
|
| 617 | { |
||
| 618 | 316 | $error = new ParserException( |
|
| 619 | 316 | Translator::gettext($msg), |
|
| 620 | 316 | $token, |
|
| 621 | 316 | $code, |
|
| 622 | 316 | ); |
|
| 623 | |||
| 624 | 316 | if ($this->strict) { |
|
| 625 | 2 | throw $error; |
|
| 626 | } |
||
| 627 | |||
| 628 | 314 | $this->errors[] = $error; |
|
| 629 | } |
||
| 630 | } |
||
| 631 |