1<?php 2/** 3 * Statement utilities. 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin\SqlParser\Utils; 9 10use PhpMyAdmin\SqlParser\Components\Expression; 11use PhpMyAdmin\SqlParser\Lexer; 12use PhpMyAdmin\SqlParser\Parser; 13use PhpMyAdmin\SqlParser\Statement; 14use PhpMyAdmin\SqlParser\Statements\AlterStatement; 15use PhpMyAdmin\SqlParser\Statements\AnalyzeStatement; 16use PhpMyAdmin\SqlParser\Statements\CallStatement; 17use PhpMyAdmin\SqlParser\Statements\CheckStatement; 18use PhpMyAdmin\SqlParser\Statements\ChecksumStatement; 19use PhpMyAdmin\SqlParser\Statements\CreateStatement; 20use PhpMyAdmin\SqlParser\Statements\DeleteStatement; 21use PhpMyAdmin\SqlParser\Statements\DropStatement; 22use PhpMyAdmin\SqlParser\Statements\ExplainStatement; 23use PhpMyAdmin\SqlParser\Statements\InsertStatement; 24use PhpMyAdmin\SqlParser\Statements\LoadStatement; 25use PhpMyAdmin\SqlParser\Statements\OptimizeStatement; 26use PhpMyAdmin\SqlParser\Statements\RenameStatement; 27use PhpMyAdmin\SqlParser\Statements\RepairStatement; 28use PhpMyAdmin\SqlParser\Statements\ReplaceStatement; 29use PhpMyAdmin\SqlParser\Statements\SelectStatement; 30use PhpMyAdmin\SqlParser\Statements\SetStatement; 31use PhpMyAdmin\SqlParser\Statements\ShowStatement; 32use PhpMyAdmin\SqlParser\Statements\TruncateStatement; 33use PhpMyAdmin\SqlParser\Statements\UpdateStatement; 34use PhpMyAdmin\SqlParser\Token; 35use PhpMyAdmin\SqlParser\TokensList; 36 37use function array_flip; 38use function array_keys; 39use function count; 40use function in_array; 41use function is_string; 42use function trim; 43 44/** 45 * Statement utilities. 46 */ 47class Query 48{ 49 /** 50 * Functions that set the flag `is_func`. 51 * 52 * @var string[] 53 */ 54 public static $FUNCTIONS = [ 55 'SUM', 56 'AVG', 57 'STD', 58 'STDDEV', 59 'MIN', 60 'MAX', 61 'BIT_OR', 62 'BIT_AND', 63 ]; 64 65 /** @var array<string,false> */ 66 public static $ALLFLAGS = [ 67 /* 68 * select ... DISTINCT ... 69 */ 70 'distinct' => false, 71 72 /* 73 * drop ... DATABASE ... 74 */ 75 'drop_database' => false, 76 77 /* 78 * ... GROUP BY ... 79 */ 80 'group' => false, 81 82 /* 83 * ... HAVING ... 84 */ 85 'having' => false, 86 87 /* 88 * INSERT ... 89 * or 90 * REPLACE ... 91 * or 92 * DELETE ... 93 */ 94 'is_affected' => false, 95 96 /* 97 * select ... PROCEDURE ANALYSE( ... ) ... 98 */ 99 'is_analyse' => false, 100 101 /* 102 * select COUNT( ... ) ... 103 */ 104 'is_count' => false, 105 106 /* 107 * DELETE ... 108 */ 109 'is_delete' => false, // @deprecated; use `querytype` 110 111 /* 112 * EXPLAIN ... 113 */ 114 'is_explain' => false, // @deprecated; use `querytype` 115 116 /* 117 * select ... INTO OUTFILE ... 118 */ 119 'is_export' => false, 120 121 /* 122 * select FUNC( ... ) ... 123 */ 124 'is_func' => false, 125 126 /* 127 * select ... GROUP BY ... 128 * or 129 * select ... HAVING ... 130 */ 131 'is_group' => false, 132 133 /* 134 * INSERT ... 135 * or 136 * REPLACE ... 137 * or 138 * LOAD DATA ... 139 */ 140 'is_insert' => false, 141 142 /* 143 * ANALYZE ... 144 * or 145 * CHECK ... 146 * or 147 * CHECKSUM ... 148 * or 149 * OPTIMIZE ... 150 * or 151 * REPAIR ... 152 */ 153 'is_maint' => false, 154 155 /* 156 * CALL ... 157 */ 158 'is_procedure' => false, 159 160 /* 161 * REPLACE ... 162 */ 163 'is_replace' => false, // @deprecated; use `querytype` 164 165 /* 166 * SELECT ... 167 */ 168 'is_select' => false, // @deprecated; use `querytype` 169 170 /* 171 * SHOW ... 172 */ 173 'is_show' => false, // @deprecated; use `querytype` 174 175 /* 176 * Contains a subquery. 177 */ 178 'is_subquery' => false, 179 180 /* 181 * ... JOIN ... 182 */ 183 'join' => false, 184 185 /* 186 * ... LIMIT ... 187 */ 188 'limit' => false, 189 190 /* 191 * TODO 192 */ 193 'offset' => false, 194 195 /* 196 * ... ORDER ... 197 */ 198 'order' => false, 199 200 /* 201 * The type of the query (which is usually the first keyword of 202 * the statement). 203 */ 204 'querytype' => false, 205 206 /* 207 * Whether a page reload is required. 208 */ 209 'reload' => false, 210 211 /* 212 * SELECT ... FROM ... 213 */ 214 'select_from' => false, 215 216 /* 217 * ... UNION ... 218 */ 219 'union' => false, 220 ]; 221 222 /** 223 * Gets an array with flags select statement has. 224 * 225 * @param SelectStatement $statement the statement to be processed 226 * @param array $flags flags set so far 227 * 228 * @return array 229 */ 230 private static function getFlagsSelect($statement, $flags) 231 { 232 $flags['querytype'] = 'SELECT'; 233 $flags['is_select'] = true; 234 235 if (! empty($statement->from)) { 236 $flags['select_from'] = true; 237 } 238 239 if ($statement->options->has('DISTINCT')) { 240 $flags['distinct'] = true; 241 } 242 243 if (! empty($statement->group) || ! empty($statement->having)) { 244 $flags['is_group'] = true; 245 } 246 247 if (! empty($statement->into) && ($statement->into->type === 'OUTFILE')) { 248 $flags['is_export'] = true; 249 } 250 251 $expressions = $statement->expr; 252 if (! empty($statement->join)) { 253 foreach ($statement->join as $join) { 254 $expressions[] = $join->expr; 255 } 256 } 257 258 foreach ($expressions as $expr) { 259 if (! empty($expr->function)) { 260 if ($expr->function === 'COUNT') { 261 $flags['is_count'] = true; 262 } elseif (in_array($expr->function, static::$FUNCTIONS)) { 263 $flags['is_func'] = true; 264 } 265 } 266 267 if (empty($expr->subquery)) { 268 continue; 269 } 270 271 $flags['is_subquery'] = true; 272 } 273 274 if (! empty($statement->procedure) && ($statement->procedure->name === 'ANALYSE')) { 275 $flags['is_analyse'] = true; 276 } 277 278 if (! empty($statement->group)) { 279 $flags['group'] = true; 280 } 281 282 if (! empty($statement->having)) { 283 $flags['having'] = true; 284 } 285 286 if (! empty($statement->union)) { 287 $flags['union'] = true; 288 } 289 290 if (! empty($statement->join)) { 291 $flags['join'] = true; 292 } 293 294 return $flags; 295 } 296 297 /** 298 * Gets an array with flags this statement has. 299 * 300 * @param Statement|null $statement the statement to be processed 301 * @param bool $all if `false`, false values will not be included 302 * 303 * @return array 304 */ 305 public static function getFlags($statement, $all = false) 306 { 307 $flags = ['querytype' => false]; 308 if ($all) { 309 $flags = self::$ALLFLAGS; 310 } 311 312 if ($statement instanceof AlterStatement) { 313 $flags['querytype'] = 'ALTER'; 314 $flags['reload'] = true; 315 } elseif ($statement instanceof CreateStatement) { 316 $flags['querytype'] = 'CREATE'; 317 $flags['reload'] = true; 318 } elseif ($statement instanceof AnalyzeStatement) { 319 $flags['querytype'] = 'ANALYZE'; 320 $flags['is_maint'] = true; 321 } elseif ($statement instanceof CheckStatement) { 322 $flags['querytype'] = 'CHECK'; 323 $flags['is_maint'] = true; 324 } elseif ($statement instanceof ChecksumStatement) { 325 $flags['querytype'] = 'CHECKSUM'; 326 $flags['is_maint'] = true; 327 } elseif ($statement instanceof OptimizeStatement) { 328 $flags['querytype'] = 'OPTIMIZE'; 329 $flags['is_maint'] = true; 330 } elseif ($statement instanceof RepairStatement) { 331 $flags['querytype'] = 'REPAIR'; 332 $flags['is_maint'] = true; 333 } elseif ($statement instanceof CallStatement) { 334 $flags['querytype'] = 'CALL'; 335 $flags['is_procedure'] = true; 336 } elseif ($statement instanceof DeleteStatement) { 337 $flags['querytype'] = 'DELETE'; 338 $flags['is_delete'] = true; 339 $flags['is_affected'] = true; 340 } elseif ($statement instanceof DropStatement) { 341 $flags['querytype'] = 'DROP'; 342 $flags['reload'] = true; 343 344 if ($statement->options->has('DATABASE') || $statement->options->has('SCHEMA')) { 345 $flags['drop_database'] = true; 346 } 347 } elseif ($statement instanceof ExplainStatement) { 348 $flags['querytype'] = 'EXPLAIN'; 349 $flags['is_explain'] = true; 350 } elseif ($statement instanceof InsertStatement) { 351 $flags['querytype'] = 'INSERT'; 352 $flags['is_affected'] = true; 353 $flags['is_insert'] = true; 354 } elseif ($statement instanceof LoadStatement) { 355 $flags['querytype'] = 'LOAD'; 356 $flags['is_affected'] = true; 357 $flags['is_insert'] = true; 358 } elseif ($statement instanceof ReplaceStatement) { 359 $flags['querytype'] = 'REPLACE'; 360 $flags['is_affected'] = true; 361 $flags['is_replace'] = true; 362 $flags['is_insert'] = true; 363 } elseif ($statement instanceof SelectStatement) { 364 $flags = self::getFlagsSelect($statement, $flags); 365 } elseif ($statement instanceof ShowStatement) { 366 $flags['querytype'] = 'SHOW'; 367 $flags['is_show'] = true; 368 } elseif ($statement instanceof UpdateStatement) { 369 $flags['querytype'] = 'UPDATE'; 370 $flags['is_affected'] = true; 371 } elseif ($statement instanceof SetStatement) { 372 $flags['querytype'] = 'SET'; 373 } 374 375 if ( 376 ($statement instanceof SelectStatement) 377 || ($statement instanceof UpdateStatement) 378 || ($statement instanceof DeleteStatement) 379 ) { 380 if (! empty($statement->limit)) { 381 $flags['limit'] = true; 382 } 383 384 if (! empty($statement->order)) { 385 $flags['order'] = true; 386 } 387 } 388 389 return $flags; 390 } 391 392 /** 393 * Parses a query and gets all information about it. 394 * 395 * @param string $query the query to be parsed 396 * 397 * @return array The array returned is the one returned by 398 * `static::getFlags()`, with the following keys added: 399 * - parser - the parser used to analyze the query; 400 * - statement - the first statement resulted from parsing; 401 * - select_tables - the real name of the tables selected; 402 * if there are no table names in the `SELECT` 403 * expressions, the table names are fetched from the 404 * `FROM` expressions 405 * - select_expr - selected expressions 406 */ 407 public static function getAll($query) 408 { 409 $parser = new Parser($query); 410 411 if (empty($parser->statements[0])) { 412 return static::getFlags(null, true); 413 } 414 415 $statement = $parser->statements[0]; 416 417 $ret = static::getFlags($statement, true); 418 419 $ret['parser'] = $parser; 420 $ret['statement'] = $statement; 421 422 if ($statement instanceof SelectStatement) { 423 $ret['select_tables'] = []; 424 $ret['select_expr'] = []; 425 426 // Finding tables' aliases and their associated real names. 427 $tableAliases = []; 428 foreach ($statement->from as $expr) { 429 if (! isset($expr->table, $expr->alias) || ($expr->table === '') || ($expr->alias === '')) { 430 continue; 431 } 432 433 $tableAliases[$expr->alias] = [ 434 $expr->table, 435 $expr->database ?? null, 436 ]; 437 } 438 439 // Trying to find selected tables only from the select expression. 440 // Sometimes, this is not possible because the tables aren't defined 441 // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film). 442 foreach ($statement->expr as $expr) { 443 if (isset($expr->table) && ($expr->table !== '')) { 444 if (isset($tableAliases[$expr->table])) { 445 $arr = $tableAliases[$expr->table]; 446 } else { 447 $arr = [ 448 $expr->table, 449 isset($expr->database) && ($expr->database !== '') ? 450 $expr->database : null, 451 ]; 452 } 453 454 if (! in_array($arr, $ret['select_tables'])) { 455 $ret['select_tables'][] = $arr; 456 } 457 } else { 458 $ret['select_expr'][] = $expr->expr; 459 } 460 } 461 462 // If no tables names were found in the SELECT clause or if there 463 // are expressions like * or COUNT(*), etc. tables names should be 464 // extracted from the FROM clause. 465 if (empty($ret['select_tables'])) { 466 foreach ($statement->from as $expr) { 467 if (! isset($expr->table) || ($expr->table === '')) { 468 continue; 469 } 470 471 $arr = [ 472 $expr->table, 473 isset($expr->database) && ($expr->database !== '') ? 474 $expr->database : null, 475 ]; 476 if (in_array($arr, $ret['select_tables'])) { 477 continue; 478 } 479 480 $ret['select_tables'][] = $arr; 481 } 482 } 483 } 484 485 return $ret; 486 } 487 488 /** 489 * Gets a list of all tables used in this statement. 490 * 491 * @param Statement $statement statement to be scanned 492 * 493 * @return array 494 */ 495 public static function getTables($statement) 496 { 497 $expressions = []; 498 499 if (($statement instanceof InsertStatement) || ($statement instanceof ReplaceStatement)) { 500 $expressions = [$statement->into->dest]; 501 } elseif ($statement instanceof UpdateStatement) { 502 $expressions = $statement->tables; 503 } elseif (($statement instanceof SelectStatement) || ($statement instanceof DeleteStatement)) { 504 $expressions = $statement->from; 505 } elseif (($statement instanceof AlterStatement) || ($statement instanceof TruncateStatement)) { 506 $expressions = [$statement->table]; 507 } elseif ($statement instanceof DropStatement) { 508 if (! $statement->options->has('TABLE')) { 509 // No tables are dropped. 510 return []; 511 } 512 513 $expressions = $statement->fields; 514 } elseif ($statement instanceof RenameStatement) { 515 foreach ($statement->renames as $rename) { 516 $expressions[] = $rename->old; 517 } 518 } 519 520 $ret = []; 521 foreach ($expressions as $expr) { 522 if (empty($expr->table)) { 523 continue; 524 } 525 526 $expr->expr = null; // Force rebuild. 527 $expr->alias = null; // Aliases are not required. 528 $ret[] = Expression::build($expr); 529 } 530 531 return $ret; 532 } 533 534 /** 535 * Gets a specific clause. 536 * 537 * @param Statement $statement the parsed query that has to be modified 538 * @param TokensList $list the list of tokens 539 * @param string $clause the clause to be returned 540 * @param int|string $type The type of the search. 541 * If int, 542 * -1 for everything that was before 543 * 0 only for the clause 544 * 1 for everything after 545 * If string, the name of the first clause that 546 * should not be included. 547 * @param bool $skipFirst whether to skip the first keyword in clause 548 * 549 * @return string 550 */ 551 public static function getClause($statement, $list, $clause, $type = 0, $skipFirst = true) 552 { 553 /** 554 * The index of the current clause. 555 * 556 * @var int 557 */ 558 $currIdx = 0; 559 560 /** 561 * The count of brackets. 562 * We keep track of them so we won't insert the clause in a subquery. 563 * 564 * @var int 565 */ 566 $brackets = 0; 567 568 /** 569 * The string to be returned. 570 * 571 * @var string 572 */ 573 $ret = ''; 574 575 /** 576 * The clauses of this type of statement and their index. 577 * 578 * @var array 579 */ 580 $clauses = array_flip(array_keys($statement->getClauses())); 581 582 /** 583 * Lexer used for lexing the clause. 584 * 585 * @var Lexer 586 */ 587 $lexer = new Lexer($clause); 588 589 /** 590 * The type of this clause. 591 * 592 * @var string 593 */ 594 $clauseType = $lexer->list->getNextOfType(Token::TYPE_KEYWORD)->keyword; 595 596 /** 597 * The index of this clause. 598 * 599 * @var int 600 */ 601 $clauseIdx = $clauses[$clauseType] ?? -1; 602 603 $firstClauseIdx = $clauseIdx; 604 $lastClauseIdx = $clauseIdx; 605 606 // Determining the behavior of this function. 607 if ($type === -1) { 608 $firstClauseIdx = -1; // Something small enough. 609 $lastClauseIdx = $clauseIdx - 1; 610 } elseif ($type === 1) { 611 $firstClauseIdx = $clauseIdx + 1; 612 $lastClauseIdx = 10000; // Something big enough. 613 } elseif (is_string($type) && isset($clauses[$type])) { 614 if ($clauses[$type] > $clauseIdx) { 615 $firstClauseIdx = $clauseIdx + 1; 616 $lastClauseIdx = $clauses[$type] - 1; 617 } else { 618 $firstClauseIdx = $clauses[$type] + 1; 619 $lastClauseIdx = $clauseIdx - 1; 620 } 621 } 622 623 // This option is unavailable for multiple clauses. 624 if ($type !== 0) { 625 $skipFirst = false; 626 } 627 628 for ($i = $statement->first; $i <= $statement->last; ++$i) { 629 $token = $list->tokens[$i]; 630 631 if ($token->type === Token::TYPE_COMMENT) { 632 continue; 633 } 634 635 if ($token->type === Token::TYPE_OPERATOR) { 636 if ($token->value === '(') { 637 ++$brackets; 638 } elseif ($token->value === ')') { 639 --$brackets; 640 } 641 } 642 643 if ($brackets === 0) { 644 // Checking if the section was changed. 645 if ( 646 ($token->type === Token::TYPE_KEYWORD) 647 && isset($clauses[$token->keyword]) 648 && ($clauses[$token->keyword] >= $currIdx) 649 ) { 650 $currIdx = $clauses[$token->keyword]; 651 if ($skipFirst && ($currIdx === $clauseIdx)) { 652 // This token is skipped (not added to the old 653 // clause) because it will be replaced. 654 continue; 655 } 656 } 657 } 658 659 if (($firstClauseIdx > $currIdx) || ($currIdx > $lastClauseIdx)) { 660 continue; 661 } 662 663 $ret .= $token->token; 664 } 665 666 return trim($ret); 667 } 668 669 /** 670 * Builds a query by rebuilding the statement from the tokens list supplied 671 * and replaces a clause. 672 * 673 * It is a very basic version of a query builder. 674 * 675 * @param Statement $statement the parsed query that has to be modified 676 * @param TokensList $list the list of tokens 677 * @param string $old The type of the clause that should be 678 * replaced. This can be an entire clause. 679 * @param string $new The new clause. If this parameter is omitted 680 * it is considered to be equal with `$old`. 681 * @param bool $onlyType whether only the type of the clause should 682 * be replaced or the entire clause 683 * 684 * @return string 685 */ 686 public static function replaceClause($statement, $list, $old, $new = null, $onlyType = false) 687 { 688 // TODO: Update the tokens list and the statement. 689 690 if ($new === null) { 691 $new = $old; 692 } 693 694 if ($onlyType) { 695 return static::getClause($statement, $list, $old, -1, false) . ' ' . 696 $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' . 697 static::getClause($statement, $list, $old, 1, false); 698 } 699 700 return static::getClause($statement, $list, $old, -1, false) . ' ' . 701 $new . ' ' . static::getClause($statement, $list, $old, 1, false); 702 } 703 704 /** 705 * Builds a query by rebuilding the statement from the tokens list supplied 706 * and replaces multiple clauses. 707 * 708 * @param Statement $statement the parsed query that has to be modified 709 * @param TokensList $list the list of tokens 710 * @param array $ops Clauses to be replaced. Contains multiple 711 * arrays having two values: [$old, $new]. 712 * Clauses must be sorted. 713 * 714 * @return string 715 */ 716 public static function replaceClauses($statement, $list, array $ops) 717 { 718 $count = count($ops); 719 720 // Nothing to do. 721 if ($count === 0) { 722 return ''; 723 } 724 725 /** 726 * Value to be returned. 727 * 728 * @var string 729 */ 730 $ret = ''; 731 732 // If there is only one clause, `replaceClause()` should be used. 733 if ($count === 1) { 734 return static::replaceClause($statement, $list, $ops[0][0], $ops[0][1]); 735 } 736 737 // Adding everything before first replacement. 738 $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' '; 739 740 // Doing replacements. 741 foreach ($ops as $i => $clause) { 742 $ret .= $clause[1] . ' '; 743 744 // Adding everything between this and next replacement. 745 if ($i + 1 === $count) { 746 continue; 747 } 748 749 $ret .= static::getClause($statement, $list, $clause[0], $ops[$i + 1][0]) . ' '; 750 } 751 752 // Adding everything after the last replacement. 753 return $ret . static::getClause($statement, $list, $ops[$count - 1][0], 1); 754 } 755 756 /** 757 * Gets the first full statement in the query. 758 * 759 * @param string $query the query to be analyzed 760 * @param string $delimiter the delimiter to be used 761 * 762 * @return array array containing the first full query, the 763 * remaining part of the query and the last 764 * delimiter 765 */ 766 public static function getFirstStatement($query, $delimiter = null) 767 { 768 $lexer = new Lexer($query, false, $delimiter); 769 $list = $lexer->list; 770 771 /** 772 * Whether a full statement was found. 773 * 774 * @var bool 775 */ 776 $fullStatement = false; 777 778 /** 779 * The first full statement. 780 * 781 * @var string 782 */ 783 $statement = ''; 784 785 for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) { 786 $token = $list->tokens[$list->idx]; 787 788 if ($token->type === Token::TYPE_COMMENT) { 789 continue; 790 } 791 792 $statement .= $token->token; 793 794 if (($token->type === Token::TYPE_DELIMITER) && ! empty($token->token)) { 795 $delimiter = $token->token; 796 $fullStatement = true; 797 break; 798 } 799 } 800 801 // No statement was found so we return the entire query as being the 802 // remaining part. 803 if (! $fullStatement) { 804 return [ 805 null, 806 $query, 807 $delimiter, 808 ]; 809 } 810 811 // At least one query was found so we have to build the rest of the 812 // remaining query. 813 $query = ''; 814 for (++$list->idx; $list->idx < $list->count; ++$list->idx) { 815 $query .= $list->tokens[$list->idx]->token; 816 } 817 818 return [ 819 trim($statement), 820 $query, 821 $delimiter, 822 ]; 823 } 824 825 /** 826 * Gets a starting offset of a specific clause. 827 * 828 * @param Statement $statement the parsed query that has to be modified 829 * @param TokensList $list the list of tokens 830 * @param string $clause the clause to be returned 831 * 832 * @return int 833 */ 834 public static function getClauseStartOffset($statement, $list, $clause) 835 { 836 /** 837 * The count of brackets. 838 * We keep track of them so we won't insert the clause in a subquery. 839 * 840 * @var int 841 */ 842 $brackets = 0; 843 844 /** 845 * The clauses of this type of statement and their index. 846 * 847 * @var array 848 */ 849 $clauses = array_flip(array_keys($statement->getClauses())); 850 851 for ($i = $statement->first; $i <= $statement->last; ++$i) { 852 $token = $list->tokens[$i]; 853 854 if ($token->type === Token::TYPE_COMMENT) { 855 continue; 856 } 857 858 if ($token->type === Token::TYPE_OPERATOR) { 859 if ($token->value === '(') { 860 ++$brackets; 861 } elseif ($token->value === ')') { 862 --$brackets; 863 } 864 } 865 866 if ($brackets !== 0) { 867 continue; 868 } 869 870 if ( 871 ($token->type === Token::TYPE_KEYWORD) 872 && isset($clauses[$token->keyword]) 873 && ($clause === $token->keyword) 874 ) { 875 return $i; 876 } 877 878 if ($token->keyword === 'UNION') { 879 return -1; 880 } 881 } 882 883 return -1; 884 } 885} 886