Passed
Push — master ( 8fddb4...ad6c5d )
by William
02:44
created

Query::getClauseStartOffset()   B

Complexity

Conditions 11
Paths 18

Size

Total Lines 48
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 21
nc 18
nop 3
dl 0
loc 48
ccs 21
cts 21
cp 1
crap 11
rs 7.3166
c 0
b 0
f 0

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 31
            ($statement instanceof SelectStatement)
438 19
            || ($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 1
                        $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 1
                    $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 1
                $query,
867 1
                $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 1
            $query,
881 1
            $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 812
    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 812
        $brackets = 0;
903
904
        /**
905
         * The clauses of this type of statement and their index.
906
         */
907 812
        $clauses = array_flip(array_keys($statement->getClauses()));
908
909 812
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
910 812
            $token = $list->tokens[$i];
911
912 812
            if ($token->type === Token::TYPE_COMMENT) {
913 68
                continue;
914
            }
915
916 812
            if ($token->type === Token::TYPE_OPERATOR) {
917 628
                if ($token->value === '(') {
918 312
                    ++$brackets;
919 628
                } elseif ($token->value === ')') {
920 316
                    --$brackets;
921
                }
922
            }
923
924 812
            if ($brackets !== 0) {
925 320
                continue;
926
            }
927
928
            if (
929 808
                ($token->type === Token::TYPE_KEYWORD)
930 808
                && isset($clauses[$token->keyword])
931 808
                && ($clause === $token->keyword)
932
            ) {
933 796
                return $i;
934
            }
935
936 808
            if ($token->keyword === 'UNION') {
937 20
                return -1;
938
            }
939
        }
940
941 812
        return -1;
942
    }
943
}
944