Passed
Push — master ( bb49d4...6b91b6 )
by William
03:49
created

Query::getAll()   D

Complexity

Conditions 21
Paths 4

Size

Total Lines 79
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 21.0595

Importance

Changes 0
Metric Value
cc 21
eloc 42
c 0
b 0
f 0
nc 4
nop 1
dl 0
loc 79
rs 4.1666
ccs 37
cts 39
cp 0.9487
crap 21.0595

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\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 62
            ($statement instanceof SelectStatement)
438 38
            || ($statement instanceof UpdateStatement)
439 124
            || ($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&array{
468
     *      select_expr?: (string|null)[],
469
     *      select_tables?: array{string, string|null}[],
470
     *      statement?: Statement|null, parser?: Parser
471
     * }
472
     */
473 4
    public static function getAll($query)
474
    {
475 4
        $parser = new Parser($query);
476
477 4
        if (empty($parser->statements[0])) {
478 4
            return static::getFlags(null, true);
479
        }
480
481 4
        $statement = $parser->statements[0];
482
483 4
        $ret = static::getFlags($statement, true);
484
485 4
        $ret['parser'] = $parser;
486 4
        $ret['statement'] = $statement;
487
488 4
        if ($statement instanceof SelectStatement) {
489 4
            $ret['select_tables'] = [];
490 4
            $ret['select_expr'] = [];
491
492
            // Finding tables' aliases and their associated real names.
493 4
            $tableAliases = [];
494 4
            foreach ($statement->from as $expr) {
495 4
                if (! isset($expr->table, $expr->alias) || ($expr->table === '') || ($expr->alias === '')) {
496 4
                    continue;
497
                }
498
499 4
                $tableAliases[$expr->alias] = [
500 4
                    $expr->table,
501 4
                    $expr->database ?? null,
502
                ];
503
            }
504
505
            // Trying to find selected tables only from the select expression.
506
            // Sometimes, this is not possible because the tables aren't defined
507
            // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film).
508 4
            foreach ($statement->expr as $expr) {
509 4
                if (isset($expr->table) && ($expr->table !== '')) {
510 4
                    if (isset($tableAliases[$expr->table])) {
511 4
                        $arr = $tableAliases[$expr->table];
512
                    } else {
513
                        $arr = [
514 4
                            $expr->table,
515 4
                            isset($expr->database) && ($expr->database !== '') ?
516 4
                                $expr->database : null,
517
                        ];
518
                    }
519
520 4
                    if (! in_array($arr, $ret['select_tables'])) {
521 4
                        $ret['select_tables'][] = $arr;
522
                    }
523
                } else {
524 4
                    $ret['select_expr'][] = $expr->expr;
525
                }
526
            }
527
528
            // If no tables names were found in the SELECT clause or if there
529
            // are expressions like * or COUNT(*), etc. tables names should be
530
            // extracted from the FROM clause.
531 4
            if (empty($ret['select_tables'])) {
532 4
                foreach ($statement->from as $expr) {
533 4
                    if (! isset($expr->table) || ($expr->table === '')) {
534
                        continue;
535
                    }
536
537
                    $arr = [
538 4
                        $expr->table,
539 4
                        isset($expr->database) && ($expr->database !== '') ?
540 4
                            $expr->database : null,
541
                    ];
542 4
                    if (in_array($arr, $ret['select_tables'])) {
543
                        continue;
544
                    }
545
546 4
                    $ret['select_tables'][] = $arr;
547
                }
548
            }
549
        }
550
551 4
        return $ret;
552
    }
553
554
    /**
555
     * Gets a list of all tables used in this statement.
556
     *
557
     * @param Statement $statement statement to be scanned
558
     *
559
     * @return array<int, string>
560
     */
561 40
    public static function getTables($statement)
562
    {
563 40
        $expressions = [];
564
565 40
        if (($statement instanceof InsertStatement) || ($statement instanceof ReplaceStatement)) {
566 8
            $expressions = [$statement->into->dest];
567 32
        } elseif ($statement instanceof UpdateStatement) {
568 8
            $expressions = $statement->tables;
569 24
        } elseif (($statement instanceof SelectStatement) || ($statement instanceof DeleteStatement)) {
570 8
            $expressions = $statement->from;
571 16
        } elseif (($statement instanceof AlterStatement) || ($statement instanceof TruncateStatement)) {
572 4
            $expressions = [$statement->table];
573 12
        } elseif ($statement instanceof DropStatement) {
574 8
            if (! $statement->options->has('TABLE')) {
575
                // No tables are dropped.
576 4
                return [];
577
            }
578
579 4
            $expressions = $statement->fields;
580 4
        } elseif ($statement instanceof RenameStatement) {
581 4
            foreach ($statement->renames as $rename) {
582 4
                $expressions[] = $rename->old;
583
            }
584
        }
585
586 36
        $ret = [];
587 36
        foreach ($expressions as $expr) {
588 36
            if (empty($expr->table)) {
589
                continue;
590
            }
591
592 36
            $expr->expr = null; // Force rebuild.
593 36
            $expr->alias = null; // Aliases are not required.
594 36
            $ret[] = Expression::build($expr);
595
        }
596
597 36
        return $ret;
598
    }
599
600
    /**
601
     * Gets a specific clause.
602
     *
603
     * @param Statement  $statement the parsed query that has to be modified
604
     * @param TokensList $list      the list of tokens
605
     * @param string     $clause    the clause to be returned
606
     * @param int|string $type      The type of the search.
607
     *                              If int,
608
     *                              -1 for everything that was before
609
     *                              0 only for the clause
610
     *                              1 for everything after
611
     *                              If string, the name of the first clause that
612
     *                              should not be included.
613
     * @param bool       $skipFirst whether to skip the first keyword in clause
614
     *
615
     * @return string
616
     */
617 20
    public static function getClause($statement, $list, $clause, $type = 0, $skipFirst = true)
618
    {
619
        /**
620
         * The index of the current clause.
621
         *
622
         * @var int
623
         */
624 20
        $currIdx = 0;
625
626
        /**
627
         * The count of brackets.
628
         * We keep track of them so we won't insert the clause in a subquery.
629
         *
630
         * @var int
631
         */
632 20
        $brackets = 0;
633
634
        /**
635
         * The string to be returned.
636
         *
637
         * @var string
638
         */
639 20
        $ret = '';
640
641
        /**
642
         * The clauses of this type of statement and their index.
643
         */
644 20
        $clauses = array_flip(array_keys($statement->getClauses()));
645
646
        /**
647
         * Lexer used for lexing the clause.
648
         */
649 20
        $lexer = new Lexer($clause);
650
651
        /**
652
         * The type of this clause.
653
         *
654
         * @var string
655
         */
656 20
        $clauseType = $lexer->list->getNextOfType(Token::TYPE_KEYWORD)->keyword;
657
658
        /**
659
         * The index of this clause.
660
         */
661 20
        $clauseIdx = $clauses[$clauseType] ?? -1;
662
663 20
        $firstClauseIdx = $clauseIdx;
664 20
        $lastClauseIdx = $clauseIdx;
665
666
        // Determining the behavior of this function.
667 20
        if ($type === -1) {
668 16
            $firstClauseIdx = -1; // Something small enough.
669 16
            $lastClauseIdx = $clauseIdx - 1;
670 20
        } elseif ($type === 1) {
671 16
            $firstClauseIdx = $clauseIdx + 1;
672 16
            $lastClauseIdx = 10000; // Something big enough.
673 12
        } elseif (is_string($type) && isset($clauses[$type])) {
674 8
            if ($clauses[$type] > $clauseIdx) {
675 8
                $firstClauseIdx = $clauseIdx + 1;
676 8
                $lastClauseIdx = $clauses[$type] - 1;
677
            } else {
678 4
                $firstClauseIdx = $clauses[$type] + 1;
679 4
                $lastClauseIdx = $clauseIdx - 1;
680
            }
681
        }
682
683
        // This option is unavailable for multiple clauses.
684 20
        if ($type !== 0) {
685 20
            $skipFirst = false;
686
        }
687
688 20
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
689 20
            $token = $list->tokens[$i];
690
691 20
            if ($token->type === Token::TYPE_COMMENT) {
692 4
                continue;
693
            }
694
695 20
            if ($token->type === Token::TYPE_OPERATOR) {
696 16
                if ($token->value === '(') {
697 16
                    ++$brackets;
698 16
                } elseif ($token->value === ')') {
699 16
                    --$brackets;
700
                }
701
            }
702
703 20
            if ($brackets === 0) {
704
                // Checking if the section was changed.
705
                if (
706 20
                    ($token->type === Token::TYPE_KEYWORD)
707 20
                    && isset($clauses[$token->keyword])
708 20
                    && ($clauses[$token->keyword] >= $currIdx)
709
                ) {
710 16
                    $currIdx = $clauses[$token->keyword];
711 16
                    if ($skipFirst && ($currIdx === $clauseIdx)) {
712
                        // This token is skipped (not added to the old
713
                        // clause) because it will be replaced.
714 8
                        continue;
715
                    }
716
                }
717
            }
718
719 20
            if (($firstClauseIdx > $currIdx) || ($currIdx > $lastClauseIdx)) {
720 20
                continue;
721
            }
722
723 20
            $ret .= $token->token;
724
        }
725
726 20
        return trim($ret);
727
    }
728
729
    /**
730
     * Builds a query by rebuilding the statement from the tokens list supplied
731
     * and replaces a clause.
732
     *
733
     * It is a very basic version of a query builder.
734
     *
735
     * @param Statement  $statement the parsed query that has to be modified
736
     * @param TokensList $list      the list of tokens
737
     * @param string     $old       The type of the clause that should be
738
     *                              replaced. This can be an entire clause.
739
     * @param string     $new       The new clause. If this parameter is omitted
740
     *                              it is considered to be equal with `$old`.
741
     * @param bool       $onlyType  whether only the type of the clause should
742
     *                              be replaced or the entire clause
743
     *
744
     * @return string
745
     */
746 16
    public static function replaceClause($statement, $list, $old, $new = null, $onlyType = false)
747
    {
748
        // TODO: Update the tokens list and the statement.
749
750 16
        if ($new === null) {
751 8
            $new = $old;
752
        }
753
754 16
        if ($onlyType) {
755 4
            return static::getClause($statement, $list, $old, -1, false) . ' ' .
756 4
                $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' .
757 4
                static::getClause($statement, $list, $old, 1, false);
758
        }
759
760 12
        return static::getClause($statement, $list, $old, -1, false) . ' ' .
761 12
            $new . ' ' . static::getClause($statement, $list, $old, 1, false);
762
    }
763
764
    /**
765
     * Builds a query by rebuilding the statement from the tokens list supplied
766
     * and replaces multiple clauses.
767
     *
768
     * @param Statement                      $statement the parsed query that has to be modified
769
     * @param TokensList                     $list      the list of tokens
770
     * @param array<int, array<int, string>> $ops       Clauses to be replaced. Contains multiple
771
     *                              arrays having two values: [$old, $new].
772
     *                              Clauses must be sorted.
773
     *
774
     * @return string
775
     */
776 4
    public static function replaceClauses($statement, $list, array $ops)
777
    {
778 4
        $count = count($ops);
779
780
        // Nothing to do.
781 4
        if ($count === 0) {
782 4
            return '';
783
        }
784
785
        /**
786
         * Value to be returned.
787
         *
788
         * @var string
789
         */
790 4
        $ret = '';
791
792
        // If there is only one clause, `replaceClause()` should be used.
793 4
        if ($count === 1) {
794 4
            return static::replaceClause($statement, $list, $ops[0][0], $ops[0][1]);
795
        }
796
797
        // Adding everything before first replacement.
798 4
        $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' ';
799
800
        // Doing replacements.
801 4
        foreach ($ops as $i => $clause) {
802 4
            $ret .= $clause[1] . ' ';
803
804
            // Adding everything between this and next replacement.
805 4
            if ($i + 1 === $count) {
806 4
                continue;
807
            }
808
809 4
            $ret .= static::getClause($statement, $list, $clause[0], $ops[$i + 1][0]) . ' ';
810
        }
811
812
        // Adding everything after the last replacement.
813 4
        return $ret . static::getClause($statement, $list, $ops[$count - 1][0], 1);
814
    }
815
816
    /**
817
     * Gets the first full statement in the query.
818
     *
819
     * @param string $query     the query to be analyzed
820
     * @param string $delimiter the delimiter to be used
821
     *
822
     * @return array<int, string|null> array containing the first full query,
823
     *                                 the remaining part of the query and the last delimiter
824
     * @psalm-return array{string|null, string, string|null}
825
     */
826 4
    public static function getFirstStatement($query, $delimiter = null)
827
    {
828 4
        $lexer = new Lexer($query, false, $delimiter);
829 4
        $list = $lexer->list;
830
831
        /**
832
         * Whether a full statement was found.
833
         *
834
         * @var bool
835
         */
836 4
        $fullStatement = false;
837
838
        /**
839
         * The first full statement.
840
         *
841
         * @var string
842
         */
843 4
        $statement = '';
844
845 4
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
846 4
            $token = $list->tokens[$list->idx];
847
848 4
            if ($token->type === Token::TYPE_COMMENT) {
849 4
                continue;
850
            }
851
852 4
            $statement .= $token->token;
853
854 4
            if (($token->type === Token::TYPE_DELIMITER) && ! empty($token->token)) {
855 4
                $delimiter = $token->token;
856 4
                $fullStatement = true;
857 4
                break;
858
            }
859
        }
860
861
        // No statement was found so we return the entire query as being the
862
        // remaining part.
863 4
        if (! $fullStatement) {
864
            return [
865 4
                null,
866 2
                $query,
867 2
                $delimiter,
868
            ];
869
        }
870
871
        // At least one query was found so we have to build the rest of the
872
        // remaining query.
873 4
        $query = '';
874 4
        for (++$list->idx; $list->idx < $list->count; ++$list->idx) {
875 4
            $query .= $list->tokens[$list->idx]->token;
876
        }
877
878
        return [
879 4
            trim($statement),
880 2
            $query,
881 2
            $delimiter,
882
        ];
883
    }
884
885
    /**
886
     * Gets a starting offset of a specific clause.
887
     *
888
     * @param Statement  $statement the parsed query that has to be modified
889
     * @param TokensList $list      the list of tokens
890
     * @param string     $clause    the clause to be returned
891
     *
892
     * @return int
893
     */
894 820
    public static function getClauseStartOffset($statement, $list, $clause)
895
    {
896
        /**
897
         * The count of brackets.
898
         * We keep track of them so we won't insert the clause in a subquery.
899
         *
900
         * @var int
901
         */
902 820
        $brackets = 0;
903
904
        /**
905
         * The clauses of this type of statement and their index.
906
         */
907 820
        $clauses = array_flip(array_keys($statement->getClauses()));
908
909 820
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
910 820
            $token = $list->tokens[$i];
911
912 820
            if ($token->type === Token::TYPE_COMMENT) {
913 68
                continue;
914
            }
915
916 820
            if ($token->type === Token::TYPE_OPERATOR) {
917 632
                if ($token->value === '(') {
918 316
                    ++$brackets;
919 632
                } elseif ($token->value === ')') {
920 320
                    --$brackets;
921
                }
922
            }
923
924 820
            if ($brackets !== 0) {
925 324
                continue;
926
            }
927
928
            if (
929 816
                ($token->type === Token::TYPE_KEYWORD)
930 816
                && isset($clauses[$token->keyword])
931 816
                && ($clause === $token->keyword)
932
            ) {
933 804
                return $i;
934
            }
935
936 816
            if ($token->keyword === 'UNION') {
937 20
                return -1;
938
            }
939
        }
940
941 820
        return -1;
942
    }
943
}
944