Passed
Pull Request — master (#386)
by William
03:14
created

Query   F

Complexity

Total Complexity 128

Size/Duplication

Total Lines 864
Duplicated Lines 0 %

Test Coverage

Coverage 98.92%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 320
dl 0
loc 864
ccs 274
cts 277
cp 0.9892
rs 2
c 2
b 0
f 0
wmc 128

9 Methods

Rating   Name   Duplication   Size   Complexity  
D getAll() 0 79 21
B getClauseStartOffset() 0 48 11
A replaceClauses() 0 38 5
F getFlagsSelect() 0 65 20
A replaceClause() 0 16 3
C getTables() 0 37 14
F getClause() 0 110 20
B getFirstStatement() 0 56 7
F getFlags() 0 85 27

How to fix   Complexity   

Complex Class

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Query, and based on these observations, apply Extract Interface, too.

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
            ($statement instanceof SelectStatement)
438
            || ($statement instanceof UpdateStatement)
439
            || ($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
                $query,
863
                $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
            $query,
877
            $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