Passed
Push — master ( 0efc01...ef206b )
by Maurício
02:53
created

Query::getTables()   C

Complexity

Conditions 14
Paths 22

Size

Total Lines 37
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 14.0125

Importance

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

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Statement utilities.
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\SqlParser\Utils;
9
10
use PhpMyAdmin\SqlParser\Components\Expression;
11
use PhpMyAdmin\SqlParser\Lexer;
12
use PhpMyAdmin\SqlParser\Parser;
13
use PhpMyAdmin\SqlParser\Statement;
14
use PhpMyAdmin\SqlParser\Statements\AlterStatement;
15
use PhpMyAdmin\SqlParser\Statements\AnalyzeStatement;
16
use PhpMyAdmin\SqlParser\Statements\CallStatement;
17
use PhpMyAdmin\SqlParser\Statements\CheckStatement;
18
use PhpMyAdmin\SqlParser\Statements\ChecksumStatement;
19
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
20
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
21
use PhpMyAdmin\SqlParser\Statements\DropStatement;
22
use PhpMyAdmin\SqlParser\Statements\ExplainStatement;
23
use PhpMyAdmin\SqlParser\Statements\InsertStatement;
24
use PhpMyAdmin\SqlParser\Statements\LoadStatement;
25
use PhpMyAdmin\SqlParser\Statements\OptimizeStatement;
26
use PhpMyAdmin\SqlParser\Statements\RenameStatement;
27
use PhpMyAdmin\SqlParser\Statements\RepairStatement;
28
use PhpMyAdmin\SqlParser\Statements\ReplaceStatement;
29
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
30
use PhpMyAdmin\SqlParser\Statements\SetStatement;
31
use PhpMyAdmin\SqlParser\Statements\ShowStatement;
32
use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
33
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
34
use PhpMyAdmin\SqlParser\Token;
35
use PhpMyAdmin\SqlParser\TokensList;
36
37
use function array_flip;
38
use function array_keys;
39
use function count;
40
use function in_array;
41
use function is_string;
42
use function trim;
43
44
/**
45
 * Statement utilities.
46
 */
47
class Query
48
{
49
    /**
50
     * Functions that set the flag `is_func`.
51
     *
52
     * @var string[]
53
     */
54
    public static $FUNCTIONS = [
55
        'SUM',
56
        'AVG',
57
        'STD',
58
        'STDDEV',
59
        'MIN',
60
        'MAX',
61
        'BIT_OR',
62
        'BIT_AND',
63
    ];
64
65
    /** @var array<string,false> */
66
    public static $ALLFLAGS = [
67
        /*
68
         * select ... DISTINCT ...
69
         */
70
        'distinct' => false,
71
72
        /*
73
         * drop ... DATABASE ...
74
         */
75
        'drop_database' => false,
76
77
        /*
78
         * ... GROUP BY ...
79
         */
80
        'group' => false,
81
82
        /*
83
         * ... HAVING ...
84
         */
85
        'having' => false,
86
87
        /*
88
         * INSERT ...
89
         * or
90
         * REPLACE ...
91
         * or
92
         * DELETE ...
93
         */
94
        'is_affected' => false,
95
96
        /*
97
         * select ... PROCEDURE ANALYSE( ... ) ...
98
         */
99
        'is_analyse' => false,
100
101
        /*
102
         * select COUNT( ... ) ...
103
         */
104
        'is_count' => false,
105
106
        /*
107
         * DELETE ...
108
         */
109
        'is_delete' => false, // @deprecated; use `querytype`
110
111
        /*
112
         * EXPLAIN ...
113
         */
114
        'is_explain' => false, // @deprecated; use `querytype`
115
116
        /*
117
         * select ... INTO OUTFILE ...
118
         */
119
        'is_export' => false,
120
121
        /*
122
         * select FUNC( ... ) ...
123
         */
124
        'is_func' => false,
125
126
        /*
127
         * select ... GROUP BY ...
128
         * or
129
         * select ... HAVING ...
130
         */
131
        'is_group' => false,
132
133
        /*
134
         * INSERT ...
135
         * or
136
         * REPLACE ...
137
         * or
138
         * LOAD DATA ...
139
         */
140
        'is_insert' => false,
141
142
        /*
143
         * ANALYZE ...
144
         * or
145
         * CHECK ...
146
         * or
147
         * CHECKSUM ...
148
         * or
149
         * OPTIMIZE ...
150
         * or
151
         * REPAIR ...
152
         */
153
        'is_maint' => false,
154
155
        /*
156
         * CALL ...
157
         */
158
        'is_procedure' => false,
159
160
        /*
161
         * REPLACE ...
162
         */
163
        'is_replace' => false, // @deprecated; use `querytype`
164
165
        /*
166
         * SELECT ...
167
         */
168
        'is_select' => false, // @deprecated; use `querytype`
169
170
        /*
171
         * SHOW ...
172
         */
173
        'is_show' => false, // @deprecated; use `querytype`
174
175
        /*
176
         * Contains a subquery.
177
         */
178
        'is_subquery' => false,
179
180
        /*
181
         * ... JOIN ...
182
         */
183
        'join' => false,
184
185
        /*
186
         * ... LIMIT ...
187
         */
188
        'limit' => false,
189
190
        /*
191
         * TODO
192
         */
193
        'offset' => false,
194
195
        /*
196
         * ... ORDER ...
197
         */
198
        'order' => false,
199
200
        /*
201
         * The type of the query (which is usually the first keyword of
202
         * the statement).
203
         */
204
        'querytype' => false,
205
206
        /*
207
         * Whether a page reload is required.
208
         */
209
        'reload' => false,
210
211
        /*
212
         * SELECT ... FROM ...
213
         */
214
        'select_from' => false,
215
216
        /*
217
         * ... UNION ...
218
         */
219
        'union' => false,
220
    ];
221
222
    /**
223
     * Gets an array with flags select statement has.
224
     *
225
     * @param SelectStatement $statement the statement to be processed
226
     * @param array           $flags     flags set so far
227
     *
228
     * @return array
229
     */
230 52
    private static function getFlagsSelect($statement, $flags)
231
    {
232 52
        $flags['querytype'] = 'SELECT';
233 52
        $flags['is_select'] = true;
234
235 52
        if (! empty($statement->from)) {
236 44
            $flags['select_from'] = true;
237
        }
238
239 52
        if ($statement->options->has('DISTINCT')) {
240 4
            $flags['distinct'] = true;
241
        }
242
243 52
        if (! empty($statement->group) || ! empty($statement->having)) {
244 8
            $flags['is_group'] = true;
245
        }
246
247 52
        if (! empty($statement->into) && ($statement->into->type === 'OUTFILE')) {
248 4
            $flags['is_export'] = true;
249
        }
250
251 52
        $expressions = $statement->expr;
252 52
        if (! empty($statement->join)) {
253 4
            foreach ($statement->join as $join) {
254 4
                $expressions[] = $join->expr;
255
            }
256
        }
257
258 52
        foreach ($expressions as $expr) {
259 52
            if (! empty($expr->function)) {
260 4
                if ($expr->function === 'COUNT') {
261 4
                    $flags['is_count'] = true;
262 4
                } elseif (in_array($expr->function, static::$FUNCTIONS)) {
263 4
                    $flags['is_func'] = true;
264
                }
265
            }
266
267 52
            if (empty($expr->subquery)) {
268 48
                continue;
269
            }
270
271 4
            $flags['is_subquery'] = true;
272
        }
273
274 52
        if (! empty($statement->procedure) && ($statement->procedure->name === 'ANALYSE')) {
275 4
            $flags['is_analyse'] = true;
276
        }
277
278 52
        if (! empty($statement->group)) {
279 4
            $flags['group'] = true;
280
        }
281
282 52
        if (! empty($statement->having)) {
283 4
            $flags['having'] = true;
284
        }
285
286 52
        if (! empty($statement->union)) {
287 4
            $flags['union'] = true;
288
        }
289
290 52
        if (! empty($statement->join)) {
291 4
            $flags['join'] = true;
292
        }
293
294 52
        return $flags;
295
    }
296
297
    /**
298
     * Gets an array with flags this statement has.
299
     *
300
     * @param Statement|null $statement the statement to be processed
301
     * @param bool           $all       if `false`, false values will not be included
302
     *
303
     * @return array
304
     */
305 124
    public static function getFlags($statement, $all = false)
306
    {
307 124
        $flags = ['querytype' => false];
308 124
        if ($all) {
309 4
            $flags = self::$ALLFLAGS;
310
        }
311
312 124
        if ($statement instanceof AlterStatement) {
313 4
            $flags['querytype'] = 'ALTER';
314 4
            $flags['reload'] = true;
315 120
        } elseif ($statement instanceof CreateStatement) {
316 4
            $flags['querytype'] = 'CREATE';
317 4
            $flags['reload'] = true;
318 116
        } elseif ($statement instanceof AnalyzeStatement) {
319 4
            $flags['querytype'] = 'ANALYZE';
320 4
            $flags['is_maint'] = true;
321 112
        } elseif ($statement instanceof CheckStatement) {
322 4
            $flags['querytype'] = 'CHECK';
323 4
            $flags['is_maint'] = true;
324 108
        } elseif ($statement instanceof ChecksumStatement) {
325 4
            $flags['querytype'] = 'CHECKSUM';
326 4
            $flags['is_maint'] = true;
327 104
        } elseif ($statement instanceof OptimizeStatement) {
328 4
            $flags['querytype'] = 'OPTIMIZE';
329 4
            $flags['is_maint'] = true;
330 100
        } elseif ($statement instanceof RepairStatement) {
331 4
            $flags['querytype'] = 'REPAIR';
332 4
            $flags['is_maint'] = true;
333 96
        } elseif ($statement instanceof CallStatement) {
334 4
            $flags['querytype'] = 'CALL';
335 4
            $flags['is_procedure'] = true;
336 92
        } elseif ($statement instanceof DeleteStatement) {
337 4
            $flags['querytype'] = 'DELETE';
338 4
            $flags['is_delete'] = true;
339 4
            $flags['is_affected'] = true;
340 88
        } elseif ($statement instanceof DropStatement) {
341 8
            $flags['querytype'] = 'DROP';
342 8
            $flags['reload'] = true;
343
344 8
            if ($statement->options->has('DATABASE') || $statement->options->has('SCHEMA')) {
345 8
                $flags['drop_database'] = true;
346
            }
347 80
        } elseif ($statement instanceof ExplainStatement) {
348 4
            $flags['querytype'] = 'EXPLAIN';
349 4
            $flags['is_explain'] = true;
350 76
        } elseif ($statement instanceof InsertStatement) {
351 4
            $flags['querytype'] = 'INSERT';
352 4
            $flags['is_affected'] = true;
353 4
            $flags['is_insert'] = true;
354 72
        } elseif ($statement instanceof LoadStatement) {
355 4
            $flags['querytype'] = 'LOAD';
356 4
            $flags['is_affected'] = true;
357 4
            $flags['is_insert'] = true;
358 68
        } elseif ($statement instanceof ReplaceStatement) {
359 4
            $flags['querytype'] = 'REPLACE';
360 4
            $flags['is_affected'] = true;
361 4
            $flags['is_replace'] = true;
362 4
            $flags['is_insert'] = true;
363 64
        } elseif ($statement instanceof SelectStatement) {
364 52
            $flags = self::getFlagsSelect($statement, $flags);
365 16
        } elseif ($statement instanceof ShowStatement) {
366 4
            $flags['querytype'] = 'SHOW';
367 4
            $flags['is_show'] = true;
368 12
        } elseif ($statement instanceof UpdateStatement) {
369 4
            $flags['querytype'] = 'UPDATE';
370 4
            $flags['is_affected'] = true;
371 8
        } elseif ($statement instanceof SetStatement) {
372 4
            $flags['querytype'] = 'SET';
373
        }
374
375
        if (
376 124
            ($statement instanceof SelectStatement)
377 76
            || ($statement instanceof UpdateStatement)
378 124
            || ($statement instanceof DeleteStatement)
379
        ) {
380 60
            if (! empty($statement->limit)) {
381 8
                $flags['limit'] = true;
382
            }
383
384 60
            if (! empty($statement->order)) {
385 8
                $flags['order'] = true;
386
            }
387
        }
388
389 124
        return $flags;
390
    }
391
392
    /**
393
     * Parses a query and gets all information about it.
394
     *
395
     * @param string $query the query to be parsed
396
     *
397
     * @return array The array returned is the one returned by
398
     *               `static::getFlags()`, with the following keys added:
399
     *               - parser - the parser used to analyze the query;
400
     *               - statement - the first statement resulted from parsing;
401
     *               - select_tables - the real name of the tables selected;
402
     *               if there are no table names in the `SELECT`
403
     *               expressions, the table names are fetched from the
404
     *               `FROM` expressions
405
     *               - select_expr - selected expressions
406
     */
407 4
    public static function getAll($query)
408
    {
409 4
        $parser = new Parser($query);
410
411 4
        if (empty($parser->statements[0])) {
412 4
            return static::getFlags(null, true);
413
        }
414
415 4
        $statement = $parser->statements[0];
416
417 4
        $ret = static::getFlags($statement, true);
418
419 4
        $ret['parser'] = $parser;
420 4
        $ret['statement'] = $statement;
421
422 4
        if ($statement instanceof SelectStatement) {
423 4
            $ret['select_tables'] = [];
424 4
            $ret['select_expr'] = [];
425
426
            // Finding tables' aliases and their associated real names.
427 4
            $tableAliases = [];
428 4
            foreach ($statement->from as $expr) {
429 4
                if (! isset($expr->table, $expr->alias) || ($expr->table === '') || ($expr->alias === '')) {
430 4
                    continue;
431
                }
432
433 4
                $tableAliases[$expr->alias] = [
434 4
                    $expr->table,
435 4
                    $expr->database ?? null,
436
                ];
437
            }
438
439
            // Trying to find selected tables only from the select expression.
440
            // Sometimes, this is not possible because the tables aren't defined
441
            // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film).
442 4
            foreach ($statement->expr as $expr) {
443 4
                if (isset($expr->table) && ($expr->table !== '')) {
444 4
                    if (isset($tableAliases[$expr->table])) {
445 4
                        $arr = $tableAliases[$expr->table];
446
                    } else {
447 1
                        $arr = [
448 4
                            $expr->table,
449 4
                            isset($expr->database) && ($expr->database !== '') ?
450 4
                                $expr->database : null,
451
                        ];
452
                    }
453
454 4
                    if (! in_array($arr, $ret['select_tables'])) {
455 4
                        $ret['select_tables'][] = $arr;
456
                    }
457
                } else {
458 4
                    $ret['select_expr'][] = $expr->expr;
459
                }
460
            }
461
462
            // If no tables names were found in the SELECT clause or if there
463
            // are expressions like * or COUNT(*), etc. tables names should be
464
            // extracted from the FROM clause.
465 4
            if (empty($ret['select_tables'])) {
466 4
                foreach ($statement->from as $expr) {
467 4
                    if (! isset($expr->table) || ($expr->table === '')) {
468
                        continue;
469
                    }
470
471 1
                    $arr = [
472 4
                        $expr->table,
473 4
                        isset($expr->database) && ($expr->database !== '') ?
474 4
                            $expr->database : null,
475
                    ];
476 4
                    if (in_array($arr, $ret['select_tables'])) {
477
                        continue;
478
                    }
479
480 4
                    $ret['select_tables'][] = $arr;
481
                }
482
            }
483
        }
484
485 4
        return $ret;
486
    }
487
488
    /**
489
     * Gets a list of all tables used in this statement.
490
     *
491
     * @param Statement $statement statement to be scanned
492
     *
493
     * @return array
494
     */
495 40
    public static function getTables($statement)
496
    {
497 40
        $expressions = [];
498
499 40
        if (($statement instanceof InsertStatement) || ($statement instanceof ReplaceStatement)) {
500 8
            $expressions = [$statement->into->dest];
501 32
        } elseif ($statement instanceof UpdateStatement) {
502 8
            $expressions = $statement->tables;
503 24
        } elseif (($statement instanceof SelectStatement) || ($statement instanceof DeleteStatement)) {
504 8
            $expressions = $statement->from;
505 16
        } elseif (($statement instanceof AlterStatement) || ($statement instanceof TruncateStatement)) {
506 4
            $expressions = [$statement->table];
507 12
        } elseif ($statement instanceof DropStatement) {
508 8
            if (! $statement->options->has('TABLE')) {
509
                // No tables are dropped.
510 4
                return [];
511
            }
512
513 4
            $expressions = $statement->fields;
514 4
        } elseif ($statement instanceof RenameStatement) {
515 4
            foreach ($statement->renames as $rename) {
516 4
                $expressions[] = $rename->old;
517
            }
518
        }
519
520 36
        $ret = [];
521 36
        foreach ($expressions as $expr) {
522 36
            if (empty($expr->table)) {
523
                continue;
524
            }
525
526 36
            $expr->expr = null; // Force rebuild.
527 36
            $expr->alias = null; // Aliases are not required.
528 36
            $ret[] = Expression::build($expr);
529
        }
530
531 36
        return $ret;
532
    }
533
534
    /**
535
     * Gets a specific clause.
536
     *
537
     * @param Statement  $statement the parsed query that has to be modified
538
     * @param TokensList $list      the list of tokens
539
     * @param string     $clause    the clause to be returned
540
     * @param int|string $type      The type of the search.
541
     *                              If int,
542
     *                              -1 for everything that was before
543
     *                              0 only for the clause
544
     *                              1 for everything after
545
     *                              If string, the name of the first clause that
546
     *                              should not be included.
547
     * @param bool       $skipFirst whether to skip the first keyword in clause
548
     *
549
     * @return string
550
     */
551 20
    public static function getClause($statement, $list, $clause, $type = 0, $skipFirst = true)
552
    {
553
        /**
554
         * The index of the current clause.
555
         *
556
         * @var int
557
         */
558 20
        $currIdx = 0;
559
560
        /**
561
         * The count of brackets.
562
         * We keep track of them so we won't insert the clause in a subquery.
563
         *
564
         * @var int
565
         */
566 20
        $brackets = 0;
567
568
        /**
569
         * The string to be returned.
570
         *
571
         * @var string
572
         */
573 20
        $ret = '';
574
575
        /**
576
         * The clauses of this type of statement and their index.
577
         *
578
         * @var array
579
         */
580 20
        $clauses = array_flip(array_keys($statement->getClauses()));
581
582
        /**
583
         * Lexer used for lexing the clause.
584
         *
585
         * @var Lexer
586
         */
587 20
        $lexer = new Lexer($clause);
588
589
        /**
590
         * The type of this clause.
591
         *
592
         * @var string
593
         */
594 20
        $clauseType = $lexer->list->getNextOfType(Token::TYPE_KEYWORD)->keyword;
595
596
        /**
597
         * The index of this clause.
598
         *
599
         * @var int
600
         */
601 20
        $clauseIdx = $clauses[$clauseType] ?? -1;
602
603 20
        $firstClauseIdx = $clauseIdx;
604 20
        $lastClauseIdx = $clauseIdx;
605
606
        // Determining the behavior of this function.
607 20
        if ($type === -1) {
608 16
            $firstClauseIdx = -1; // Something small enough.
609 16
            $lastClauseIdx = $clauseIdx - 1;
610 20
        } elseif ($type === 1) {
611 16
            $firstClauseIdx = $clauseIdx + 1;
612 16
            $lastClauseIdx = 10000; // Something big enough.
613 12
        } elseif (is_string($type) && isset($clauses[$type])) {
614 8
            if ($clauses[$type] > $clauseIdx) {
615 8
                $firstClauseIdx = $clauseIdx + 1;
616 8
                $lastClauseIdx = $clauses[$type] - 1;
617
            } else {
618 4
                $firstClauseIdx = $clauses[$type] + 1;
619 4
                $lastClauseIdx = $clauseIdx - 1;
620
            }
621
        }
622
623
        // This option is unavailable for multiple clauses.
624 20
        if ($type !== 0) {
625 20
            $skipFirst = false;
626
        }
627
628 20
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
629 20
            $token = $list->tokens[$i];
630
631 20
            if ($token->type === Token::TYPE_COMMENT) {
632 4
                continue;
633
            }
634
635 20
            if ($token->type === Token::TYPE_OPERATOR) {
636 16
                if ($token->value === '(') {
637 16
                    ++$brackets;
638 16
                } elseif ($token->value === ')') {
639 16
                    --$brackets;
640
                }
641
            }
642
643 20
            if ($brackets === 0) {
644
                // Checking if the section was changed.
645
                if (
646 20
                    ($token->type === Token::TYPE_KEYWORD)
647 20
                    && isset($clauses[$token->keyword])
648 20
                    && ($clauses[$token->keyword] >= $currIdx)
649
                ) {
650 16
                    $currIdx = $clauses[$token->keyword];
651 16
                    if ($skipFirst && ($currIdx === $clauseIdx)) {
652
                        // This token is skipped (not added to the old
653
                        // clause) because it will be replaced.
654 8
                        continue;
655
                    }
656
                }
657
            }
658
659 20
            if (($firstClauseIdx > $currIdx) || ($currIdx > $lastClauseIdx)) {
660 20
                continue;
661
            }
662
663 20
            $ret .= $token->token;
664
        }
665
666 20
        return trim($ret);
667
    }
668
669
    /**
670
     * Builds a query by rebuilding the statement from the tokens list supplied
671
     * and replaces a clause.
672
     *
673
     * It is a very basic version of a query builder.
674
     *
675
     * @param Statement  $statement the parsed query that has to be modified
676
     * @param TokensList $list      the list of tokens
677
     * @param string     $old       The type of the clause that should be
678
     *                              replaced. This can be an entire clause.
679
     * @param string     $new       The new clause. If this parameter is omitted
680
     *                              it is considered to be equal with `$old`.
681
     * @param bool       $onlyType  whether only the type of the clause should
682
     *                              be replaced or the entire clause
683
     *
684
     * @return string
685
     */
686 16
    public static function replaceClause($statement, $list, $old, $new = null, $onlyType = false)
687
    {
688
        // TODO: Update the tokens list and the statement.
689
690 16
        if ($new === null) {
691 8
            $new = $old;
692
        }
693
694 16
        if ($onlyType) {
695 4
            return static::getClause($statement, $list, $old, -1, false) . ' ' .
696 4
                $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' .
697 4
                static::getClause($statement, $list, $old, 1, false);
698
        }
699
700 12
        return static::getClause($statement, $list, $old, -1, false) . ' ' .
701 12
            $new . ' ' . static::getClause($statement, $list, $old, 1, false);
702
    }
703
704
    /**
705
     * Builds a query by rebuilding the statement from the tokens list supplied
706
     * and replaces multiple clauses.
707
     *
708
     * @param Statement  $statement the parsed query that has to be modified
709
     * @param TokensList $list      the list of tokens
710
     * @param array      $ops       Clauses to be replaced. Contains multiple
711
     *                              arrays having two values: [$old, $new].
712
     *                              Clauses must be sorted.
713
     *
714
     * @return string
715
     */
716 4
    public static function replaceClauses($statement, $list, array $ops)
717
    {
718 4
        $count = count($ops);
719
720
        // Nothing to do.
721 4
        if ($count === 0) {
722 4
            return '';
723
        }
724
725
        /**
726
         * Value to be returned.
727
         *
728
         * @var string
729
         */
730 4
        $ret = '';
731
732
        // If there is only one clause, `replaceClause()` should be used.
733 4
        if ($count === 1) {
734 4
            return static::replaceClause($statement, $list, $ops[0][0], $ops[0][1]);
735
        }
736
737
        // Adding everything before first replacement.
738 4
        $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' ';
739
740
        // Doing replacements.
741 4
        foreach ($ops as $i => $clause) {
742 4
            $ret .= $clause[1] . ' ';
743
744
            // Adding everything between this and next replacement.
745 4
            if ($i + 1 === $count) {
746 4
                continue;
747
            }
748
749 4
            $ret .= static::getClause($statement, $list, $clause[0], $ops[$i + 1][0]) . ' ';
750
        }
751
752
        // Adding everything after the last replacement.
753 4
        return $ret . static::getClause($statement, $list, $ops[$count - 1][0], 1);
754
    }
755
756
    /**
757
     * Gets the first full statement in the query.
758
     *
759
     * @param string $query     the query to be analyzed
760
     * @param string $delimiter the delimiter to be used
761
     *
762
     * @return array array containing the first full query, the
763
     *               remaining part of the query and the last
764
     *               delimiter
765
     */
766 4
    public static function getFirstStatement($query, $delimiter = null)
767
    {
768 4
        $lexer = new Lexer($query, false, $delimiter);
769 4
        $list = $lexer->list;
770
771
        /**
772
         * Whether a full statement was found.
773
         *
774
         * @var bool
775
         */
776 4
        $fullStatement = false;
777
778
        /**
779
         * The first full statement.
780
         *
781
         * @var string
782
         */
783 4
        $statement = '';
784
785 4
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
786 4
            $token = $list->tokens[$list->idx];
787
788 4
            if ($token->type === Token::TYPE_COMMENT) {
789 4
                continue;
790
            }
791
792 4
            $statement .= $token->token;
793
794 4
            if (($token->type === Token::TYPE_DELIMITER) && ! empty($token->token)) {
795 4
                $delimiter = $token->token;
796 4
                $fullStatement = true;
797 4
                break;
798
            }
799
        }
800
801
        // No statement was found so we return the entire query as being the
802
        // remaining part.
803 4
        if (! $fullStatement) {
804
            return [
805 4
                null,
806 4
                $query,
807 4
                $delimiter,
808
            ];
809
        }
810
811
        // At least one query was found so we have to build the rest of the
812
        // remaining query.
813 4
        $query = '';
814 4
        for (++$list->idx; $list->idx < $list->count; ++$list->idx) {
815 4
            $query .= $list->tokens[$list->idx]->token;
816
        }
817
818
        return [
819 4
            trim($statement),
820 4
            $query,
821 4
            $delimiter,
822
        ];
823
    }
824
825
    /**
826
     * Gets a starting offset of a specific clause.
827
     *
828
     * @param Statement  $statement the parsed query that has to be modified
829
     * @param TokensList $list      the list of tokens
830
     * @param string     $clause    the clause to be returned
831
     *
832
     * @return int
833
     */
834 712
    public static function getClauseStartOffset($statement, $list, $clause)
835
    {
836
        /**
837
         * The count of brackets.
838
         * We keep track of them so we won't insert the clause in a subquery.
839
         *
840
         * @var int
841
         */
842 712
        $brackets = 0;
843
844
        /**
845
         * The clauses of this type of statement and their index.
846
         *
847
         * @var array
848
         */
849 712
        $clauses = array_flip(array_keys($statement->getClauses()));
850
851 712
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
852 712
            $token = $list->tokens[$i];
853
854 712
            if ($token->type === Token::TYPE_COMMENT) {
855 60
                continue;
856
            }
857
858 712
            if ($token->type === Token::TYPE_OPERATOR) {
859 568
                if ($token->value === '(') {
860 264
                    ++$brackets;
861 568
                } elseif ($token->value === ')') {
862 264
                    --$brackets;
863
                }
864
            }
865
866 712
            if ($brackets !== 0) {
867 268
                continue;
868
            }
869
870
            if (
871 712
                ($token->type === Token::TYPE_KEYWORD)
872 712
                && isset($clauses[$token->keyword])
873 712
                && ($clause === $token->keyword)
874
            ) {
875 700
                return $i;
876
            }
877
878 712
            if ($token->keyword === 'UNION') {
879 20
                return -1;
880
            }
881
        }
882
883 712
        return -1;
884
    }
885
}
886