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

Query::getClause()   F

Complexity

Conditions 20
Paths 300

Size

Total Lines 115
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 44
CRAP Score 20

Importance

Changes 0
Metric Value
cc 20
eloc 44
nc 300
nop 5
dl 0
loc 115
ccs 44
cts 44
cp 1
crap 20
rs 2.0833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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