Passed
Push — master ( de2d27...d28f1e )
by Maurício
10:00 queued 06:30
created

SelectStatement::parse()   D

Complexity

Conditions 43
Paths 30

Size

Total Lines 190
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 79
CRAP Score 43.0278

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 43
eloc 84
c 1
b 0
f 0
nc 30
nop 2
dl 0
loc 190
ccs 79
cts 81
cp 0.9753
crap 43.0278
rs 4.1666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Statements;
6
7
use PhpMyAdmin\SqlParser\Components\ArrayObj;
8
use PhpMyAdmin\SqlParser\Components\Condition;
9
use PhpMyAdmin\SqlParser\Components\Expression;
10
use PhpMyAdmin\SqlParser\Components\FunctionCall;
11
use PhpMyAdmin\SqlParser\Components\GroupKeyword;
12
use PhpMyAdmin\SqlParser\Components\IndexHint;
13
use PhpMyAdmin\SqlParser\Components\IntoKeyword;
14
use PhpMyAdmin\SqlParser\Components\JoinKeyword;
15
use PhpMyAdmin\SqlParser\Components\Limit;
16
use PhpMyAdmin\SqlParser\Components\OptionsArray;
17
use PhpMyAdmin\SqlParser\Components\OrderKeyword;
18
use PhpMyAdmin\SqlParser\Exceptions\ParserException;
19
use PhpMyAdmin\SqlParser\Parser;
20
use PhpMyAdmin\SqlParser\Parsers\OptionsArrays;
21
use PhpMyAdmin\SqlParser\Statement;
22
use PhpMyAdmin\SqlParser\TokensList;
23
use PhpMyAdmin\SqlParser\TokenType;
24
25
use function array_key_exists;
26
use function is_string;
27
28
/**
29
 * `SELECT` statement.
30
 *
31
 * SELECT
32
 *     [ALL | DISTINCT | DISTINCTROW ]
33
 *       [HIGH_PRIORITY]
34
 *       [MAX_STATEMENT_TIME = N]
35
 *       [STRAIGHT_JOIN]
36
 *       [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
37
 *       [SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
38
 *     select_expr [, select_expr ...]
39
 *     [FROM table_references
40
 *       [PARTITION partition_list]
41
 *     [WHERE where_condition]
42
 *     [GROUP BY {col_name | expr | position}
43
 *       [ASC | DESC], ... [WITH ROLLUP]]
44
 *     [HAVING where_condition]
45
 *     [ORDER BY {col_name | expr | position}
46
 *       [ASC | DESC], ...]
47
 *     [LIMIT {[offset,] row_count | row_count OFFSET offset}]
48
 *     [PROCEDURE procedure_name(argument_list)]
49
 *     [INTO OUTFILE 'file_name'
50
 *         [CHARACTER SET charset_name]
51
 *         export_options
52
 *       | INTO DUMPFILE 'file_name'
53
 *       | INTO var_name [, var_name]]
54
 *     [FOR UPDATE | LOCK IN SHARE MODE]]
55
 */
56
class SelectStatement extends Statement
57
{
58
    /**
59
     * Options for `SELECT` statements and their slot ID.
60
     *
61
     * @var array<string, int|array<int, int|string>>
62
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
63
     */
64
    public static array $statementOptions = [
65
        'ALL' => 1,
66
        'DISTINCT' => 1,
67
        'DISTINCTROW' => 1,
68
        'HIGH_PRIORITY' => 2,
69
        'MAX_STATEMENT_TIME' => [
70
            3,
71
            'var=',
72
        ],
73
        'STRAIGHT_JOIN' => 4,
74
        'SQL_SMALL_RESULT' => 5,
75
        'SQL_BIG_RESULT' => 6,
76
        'SQL_BUFFER_RESULT' => 7,
77
        'SQL_CACHE' => 8,
78
        'SQL_NO_CACHE' => 8,
79
        'SQL_CALC_FOUND_ROWS' => 9,
80
    ];
81
82
    protected const STATEMENT_GROUP_OPTIONS = ['WITH ROLLUP' => 1];
83
84
    protected const STATEMENT_END_OPTIONS = [
85
        'FOR UPDATE' => 1,
86
        'LOCK IN SHARE MODE' => 1,
87
    ];
88
89
    /**
90
     * The clauses of this statement, in order.
91
     *
92
     * @see Statement::$clauses
93
     *
94
     * @var array<string, array{non-empty-string, int-mask-of<self::ADD_*>}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, array{non-...-mask-of<self::ADD_*>}> at position 6 could not be parsed: Expected ':' at position 6, but found 'non-empty-string'.
Loading history...
95
     */
96
    public static array $clauses = [
97
        'SELECT' => [
98
            'SELECT',
99
            Statement::ADD_KEYWORD,
100
        ],
101
        // Used for options.
102
        '_OPTIONS' => [
103
            '_OPTIONS',
104
            Statement::ADD_CLAUSE,
105
        ],
106
        // Used for selected expressions.
107
        '_SELECT' => [
108
            'SELECT',
109
            Statement::ADD_CLAUSE,
110
        ],
111
        'INTO' => [
112
            'INTO',
113
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
114
        ],
115
        'FROM' => [
116
            'FROM',
117
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
118
        ],
119
        'FORCE' => [
120
            'FORCE',
121
            Statement::ADD_CLAUSE,
122
        ],
123
        'USE' => [
124
            'USE',
125
            Statement::ADD_CLAUSE,
126
        ],
127
        'IGNORE' => [
128
            'IGNORE',
129
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
130
        ],
131
        'PARTITION' => [
132
            'PARTITION',
133
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
134
        ],
135
136
        'JOIN' => [
137
            'JOIN',
138
            Statement::ADD_CLAUSE,
139
        ],
140
        'FULL JOIN' => [
141
            'FULL JOIN',
142
            Statement::ADD_CLAUSE,
143
        ],
144
        'INNER JOIN' => [
145
            'INNER JOIN',
146
            Statement::ADD_CLAUSE,
147
        ],
148
        'LEFT JOIN' => [
149
            'LEFT JOIN',
150
            Statement::ADD_CLAUSE,
151
        ],
152
        'LEFT OUTER JOIN' => [
153
            'LEFT OUTER JOIN',
154
            Statement::ADD_CLAUSE,
155
        ],
156
        'RIGHT JOIN' => [
157
            'RIGHT JOIN',
158
            Statement::ADD_CLAUSE,
159
        ],
160
        'RIGHT OUTER JOIN' => [
161
            'RIGHT OUTER JOIN',
162
            Statement::ADD_CLAUSE,
163
        ],
164
        'NATURAL JOIN' => [
165
            'NATURAL JOIN',
166
            Statement::ADD_CLAUSE,
167
        ],
168
        'NATURAL LEFT JOIN' => [
169
            'NATURAL LEFT JOIN',
170
            Statement::ADD_CLAUSE,
171
        ],
172
        'NATURAL RIGHT JOIN' => [
173
            'NATURAL RIGHT JOIN',
174
            Statement::ADD_CLAUSE,
175
        ],
176
        'NATURAL LEFT OUTER JOIN' => [
177
            'NATURAL LEFT OUTER JOIN',
178
            Statement::ADD_CLAUSE,
179
        ],
180
        'NATURAL RIGHT OUTER JOIN' => [
181
            'NATURAL RIGHT JOIN',
182
            Statement::ADD_CLAUSE,
183
        ],
184
        'WHERE' => [
185
            'WHERE',
186
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
187
        ],
188
        'GROUP BY' => [
189
            'GROUP BY',
190
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
191
        ],
192
        '_GROUP_OPTIONS' => [
193
            '_GROUP_OPTIONS',
194
            Statement::ADD_CLAUSE,
195
        ],
196
        'HAVING' => [
197
            'HAVING',
198
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
199
        ],
200
        'ORDER BY' => [
201
            'ORDER BY',
202
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
203
        ],
204
        'LIMIT' => [
205
            'LIMIT',
206
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
207
        ],
208
        'PROCEDURE' => [
209
            'PROCEDURE',
210
            Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
211
        ],
212
        'UNION' => [
213
            'UNION',
214
            Statement::ADD_CLAUSE,
215
        ],
216
        'EXCEPT' => [
217
            'EXCEPT',
218
            Statement::ADD_CLAUSE,
219
        ],
220
        'INTERSECT' => [
221
            'INTERSECT',
222
            Statement::ADD_CLAUSE,
223
        ],
224
        '_END_OPTIONS' => [
225
            '_END_OPTIONS',
226
            Statement::ADD_CLAUSE,
227
        ],
228
        // These are available only when `UNION` is present.
229
        // 'ORDER BY'                      => ['ORDER BY', Statement::ADD_CLAUSE|Statement::ADD_KEYWORD],
230
        // 'LIMIT'                         => ['LIMIT', Statement::ADD_CLAUSE|Statement::ADD_KEYWORD],
231
    ];
232
233
    /**
234
     * Expressions that are being selected by this statement.
235
     *
236
     * @var Expression[]
237
     */
238
    public array $expr = [];
239
240
    /**
241
     * Tables used as sources for this statement.
242
     *
243
     * @var Expression[]
244
     */
245
    public array $from = [];
246
247
    /**
248
     * Index hints
249
     *
250
     * @var IndexHint[]|null
251
     */
252
    public array|null $indexHints = null;
253
254
    /**
255
     * Partitions used as source for this statement.
256
     */
257
    public ArrayObj|null $partition = null;
258
259
    /**
260
     * Conditions used for filtering each row of the result set.
261
     *
262
     * @var Condition[]|null
263
     */
264
    public array|null $where = null;
265
266
    /**
267
     * Conditions used for grouping the result set.
268
     *
269
     * @var GroupKeyword[]|null
270
     */
271
    public array|null $group = null;
272
273
    /**
274
     * List of options available for the GROUP BY component.
275
     */
276
    public OptionsArray|null $groupOptions = null;
277
278
    /**
279
     * Conditions used for filtering the result set.
280
     *
281
     * @var Condition[]|null
282
     */
283
    public array|null $having = null;
284
285
    /**
286
     * Specifies the order of the rows in the result set.
287
     *
288
     * @var OrderKeyword[]|null
289
     */
290
    public array|null $order = null;
291
292
    /**
293
     * Conditions used for limiting the size of the result set.
294
     */
295
    public Limit|null $limit = null;
296
297
    /**
298
     * Procedure that should process the data in the result set.
299
     */
300
    public FunctionCall|null $procedure = null;
301
302
    /**
303
     * Destination of this result set.
304
     */
305
    public IntoKeyword|null $into = null;
306
307
    /**
308
     * Joins.
309
     *
310
     * @var JoinKeyword[]|null
311
     */
312
    public array|null $join = null;
313
314
    /**
315
     * Unions.
316
     *
317
     * @var SelectStatement[]
318
     */
319
    public array $union = [];
320
321
    /**
322
     * The end options of this query.
323
     *
324
     * @see SelectStatement::STATEMENT_END_OPTIONS
325
     */
326
    public OptionsArray|null $endOptions = null;
327
328
    /**
329
     * Parses the statements defined by the tokens list.
330
     *
331
     * @param Parser     $parser the instance that requests parsing
332
     * @param TokensList $list   the list of tokens to be parsed
333
     *
334
     * @throws ParserException
335
     */
336 384
    public function parse(Parser $parser, TokensList $list): void
337
    {
338
        /**
339
         * Array containing all list of clauses parsed.
340
         * This is used to check for duplicates.
341
         */
342 384
        $parsedClauses = [];
343
344
        // This may be corrected by the parser.
345 384
        $this->first = $list->idx;
346
347
        /**
348
         * Whether options were parsed or not.
349
         * For statements that do not have any options this is set to `true` by
350
         * default.
351
         */
352 384
        $parsedOptions = static::$statementOptions === [];
353
354 384
        for (; $list->idx < $list->count; ++$list->idx) {
355
            /**
356
             * Token parsed at this moment.
357
             */
358 384
            $token = $list->tokens[$list->idx];
359
360
            // End of statement.
361 384
            if ($token->type === TokenType::Delimiter) {
362 364
                break;
363
            }
364
365
            // Checking if this closing bracket is the pair for a bracket
366
            // outside the statement.
367 384
            if (($token->value === ')') && ($parser->brackets > 0)) {
368 14
                --$parser->brackets;
369 14
                continue;
370
            }
371
372
            // Only keywords are relevant here. Other parts of the query are
373
            // processed in the functions below.
374 384
            if ($token->type !== TokenType::Keyword) {
375 36
                if (($token->type !== TokenType::Comment) && ($token->type !== TokenType::Whitespace)) {
376 20
                    $parser->error('Unexpected token.', $token);
377
                }
378
379 36
                continue;
380
            }
381
382
            // Unions are parsed by the parser because they represent more than
383
            // one statement.
384
            if (
385 384
                ($token->keyword === 'UNION') ||
386 384
                ($token->keyword === 'UNION ALL') ||
387 384
                ($token->keyword === 'UNION DISTINCT') ||
388 384
                ($token->keyword === 'EXCEPT') ||
389 384
                ($token->keyword === 'INTERSECT')
390
            ) {
391 40
                break;
392
            }
393
394 384
            $lastIdx = $list->idx;
395
396
            // ON DUPLICATE KEY UPDATE ...
397
            // has to be parsed in parent statement (INSERT or REPLACE)
398
            // so look for it and break
399 384
            if ($token->value === 'ON') {
400 6
                ++$list->idx; // Skip ON
401
402
                // look for ON DUPLICATE KEY UPDATE
403 6
                $first = $list->getNextOfType(TokenType::Keyword);
404 6
                $second = $list->getNextOfType(TokenType::Keyword);
405 6
                $third = $list->getNextOfType(TokenType::Keyword);
406
407
                if (
408 6
                    $first && $second && $third
409 6
                    && $first->value === 'DUPLICATE'
410 6
                    && $second->value === 'KEY'
411 6
                    && $third->value === 'UPDATE'
412
                ) {
413 6
                    $list->idx = $lastIdx;
414 6
                    break;
415
                }
416
            }
417
418 384
            $list->idx = $lastIdx;
419
420
            /**
421
             * The name of the class that is used for parsing.
422
             */
423 384
            $class = null;
424
425
            /**
426
             * The name of the field where the result of the parsing is stored.
427
             */
428 384
            $field = null;
429
430
            /**
431
             * Parser's options.
432
             */
433 384
            $options = [];
434
435
            // Looking for duplicated clauses.
436
            if (
437 384
                is_string($token->value)
438
                && (
439 384
                    isset(Parser::KEYWORD_PARSERS[$token->value])
440 384
                    || (
441 384
                        isset(Parser::STATEMENT_PARSERS[$token->value])
442 384
                        && Parser::STATEMENT_PARSERS[$token->value] !== ''
443 384
                    )
444
                )
445
            ) {
446 384
                if (array_key_exists($token->value, $parsedClauses)) {
447 14
                    $parser->error('This type of clause was previously parsed.', $token);
448 14
                    break;
449
                }
450
451 384
                $parsedClauses[$token->value] = true;
452
            }
453
454
            // Checking if this is the beginning of a clause.
455
            // Fix Issue #221: As `truncate` is not a keyword,
456
            // but it might be the beginning of a statement of truncate,
457
            // so let the value use the keyword field for truncate type.
458 384
            $tokenValue = $token->keyword === 'TRUNCATE' ? $token->keyword : $token->value;
459 384
            if (is_string($tokenValue) && isset(Parser::KEYWORD_PARSERS[$tokenValue]) && $list->idx < $list->count) {
460 384
                $class = Parser::KEYWORD_PARSERS[$tokenValue]['class'];
461 384
                $field = Parser::KEYWORD_PARSERS[$tokenValue]['field'];
462 384
                if (isset(Parser::KEYWORD_PARSERS[$tokenValue]['options'])) {
463 326
                    $options = Parser::KEYWORD_PARSERS[$tokenValue]['options'];
464
                }
465
            }
466
467
            // Checking if this is the beginning of the statement.
468
            if (
469 384
                isset(Parser::STATEMENT_PARSERS[$token->keyword])
470 384
                && Parser::STATEMENT_PARSERS[$token->keyword] !== ''
471
            ) {
472 384
                if (static::$clauses !== [] && is_string($token->value) && ! isset(static::$clauses[$token->value])) {
473
                    // Some keywords (e.g. `SET`) may be the beginning of a
474
                    // statement and a clause.
475
                    // If such keyword was found, and it cannot be a clause of
476
                    // this statement it means it is a new statement, but no
477
                    // delimiter was found between them.
478 4
                    $parser->error(
479 4
                        'A new statement was found, but no delimiter between it and the previous one.',
480 4
                        $token,
481 4
                    );
482 4
                    break;
483
                }
484
485 384
                if (! $parsedOptions) {
486 384
                    if (! array_key_exists((string) $token->value, static::$statementOptions)) {
487
                        // Skipping keyword because if it is not a option.
488 384
                        ++$list->idx;
489
                    }
490
491 384
                    $this->options = OptionsArrays::parse($parser, $list, static::$statementOptions);
492 384
                    $parsedOptions = true;
493
                }
494 326
            } elseif ($class === null) {
495 34
                if ($token->value === 'WITH ROLLUP') {
496
                    // Handle group options in Select statement
497 10
                    $this->groupOptions = OptionsArrays::parse($parser, $list, self::STATEMENT_GROUP_OPTIONS);
498 26
                } elseif ($token->value === 'FOR UPDATE' || $token->value === 'LOCK IN SHARE MODE') {
499
                    // Handle special end options in Select statement
500 8
                    $this->endOptions = OptionsArrays::parse($parser, $list, self::STATEMENT_END_OPTIONS);
501
                } else {
502
                    // There is no parser for this keyword and isn't the beginning
503
                    // of a statement (so no options) either.
504 18
                    $parser->error('Unrecognized keyword.', $token);
505 18
                    continue;
506
                }
507
            }
508
509 384
            if ($class === null) {
510 18
                continue;
511
            }
512
513
            // Parsing this keyword.
514
            // We can't parse keyword at the end of statement
515 384
            if ($list->idx >= $list->count) {
516
                $parser->error('Keyword at end of statement.', $token);
517
                continue;
518
            }
519
520 384
            ++$list->idx; // Skipping keyword or last option.
521 384
            $this->$field = $class::parse($parser, $list, $options);
522
        }
523
524
        // This may be corrected by the parser.
525 384
        $this->last = --$list->idx; // Go back to last used token.
526
    }
527
528
    /**
529
     * Gets the clauses of this statement.
530
     *
531
     * @return array<string, array{non-empty-string, int-mask-of<Statement::ADD_*>}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, array{non-...-of<Statement::ADD_*>}> at position 6 could not be parsed: Expected ':' at position 6, but found 'non-empty-string'.
Loading history...
532
     */
533 360
    public function getClauses(): array
534
    {
535
        // This is a cheap fix for `SELECT` statements that contain `UNION`.
536
        // The `ORDER BY` and `LIMIT` clauses should be at the end of the
537
        // statement.
538 360
        if ($this->union !== []) {
539 10
            $clauses = static::$clauses;
540 10
            unset($clauses['ORDER BY'], $clauses['LIMIT']);
541 10
            $clauses['ORDER BY'] = [
542 10
                'ORDER BY',
543 10
                Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
544 10
            ];
545 10
            $clauses['LIMIT'] = [
546 10
                'LIMIT',
547 10
                Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
548 10
            ];
549
550 10
            return $clauses;
551
        }
552
553 360
        return static::$clauses;
554
    }
555
556
    /**
557
     * Gets a list of all aliases and their original names.
558
     *
559
     * @param string $database the name of the database
560
     *
561
     * @return array<string, array<string, array<string, array<string, array<string, string>|string|null>>|null>>
562
     */
563 14
    public function getAliases(string $database): array
564
    {
565 14
        if ($this->expr === [] || $this->from === []) {
566 2
            return [];
567
        }
568
569 12
        $retval = [];
570
571 12
        $tables = [];
572
573
        /**
574
         * Expressions that may contain aliases.
575
         * These are extracted from `FROM` and `JOIN` keywords.
576
         */
577 12
        $expressions = $this->from;
578
579
        // Adding expressions from JOIN.
580 12
        if (! empty($this->join)) {
581 2
            foreach ($this->join as $join) {
582 2
                $expressions[] = $join->expr;
583
            }
584
        }
585
586 12
        foreach ($expressions as $expr) {
587 12
            if (! isset($expr->table) || ($expr->table === '')) {
588 4
                continue;
589
            }
590
591 8
            $thisDb = isset($expr->database) && ($expr->database !== '') ?
592 8
                $expr->database : $database;
593
594 8
            if (! isset($retval[$thisDb])) {
595 8
                $retval[$thisDb] = [
596 8
                    'alias' => null,
597 8
                    'tables' => [],
598 8
                ];
599
            }
600
601 8
            if (! isset($retval[$thisDb]['tables'][$expr->table])) {
602 8
                $retval[$thisDb]['tables'][$expr->table] = [
603 8
                    'alias' => isset($expr->alias) && ($expr->alias !== '') ?
604 6
                        $expr->alias : null,
605 8
                    'columns' => [],
606 8
                ];
607
            }
608
609 8
            if (! isset($tables[$thisDb])) {
610 8
                $tables[$thisDb] = [];
611
            }
612
613 8
            $tables[$thisDb][$expr->alias] = $expr->table;
614
        }
615
616 12
        foreach ($this->expr as $expr) {
617 12
            if (! isset($expr->column, $expr->alias) || ($expr->column === '') || ($expr->alias === '')) {
618 8
                continue;
619
            }
620
621 6
            $thisDb = isset($expr->database) && ($expr->database !== '') ?
622 6
                $expr->database : $database;
623
624 6
            if (isset($expr->table) && ($expr->table !== '')) {
625 4
                $thisTable = $tables[$thisDb][$expr->table] ?? $expr->table;
626 4
                $retval[$thisDb]['tables'][$thisTable]['columns'][$expr->column] = $expr->alias;
627
            } else {
628 6
                foreach ($retval[$thisDb]['tables'] as &$table) {
629 6
                    $table['columns'][$expr->column] = $expr->alias;
630
                }
631
            }
632
        }
633
634 12
        return $retval;
635
    }
636
}
637