Passed
Pull Request — master (#546)
by Maurício
03:22
created

SelectStatement::getClauses()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 11
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 21
rs 9.9
ccs 14
cts 14
cp 1
crap 2
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 in_array;
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 384
            if (isset(Parser::KEYWORD_PARSERS[$token->value]) || ! empty(Parser::STATEMENT_PARSERS[$token->value])) {
437 384
                if (! empty($parsedClauses[$token->value])) {
438 14
                    $parser->error('This type of clause was previously parsed.', $token);
439 14
                    break;
440
                }
441
442 384
                $parsedClauses[$token->value] = true;
443
            }
444
445
            // Checking if this is the beginning of a clause.
446
            // Fix Issue #221: As `truncate` is not a keyword
447
            // but it might be the beginning of a statement of truncate,
448
            // so let the value use the keyword field for truncate type.
449 384
            $tokenValue = in_array($token->keyword, ['TRUNCATE']) ? $token->keyword : $token->value;
450 384
            if (isset(Parser::KEYWORD_PARSERS[$tokenValue]) && $list->idx < $list->count) {
451 384
                $class = Parser::KEYWORD_PARSERS[$tokenValue]['class'];
452 384
                $field = Parser::KEYWORD_PARSERS[$tokenValue]['field'];
453 384
                if (! empty(Parser::KEYWORD_PARSERS[$tokenValue]['options'])) {
454 326
                    $options = Parser::KEYWORD_PARSERS[$tokenValue]['options'];
455
                }
456
            }
457
458
            // Checking if this is the beginning of the statement.
459 384
            if (! empty(Parser::STATEMENT_PARSERS[$token->keyword])) {
460
                if (
461 384
                    ! empty(static::$clauses) // Undefined for some statements.
462 384
                    && empty(static::$clauses[$token->value])
463
                ) {
464
                    // Some keywords (e.g. `SET`) may be the beginning of a
465
                    // statement and a clause.
466
                    // If such keyword was found and it cannot be a clause of
467
                    // this statement it means it is a new statement, but no
468
                    // delimiter was found between them.
469 4
                    $parser->error(
470 4
                        'A new statement was found, but no delimiter between it and the previous one.',
471 4
                        $token,
472 4
                    );
473 4
                    break;
474
                }
475
476 384
                if (! $parsedOptions) {
477 384
                    if (! array_key_exists((string) $token->value, static::$statementOptions)) {
478
                        // Skipping keyword because if it is not a option.
479 384
                        ++$list->idx;
480
                    }
481
482 384
                    $this->options = OptionsArrays::parse($parser, $list, static::$statementOptions);
483 384
                    $parsedOptions = true;
484
                }
485 326
            } elseif ($class === null) {
486 34
                if ($token->value === 'WITH ROLLUP') {
487
                    // Handle group options in Select statement
488 10
                    $this->groupOptions = OptionsArrays::parse($parser, $list, self::STATEMENT_GROUP_OPTIONS);
489 26
                } elseif ($token->value === 'FOR UPDATE' || $token->value === 'LOCK IN SHARE MODE') {
490
                    // Handle special end options in Select statement
491 8
                    $this->endOptions = OptionsArrays::parse($parser, $list, self::STATEMENT_END_OPTIONS);
492
                } else {
493
                    // There is no parser for this keyword and isn't the beginning
494
                    // of a statement (so no options) either.
495 18
                    $parser->error('Unrecognized keyword.', $token);
496 18
                    continue;
497
                }
498
            }
499
500 384
            if ($class === null) {
501 18
                continue;
502
            }
503
504
            // Parsing this keyword.
505
506
            // We can't parse keyword at the end of statement
507 384
            if ($list->idx >= $list->count) {
508
                $parser->error('Keyword at end of statement.', $token);
509
                continue;
510
            }
511
512 384
            ++$list->idx; // Skipping keyword or last option.
513 384
            $this->$field = $class::parse($parser, $list, $options);
514
        }
515
516
        // This may be corrected by the parser.
517 384
        $this->last = --$list->idx; // Go back to last used token.
518
    }
519
520
    /**
521
     * Gets the clauses of this statement.
522
     *
523
     * @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...
524
     */
525 360
    public function getClauses(): array
526
    {
527
        // This is a cheap fix for `SELECT` statements that contain `UNION`.
528
        // The `ORDER BY` and `LIMIT` clauses should be at the end of the
529
        // statement.
530 360
        if ($this->union !== []) {
531 10
            $clauses = static::$clauses;
532 10
            unset($clauses['ORDER BY'], $clauses['LIMIT']);
533 10
            $clauses['ORDER BY'] = [
534 10
                'ORDER BY',
535 10
                Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
536 10
            ];
537 10
            $clauses['LIMIT'] = [
538 10
                'LIMIT',
539 10
                Statement::ADD_CLAUSE | Statement::ADD_KEYWORD,
540 10
            ];
541
542 10
            return $clauses;
543
        }
544
545 360
        return static::$clauses;
546
    }
547
548
    /**
549
     * Gets a list of all aliases and their original names.
550
     *
551
     * @param string $database the name of the database
552
     *
553
     * @return array<string, array<string, array<string, array<string, array<string, string>|string|null>>|null>>
554
     */
555 14
    public function getAliases(string $database): array
556
    {
557 14
        if ($this->expr === [] || $this->from === []) {
558 2
            return [];
559
        }
560
561 12
        $retval = [];
562
563 12
        $tables = [];
564
565
        /**
566
         * Expressions that may contain aliases.
567
         * These are extracted from `FROM` and `JOIN` keywords.
568
         */
569 12
        $expressions = $this->from;
570
571
        // Adding expressions from JOIN.
572 12
        if (! empty($this->join)) {
573 2
            foreach ($this->join as $join) {
574 2
                $expressions[] = $join->expr;
575
            }
576
        }
577
578 12
        foreach ($expressions as $expr) {
579 12
            if (! isset($expr->table) || ($expr->table === '')) {
580 4
                continue;
581
            }
582
583 8
            $thisDb = isset($expr->database) && ($expr->database !== '') ?
584 8
                $expr->database : $database;
585
586 8
            if (! isset($retval[$thisDb])) {
587 8
                $retval[$thisDb] = [
588 8
                    'alias' => null,
589 8
                    'tables' => [],
590 8
                ];
591
            }
592
593 8
            if (! isset($retval[$thisDb]['tables'][$expr->table])) {
594 8
                $retval[$thisDb]['tables'][$expr->table] = [
595 8
                    'alias' => isset($expr->alias) && ($expr->alias !== '') ?
596 6
                        $expr->alias : null,
597 8
                    'columns' => [],
598 8
                ];
599
            }
600
601 8
            if (! isset($tables[$thisDb])) {
602 8
                $tables[$thisDb] = [];
603
            }
604
605 8
            $tables[$thisDb][$expr->alias] = $expr->table;
606
        }
607
608 12
        foreach ($this->expr as $expr) {
609 12
            if (! isset($expr->column, $expr->alias) || ($expr->column === '') || ($expr->alias === '')) {
610 8
                continue;
611
            }
612
613 6
            $thisDb = isset($expr->database) && ($expr->database !== '') ?
614 6
                $expr->database : $database;
615
616 6
            if (isset($expr->table) && ($expr->table !== '')) {
617 4
                $thisTable = $tables[$thisDb][$expr->table] ?? $expr->table;
618 4
                $retval[$thisDb]['tables'][$thisTable]['columns'][$expr->column] = $expr->alias;
619
            } else {
620 6
                foreach ($retval[$thisDb]['tables'] as &$table) {
621 6
                    $table['columns'][$expr->column] = $expr->alias;
622
                }
623
            }
624
        }
625
626 12
        return $retval;
627
    }
628
}
629