Passed
Push — master ( e9fa3d...22a26f )
by William
03:07
created

Query::getTables()   C

Complexity

Conditions 14
Paths 22

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 14.0125

Importance

Changes 0
Metric Value
cc 14
eloc 24
c 0
b 0
f 0
nc 22
nop 1
dl 0
loc 37
ccs 24
cts 25
cp 0.96
crap 14.0125
rs 6.2666

How to fix   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\Utils;
6
7
use PhpMyAdmin\SqlParser\Components\Expression;
8
use PhpMyAdmin\SqlParser\Lexer;
9
use PhpMyAdmin\SqlParser\Parser;
10
use PhpMyAdmin\SqlParser\Statement;
11
use PhpMyAdmin\SqlParser\Statements\AlterStatement;
12
use PhpMyAdmin\SqlParser\Statements\AnalyzeStatement;
13
use PhpMyAdmin\SqlParser\Statements\CallStatement;
14
use PhpMyAdmin\SqlParser\Statements\CheckStatement;
15
use PhpMyAdmin\SqlParser\Statements\ChecksumStatement;
16
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
17
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
18
use PhpMyAdmin\SqlParser\Statements\DropStatement;
19
use PhpMyAdmin\SqlParser\Statements\ExplainStatement;
20
use PhpMyAdmin\SqlParser\Statements\InsertStatement;
21
use PhpMyAdmin\SqlParser\Statements\LoadStatement;
22
use PhpMyAdmin\SqlParser\Statements\OptimizeStatement;
23
use PhpMyAdmin\SqlParser\Statements\RenameStatement;
24
use PhpMyAdmin\SqlParser\Statements\RepairStatement;
25
use PhpMyAdmin\SqlParser\Statements\ReplaceStatement;
26
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
27
use PhpMyAdmin\SqlParser\Statements\SetStatement;
28
use PhpMyAdmin\SqlParser\Statements\ShowStatement;
29
use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
30
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
31
use PhpMyAdmin\SqlParser\Token;
32
use PhpMyAdmin\SqlParser\TokensList;
33
34
use function array_flip;
35
use function array_keys;
36
use function count;
37
use function in_array;
38
use function is_string;
39
use function trim;
40
41
/**
42
 * Statement utilities.
43
 *
44
 * @psalm-type QueryFlagsType = array{
45
 *   distinct?: bool,
46
 *   drop_database?: bool,
47
 *   group?: bool,
48
 *   having?: bool,
49
 *   is_affected?: bool,
50
 *   is_analyse?: bool,
51
 *   is_count?: bool,
52
 *   is_delete?: bool,
53
 *   is_explain?: bool,
54
 *   is_export?: bool,
55
 *   is_func?: bool,
56
 *   is_group?: bool,
57
 *   is_insert?: bool,
58
 *   is_maint?: bool,
59
 *   is_procedure?: bool,
60
 *   is_replace?: bool,
61
 *   is_select?: bool,
62
 *   is_show?: bool,
63
 *   is_subquery?: bool,
64
 *   join?: bool,
65
 *   limit?: bool,
66
 *   offset?: bool,
67
 *   order?: bool,
68
 *   querytype: ('ALTER'|'ANALYZE'|'CALL'|'CHECK'|'CHECKSUM'|'CREATE'|'DELETE'|'DROP'|'EXPLAIN'|'INSERT'|'LOAD'|'OPTIMIZE'|'REPAIR'|'REPLACE'|'SELECT'|'SET'|'SHOW'|'UPDATE'|false),
69
 *   reload?: bool,
70
 *   select_from?: bool,
71
 *   union?: bool
72
 * }
73
 */
74
class Query
75
{
76
    /**
77
     * Functions that set the flag `is_func`.
78
     *
79
     * @var string[]
80
     */
81
    public static $FUNCTIONS = [
82
        'SUM',
83
        'AVG',
84
        'STD',
85
        'STDDEV',
86
        'MIN',
87
        'MAX',
88
        'BIT_OR',
89
        'BIT_AND',
90
    ];
91
92
    /**
93
     * @var array<string, false>
94
     * @psalm-var array{
95
     *   distinct: false,
96
     *   drop_database: false,
97
     *   group: false,
98
     *   having: false,
99
     *   is_affected: false,
100
     *   is_analyse: false,
101
     *   is_count: false,
102
     *   is_delete: false,
103
     *   is_explain: false,
104
     *   is_export: false,
105
     *   is_func: false,
106
     *   is_group: false,
107
     *   is_insert: false,
108
     *   is_maint: false,
109
     *   is_procedure: false,
110
     *   is_replace: false,
111
     *   is_select: false,
112
     *   is_show: false,
113
     *   is_subquery: false,
114
     *   join: false,
115
     *   limit: false,
116
     *   offset: false,
117
     *   order: false,
118
     *   querytype: false,
119
     *   reload: false,
120
     *   select_from: false,
121
     *   union: false
122
     * }
123
     */
124
    public static $ALLFLAGS = [
125
        /*
126
         * select ... DISTINCT ...
127
         */
128
        'distinct' => false,
129
130
        /*
131
         * drop ... DATABASE ...
132
         */
133
        'drop_database' => false,
134
135
        /*
136
         * ... GROUP BY ...
137
         */
138
        'group' => false,
139
140
        /*
141
         * ... HAVING ...
142
         */
143
        'having' => false,
144
145
        /*
146
         * INSERT ...
147
         * or
148
         * REPLACE ...
149
         * or
150
         * DELETE ...
151
         */
152
        'is_affected' => false,
153
154
        /*
155
         * select ... PROCEDURE ANALYSE( ... ) ...
156
         */
157
        'is_analyse' => false,
158
159
        /*
160
         * select COUNT( ... ) ...
161
         */
162
        'is_count' => false,
163
164
        /*
165
         * DELETE ...
166
         */
167
        'is_delete' => false, // @deprecated; use `querytype`
168
169
        /*
170
         * EXPLAIN ...
171
         */
172
        'is_explain' => false, // @deprecated; use `querytype`
173
174
        /*
175
         * select ... INTO OUTFILE ...
176
         */
177
        'is_export' => false,
178
179
        /*
180
         * select FUNC( ... ) ...
181
         */
182
        'is_func' => false,
183
184
        /*
185
         * select ... GROUP BY ...
186
         * or
187
         * select ... HAVING ...
188
         */
189
        'is_group' => false,
190
191
        /*
192
         * INSERT ...
193
         * or
194
         * REPLACE ...
195
         * or
196
         * LOAD DATA ...
197
         */
198
        'is_insert' => false,
199
200
        /*
201
         * ANALYZE ...
202
         * or
203
         * CHECK ...
204
         * or
205
         * CHECKSUM ...
206
         * or
207
         * OPTIMIZE ...
208
         * or
209
         * REPAIR ...
210
         */
211
        'is_maint' => false,
212
213
        /*
214
         * CALL ...
215
         */
216
        'is_procedure' => false,
217
218
        /*
219
         * REPLACE ...
220
         */
221
        'is_replace' => false, // @deprecated; use `querytype`
222
223
        /*
224
         * SELECT ...
225
         */
226
        'is_select' => false, // @deprecated; use `querytype`
227
228
        /*
229
         * SHOW ...
230
         */
231
        'is_show' => false, // @deprecated; use `querytype`
232
233
        /*
234
         * Contains a subquery.
235
         */
236
        'is_subquery' => false,
237
238
        /*
239
         * ... JOIN ...
240
         */
241
        'join' => false,
242
243
        /*
244
         * ... LIMIT ...
245
         */
246
        'limit' => false,
247
248
        /*
249
         * TODO
250
         */
251
        'offset' => false,
252
253
        /*
254
         * ... ORDER ...
255
         */
256
        'order' => false,
257
258
        /*
259
         * The type of the query (which is usually the first keyword of
260
         * the statement).
261
         */
262
        'querytype' => false,
263
264
        /*
265
         * Whether a page reload is required.
266
         */
267
        'reload' => false,
268
269
        /*
270
         * SELECT ... FROM ...
271
         */
272
        'select_from' => false,
273
274
        /*
275
         * ... UNION ...
276
         */
277
        'union' => false,
278
    ];
279
280
    /**
281
     * Gets an array with flags select statement has.
282
     *
283
     * @param SelectStatement            $statement the statement to be processed
284
     * @param array<string, bool|string> $flags     flags set so far
285
     * @psalm-param QueryFlagsType $flags
286
     *
287
     * @return array<string, bool|string>
288
     * @psalm-return QueryFlagsType
289
     */
290 52
    private static function getFlagsSelect($statement, $flags)
291
    {
292 52
        $flags['querytype'] = 'SELECT';
293 52
        $flags['is_select'] = true;
294
295 52
        if (! empty($statement->from)) {
296 44
            $flags['select_from'] = true;
297
        }
298
299 52
        if ($statement->options->has('DISTINCT')) {
0 ignored issues
show
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

299
        if ($statement->options->/** @scrutinizer ignore-call */ has('DISTINCT')) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
300 4
            $flags['distinct'] = true;
301
        }
302
303 52
        if (! empty($statement->group) || ! empty($statement->having)) {
304 8
            $flags['is_group'] = true;
305
        }
306
307 52
        if (! empty($statement->into) && ($statement->into->type === 'OUTFILE')) {
308 4
            $flags['is_export'] = true;
309
        }
310
311 52
        $expressions = $statement->expr;
312 52
        if (! empty($statement->join)) {
313 4
            foreach ($statement->join as $join) {
314 4
                $expressions[] = $join->expr;
315
            }
316
        }
317
318 52
        foreach ($expressions as $expr) {
319 52
            if (! empty($expr->function)) {
320 4
                if ($expr->function === 'COUNT') {
321 4
                    $flags['is_count'] = true;
322 4
                } elseif (in_array($expr->function, static::$FUNCTIONS)) {
323 4
                    $flags['is_func'] = true;
324
                }
325
            }
326
327 52
            if (empty($expr->subquery)) {
328 48
                continue;
329
            }
330
331 4
            $flags['is_subquery'] = true;
332
        }
333
334 52
        if (! empty($statement->procedure) && ($statement->procedure->name === 'ANALYSE')) {
335 4
            $flags['is_analyse'] = true;
336
        }
337
338 52
        if (! empty($statement->group)) {
339 4
            $flags['group'] = true;
340
        }
341
342 52
        if (! empty($statement->having)) {
343 4
            $flags['having'] = true;
344
        }
345
346 52
        if (! empty($statement->union)) {
347 4
            $flags['union'] = true;
348
        }
349
350 52
        if (! empty($statement->join)) {
351 4
            $flags['join'] = true;
352
        }
353
354 52
        return $flags;
355
    }
356
357
    /**
358
     * Gets an array with flags this statement has.
359
     *
360
     * @param Statement|null $statement the statement to be processed
361
     * @param bool           $all       if `false`, false values will not be included
362
     *
363
     * @return array<string, bool|string>
364
     * @psalm-return QueryFlagsType
365
     */
366 124
    public static function getFlags($statement, $all = false)
367
    {
368 124
        $flags = ['querytype' => false];
369 124
        if ($all) {
370 4
            $flags = self::$ALLFLAGS;
371
        }
372
373 124
        if ($statement instanceof AlterStatement) {
374 4
            $flags['querytype'] = 'ALTER';
375 4
            $flags['reload'] = true;
376 120
        } elseif ($statement instanceof CreateStatement) {
377 4
            $flags['querytype'] = 'CREATE';
378 4
            $flags['reload'] = true;
379 116
        } elseif ($statement instanceof AnalyzeStatement) {
380 4
            $flags['querytype'] = 'ANALYZE';
381 4
            $flags['is_maint'] = true;
382 112
        } elseif ($statement instanceof CheckStatement) {
383 4
            $flags['querytype'] = 'CHECK';
384 4
            $flags['is_maint'] = true;
385 108
        } elseif ($statement instanceof ChecksumStatement) {
386 4
            $flags['querytype'] = 'CHECKSUM';
387 4
            $flags['is_maint'] = true;
388 104
        } elseif ($statement instanceof OptimizeStatement) {
389 4
            $flags['querytype'] = 'OPTIMIZE';
390 4
            $flags['is_maint'] = true;
391 100
        } elseif ($statement instanceof RepairStatement) {
392 4
            $flags['querytype'] = 'REPAIR';
393 4
            $flags['is_maint'] = true;
394 96
        } elseif ($statement instanceof CallStatement) {
395 4
            $flags['querytype'] = 'CALL';
396 4
            $flags['is_procedure'] = true;
397 92
        } elseif ($statement instanceof DeleteStatement) {
398 4
            $flags['querytype'] = 'DELETE';
399 4
            $flags['is_delete'] = true;
400 4
            $flags['is_affected'] = true;
401 88
        } elseif ($statement instanceof DropStatement) {
402 8
            $flags['querytype'] = 'DROP';
403 8
            $flags['reload'] = true;
404
405 8
            if ($statement->options->has('DATABASE') || $statement->options->has('SCHEMA')) {
406 8
                $flags['drop_database'] = true;
407
            }
408 80
        } elseif ($statement instanceof ExplainStatement) {
409 4
            $flags['querytype'] = 'EXPLAIN';
410 4
            $flags['is_explain'] = true;
411 76
        } elseif ($statement instanceof InsertStatement) {
412 4
            $flags['querytype'] = 'INSERT';
413 4
            $flags['is_affected'] = true;
414 4
            $flags['is_insert'] = true;
415 72
        } elseif ($statement instanceof LoadStatement) {
416 4
            $flags['querytype'] = 'LOAD';
417 4
            $flags['is_affected'] = true;
418 4
            $flags['is_insert'] = true;
419 68
        } elseif ($statement instanceof ReplaceStatement) {
420 4
            $flags['querytype'] = 'REPLACE';
421 4
            $flags['is_affected'] = true;
422 4
            $flags['is_replace'] = true;
423 4
            $flags['is_insert'] = true;
424 64
        } elseif ($statement instanceof SelectStatement) {
425 52
            $flags = self::getFlagsSelect($statement, $flags);
426 16
        } elseif ($statement instanceof ShowStatement) {
427 4
            $flags['querytype'] = 'SHOW';
428 4
            $flags['is_show'] = true;
429 12
        } elseif ($statement instanceof UpdateStatement) {
430 4
            $flags['querytype'] = 'UPDATE';
431 4
            $flags['is_affected'] = true;
432 8
        } elseif ($statement instanceof SetStatement) {
433 4
            $flags['querytype'] = 'SET';
434
        }
435
436
        if (
437 124
            ($statement instanceof SelectStatement)
438 19
            || ($statement instanceof UpdateStatement)
439 31
            || ($statement instanceof DeleteStatement)
440
        ) {
441 60
            if (! empty($statement->limit)) {
442 8
                $flags['limit'] = true;
443
            }
444
445 60
            if (! empty($statement->order)) {
446 8
                $flags['order'] = true;
447
            }
448
        }
449
450 124
        return $flags;
451
    }
452
453
    /**
454
     * Parses a query and gets all information about it.
455
     *
456
     * @param string $query the query to be parsed
457
     *
458
     * @return array<string, bool|string> The array returned is the one returned by
459
     *               `static::getFlags()`, with the following keys added:
460
     *               - parser - the parser used to analyze the query;
461
     *               - statement - the first statement resulted from parsing;
462
     *               - select_tables - the real name of the tables selected;
463
     *               if there are no table names in the `SELECT`
464
     *               expressions, the table names are fetched from the
465
     *               `FROM` expressions
466
     *               - select_expr - selected expressions
467
     * @psalm-return QueryFlagsType
468
     */
469 4
    public static function getAll($query)
470
    {
471 4
        $parser = new Parser($query);
472
473 4
        if (empty($parser->statements[0])) {
474 4
            return static::getFlags(null, true);
475
        }
476
477 4
        $statement = $parser->statements[0];
478
479 4
        $ret = static::getFlags($statement, true);
480
481 4
        $ret['parser'] = $parser;
482 4
        $ret['statement'] = $statement;
483
484 4
        if ($statement instanceof SelectStatement) {
485 4
            $ret['select_tables'] = [];
486 4
            $ret['select_expr'] = [];
487
488
            // Finding tables' aliases and their associated real names.
489 4
            $tableAliases = [];
490 4
            foreach ($statement->from as $expr) {
491 4
                if (! isset($expr->table, $expr->alias) || ($expr->table === '') || ($expr->alias === '')) {
492 4
                    continue;
493
                }
494
495 4
                $tableAliases[$expr->alias] = [
496 4
                    $expr->table,
497 4
                    $expr->database ?? null,
498
                ];
499
            }
500
501
            // Trying to find selected tables only from the select expression.
502
            // Sometimes, this is not possible because the tables aren't defined
503
            // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film).
504 4
            foreach ($statement->expr as $expr) {
505 4
                if (isset($expr->table) && ($expr->table !== '')) {
506 4
                    if (isset($tableAliases[$expr->table])) {
507 4
                        $arr = $tableAliases[$expr->table];
508
                    } else {
509 2
                        $arr = [
510 4
                            $expr->table,
511 4
                            isset($expr->database) && ($expr->database !== '') ?
512 4
                                $expr->database : null,
513
                        ];
514
                    }
515
516 4
                    if (! in_array($arr, $ret['select_tables'])) {
517 4
                        $ret['select_tables'][] = $arr;
518
                    }
519
                } else {
520 4
                    $ret['select_expr'][] = $expr->expr;
521
                }
522
            }
523
524
            // If no tables names were found in the SELECT clause or if there
525
            // are expressions like * or COUNT(*), etc. tables names should be
526
            // extracted from the FROM clause.
527 4
            if (empty($ret['select_tables'])) {
528 4
                foreach ($statement->from as $expr) {
529 4
                    if (! isset($expr->table) || ($expr->table === '')) {
530
                        continue;
531
                    }
532
533 2
                    $arr = [
534 4
                        $expr->table,
535 4
                        isset($expr->database) && ($expr->database !== '') ?
536 4
                            $expr->database : null,
537
                    ];
538 4
                    if (in_array($arr, $ret['select_tables'])) {
539
                        continue;
540
                    }
541
542 4
                    $ret['select_tables'][] = $arr;
543
                }
544
            }
545
        }
546
547 4
        return $ret;
548
    }
549
550
    /**
551
     * Gets a list of all tables used in this statement.
552
     *
553
     * @param Statement $statement statement to be scanned
554
     *
555
     * @return array<int, string>
556
     */
557 40
    public static function getTables($statement)
558
    {
559 40
        $expressions = [];
560
561 40
        if (($statement instanceof InsertStatement) || ($statement instanceof ReplaceStatement)) {
562 8
            $expressions = [$statement->into->dest];
563 32
        } elseif ($statement instanceof UpdateStatement) {
564 8
            $expressions = $statement->tables;
565 24
        } elseif (($statement instanceof SelectStatement) || ($statement instanceof DeleteStatement)) {
566 8
            $expressions = $statement->from;
567 16
        } elseif (($statement instanceof AlterStatement) || ($statement instanceof TruncateStatement)) {
568 4
            $expressions = [$statement->table];
569 12
        } elseif ($statement instanceof DropStatement) {
570 8
            if (! $statement->options->has('TABLE')) {
571
                // No tables are dropped.
572 4
                return [];
573
            }
574
575 4
            $expressions = $statement->fields;
576 4
        } elseif ($statement instanceof RenameStatement) {
577 4
            foreach ($statement->renames as $rename) {
578 4
                $expressions[] = $rename->old;
579
            }
580
        }
581
582 36
        $ret = [];
583 36
        foreach ($expressions as $expr) {
584 36
            if (empty($expr->table)) {
585
                continue;
586
            }
587
588 36
            $expr->expr = null; // Force rebuild.
589 36
            $expr->alias = null; // Aliases are not required.
590 36
            $ret[] = Expression::build($expr);
591
        }
592
593 36
        return $ret;
594
    }
595
596
    /**
597
     * Gets a specific clause.
598
     *
599
     * @param Statement  $statement the parsed query that has to be modified
600
     * @param TokensList $list      the list of tokens
601
     * @param string     $clause    the clause to be returned
602
     * @param int|string $type      The type of the search.
603
     *                              If int,
604
     *                              -1 for everything that was before
605
     *                              0 only for the clause
606
     *                              1 for everything after
607
     *                              If string, the name of the first clause that
608
     *                              should not be included.
609
     * @param bool       $skipFirst whether to skip the first keyword in clause
610
     *
611
     * @return string
612
     */
613 20
    public static function getClause($statement, $list, $clause, $type = 0, $skipFirst = true)
614
    {
615
        /**
616
         * The index of the current clause.
617
         *
618
         * @var int
619
         */
620 20
        $currIdx = 0;
621
622
        /**
623
         * The count of brackets.
624
         * We keep track of them so we won't insert the clause in a subquery.
625
         *
626
         * @var int
627
         */
628 20
        $brackets = 0;
629
630
        /**
631
         * The string to be returned.
632
         *
633
         * @var string
634
         */
635 20
        $ret = '';
636
637
        /**
638
         * The clauses of this type of statement and their index.
639
         */
640 20
        $clauses = array_flip(array_keys($statement->getClauses()));
641
642
        /**
643
         * Lexer used for lexing the clause.
644
         */
645 20
        $lexer = new Lexer($clause);
646
647
        /**
648
         * The type of this clause.
649
         *
650
         * @var string
651
         */
652 20
        $clauseType = $lexer->list->getNextOfType(Token::TYPE_KEYWORD)->keyword;
653
654
        /**
655
         * The index of this clause.
656
         */
657 20
        $clauseIdx = $clauses[$clauseType] ?? -1;
658
659 20
        $firstClauseIdx = $clauseIdx;
660 20
        $lastClauseIdx = $clauseIdx;
661
662
        // Determining the behavior of this function.
663 20
        if ($type === -1) {
664 16
            $firstClauseIdx = -1; // Something small enough.
665 16
            $lastClauseIdx = $clauseIdx - 1;
666 20
        } elseif ($type === 1) {
667 16
            $firstClauseIdx = $clauseIdx + 1;
668 16
            $lastClauseIdx = 10000; // Something big enough.
669 12
        } elseif (is_string($type) && isset($clauses[$type])) {
670 8
            if ($clauses[$type] > $clauseIdx) {
671 8
                $firstClauseIdx = $clauseIdx + 1;
672 8
                $lastClauseIdx = $clauses[$type] - 1;
673
            } else {
674 4
                $firstClauseIdx = $clauses[$type] + 1;
675 4
                $lastClauseIdx = $clauseIdx - 1;
676
            }
677
        }
678
679
        // This option is unavailable for multiple clauses.
680 20
        if ($type !== 0) {
681 20
            $skipFirst = false;
682
        }
683
684 20
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
685 20
            $token = $list->tokens[$i];
686
687 20
            if ($token->type === Token::TYPE_COMMENT) {
688 4
                continue;
689
            }
690
691 20
            if ($token->type === Token::TYPE_OPERATOR) {
692 16
                if ($token->value === '(') {
693 16
                    ++$brackets;
694 16
                } elseif ($token->value === ')') {
695 16
                    --$brackets;
696
                }
697
            }
698
699 20
            if ($brackets === 0) {
700
                // Checking if the section was changed.
701
                if (
702 20
                    ($token->type === Token::TYPE_KEYWORD)
703 20
                    && isset($clauses[$token->keyword])
704 20
                    && ($clauses[$token->keyword] >= $currIdx)
705
                ) {
706 16
                    $currIdx = $clauses[$token->keyword];
707 16
                    if ($skipFirst && ($currIdx === $clauseIdx)) {
708
                        // This token is skipped (not added to the old
709
                        // clause) because it will be replaced.
710 8
                        continue;
711
                    }
712
                }
713
            }
714
715 20
            if (($firstClauseIdx > $currIdx) || ($currIdx > $lastClauseIdx)) {
716 20
                continue;
717
            }
718
719 20
            $ret .= $token->token;
720
        }
721
722 20
        return trim($ret);
723
    }
724
725
    /**
726
     * Builds a query by rebuilding the statement from the tokens list supplied
727
     * and replaces a clause.
728
     *
729
     * It is a very basic version of a query builder.
730
     *
731
     * @param Statement  $statement the parsed query that has to be modified
732
     * @param TokensList $list      the list of tokens
733
     * @param string     $old       The type of the clause that should be
734
     *                              replaced. This can be an entire clause.
735
     * @param string     $new       The new clause. If this parameter is omitted
736
     *                              it is considered to be equal with `$old`.
737
     * @param bool       $onlyType  whether only the type of the clause should
738
     *                              be replaced or the entire clause
739
     *
740
     * @return string
741
     */
742 16
    public static function replaceClause($statement, $list, $old, $new = null, $onlyType = false)
743
    {
744
        // TODO: Update the tokens list and the statement.
745
746 16
        if ($new === null) {
747 8
            $new = $old;
748
        }
749
750 16
        if ($onlyType) {
751 4
            return static::getClause($statement, $list, $old, -1, false) . ' ' .
752 4
                $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' .
753 4
                static::getClause($statement, $list, $old, 1, false);
754
        }
755
756 12
        return static::getClause($statement, $list, $old, -1, false) . ' ' .
757 12
            $new . ' ' . static::getClause($statement, $list, $old, 1, false);
758
    }
759
760
    /**
761
     * Builds a query by rebuilding the statement from the tokens list supplied
762
     * and replaces multiple clauses.
763
     *
764
     * @param Statement                      $statement the parsed query that has to be modified
765
     * @param TokensList                     $list      the list of tokens
766
     * @param array<int, array<int, string>> $ops       Clauses to be replaced. Contains multiple
767
     *                              arrays having two values: [$old, $new].
768
     *                              Clauses must be sorted.
769
     *
770
     * @return string
771
     */
772 4
    public static function replaceClauses($statement, $list, array $ops)
773
    {
774 4
        $count = count($ops);
775
776
        // Nothing to do.
777 4
        if ($count === 0) {
778 4
            return '';
779
        }
780
781
        /**
782
         * Value to be returned.
783
         *
784
         * @var string
785
         */
786 4
        $ret = '';
787
788
        // If there is only one clause, `replaceClause()` should be used.
789 4
        if ($count === 1) {
790 4
            return static::replaceClause($statement, $list, $ops[0][0], $ops[0][1]);
791
        }
792
793
        // Adding everything before first replacement.
794 4
        $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' ';
795
796
        // Doing replacements.
797 4
        foreach ($ops as $i => $clause) {
798 4
            $ret .= $clause[1] . ' ';
799
800
            // Adding everything between this and next replacement.
801 4
            if ($i + 1 === $count) {
802 4
                continue;
803
            }
804
805 4
            $ret .= static::getClause($statement, $list, $clause[0], $ops[$i + 1][0]) . ' ';
806
        }
807
808
        // Adding everything after the last replacement.
809 4
        return $ret . static::getClause($statement, $list, $ops[$count - 1][0], 1);
810
    }
811
812
    /**
813
     * Gets the first full statement in the query.
814
     *
815
     * @param string $query     the query to be analyzed
816
     * @param string $delimiter the delimiter to be used
817
     *
818
     * @return array<int, string|null> array containing the first full query,
819
     *                                 the remaining part of the query and the last delimiter
820
     * @psalm-return array{string|null, string, string|null}
821
     */
822 4
    public static function getFirstStatement($query, $delimiter = null)
823
    {
824 4
        $lexer = new Lexer($query, false, $delimiter);
825 4
        $list = $lexer->list;
826
827
        /**
828
         * Whether a full statement was found.
829
         *
830
         * @var bool
831
         */
832 4
        $fullStatement = false;
833
834
        /**
835
         * The first full statement.
836
         *
837
         * @var string
838
         */
839 4
        $statement = '';
840
841 4
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
842 4
            $token = $list->tokens[$list->idx];
843
844 4
            if ($token->type === Token::TYPE_COMMENT) {
845 4
                continue;
846
            }
847
848 4
            $statement .= $token->token;
849
850 4
            if (($token->type === Token::TYPE_DELIMITER) && ! empty($token->token)) {
851 4
                $delimiter = $token->token;
852 4
                $fullStatement = true;
853 4
                break;
854
            }
855
        }
856
857
        // No statement was found so we return the entire query as being the
858
        // remaining part.
859 4
        if (! $fullStatement) {
860
            return [
861 4
                null,
862 1
                $query,
863 1
                $delimiter,
864
            ];
865
        }
866
867
        // At least one query was found so we have to build the rest of the
868
        // remaining query.
869 4
        $query = '';
870 4
        for (++$list->idx; $list->idx < $list->count; ++$list->idx) {
871 4
            $query .= $list->tokens[$list->idx]->token;
872
        }
873
874
        return [
875 4
            trim($statement),
876 1
            $query,
877 1
            $delimiter,
878
        ];
879
    }
880
881
    /**
882
     * Gets a starting offset of a specific clause.
883
     *
884
     * @param Statement  $statement the parsed query that has to be modified
885
     * @param TokensList $list      the list of tokens
886
     * @param string     $clause    the clause to be returned
887
     *
888
     * @return int
889
     */
890 812
    public static function getClauseStartOffset($statement, $list, $clause)
891
    {
892
        /**
893
         * The count of brackets.
894
         * We keep track of them so we won't insert the clause in a subquery.
895
         *
896
         * @var int
897
         */
898 812
        $brackets = 0;
899
900
        /**
901
         * The clauses of this type of statement and their index.
902
         */
903 812
        $clauses = array_flip(array_keys($statement->getClauses()));
904
905 812
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
906 812
            $token = $list->tokens[$i];
907
908 812
            if ($token->type === Token::TYPE_COMMENT) {
909 68
                continue;
910
            }
911
912 812
            if ($token->type === Token::TYPE_OPERATOR) {
913 628
                if ($token->value === '(') {
914 312
                    ++$brackets;
915 628
                } elseif ($token->value === ')') {
916 316
                    --$brackets;
917
                }
918
            }
919
920 812
            if ($brackets !== 0) {
921 320
                continue;
922
            }
923
924
            if (
925 808
                ($token->type === Token::TYPE_KEYWORD)
926 808
                && isset($clauses[$token->keyword])
927 808
                && ($clause === $token->keyword)
928
            ) {
929 796
                return $i;
930
            }
931
932 808
            if ($token->keyword === 'UNION') {
933 20
                return -1;
934
            }
935
        }
936
937 812
        return -1;
938
    }
939
}
940