SelectStatement::getAliases()   F
last analyzed

Complexity

Conditions 24
Paths 1641

Size

Total Lines 72
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 24

Importance

Changes 0
Metric Value
cc 24
eloc 37
nc 1641
nop 1
dl 0
loc 72
rs 0
c 0
b 0
f 0
ccs 39
cts 39
cp 1
crap 24

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