Completed
Push — master ( 65f66e...428edc )
by Michal
04:14
created

Query::replaceClauses()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 44
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 5
nop 3
dl 0
loc 44
ccs 21
cts 21
cp 1
crap 5
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Statement utilities.
5
 */
6
7
namespace PhpMyAdmin\SqlParser\Utils;
8
9
use PhpMyAdmin\SqlParser\Lexer;
10
use PhpMyAdmin\SqlParser\Parser;
11
use PhpMyAdmin\SqlParser\Statement;
12
use PhpMyAdmin\SqlParser\Token;
13
use PhpMyAdmin\SqlParser\TokensList;
14
use PhpMyAdmin\SqlParser\Components\Expression;
15
use PhpMyAdmin\SqlParser\Statements\AlterStatement;
16
use PhpMyAdmin\SqlParser\Statements\AnalyzeStatement;
17
use PhpMyAdmin\SqlParser\Statements\CallStatement;
18
use PhpMyAdmin\SqlParser\Statements\CheckStatement;
19
use PhpMyAdmin\SqlParser\Statements\ChecksumStatement;
20
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
21
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
22
use PhpMyAdmin\SqlParser\Statements\DropStatement;
23
use PhpMyAdmin\SqlParser\Statements\ExplainStatement;
24
use PhpMyAdmin\SqlParser\Statements\InsertStatement;
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\ShowStatement;
31
use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
32
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
33
34
/**
35
 * Statement utilities.
36
 *
37
 * @category   Statement
38
 *
39
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
40
 */
41
class Query
42
{
43
    /**
44
     * Functions that set the flag `is_func`.
45
     *
46
     * @var array
47
     */
48
    public static $FUNCTIONS = array(
49
        'SUM', 'AVG', 'STD', 'STDDEV', 'MIN', 'MAX', 'BIT_OR', 'BIT_AND',
50
    );
51
52
    /**
53
     * Gets an array with flags this statement has.
54
     *
55
     * @param Statement|null $statement the statement to be processed
56
     * @param bool           $all       if `false`, false values will not be included
57
     *
58
     * @return array
59
     */
60 29
    public static function getFlags($statement, $all = false)
61
    {
62 29
        $flags = array();
63 29
        if ($all) {
64
            $flags = array(
65
                /*
66
                 * select ... DISTINCT ...
67
                 */
68 1
                'distinct' => false,
69
70
                /*
71
                 * drop ... DATABASE ...
72
                 */
73 1
                'drop_database' => false,
74
75
                /*
76
                 * ... GROUP BY ...
77
                 */
78 1
                'group' => false,
79
80
                /*
81
                 * ... HAVING ...
82
                 */
83 1
                'having' => false,
84
85
                /*
86
                 * INSERT ...
87
                 * or
88
                 * REPLACE ...
89
                 * or
90
                 * DELETE ...
91
                 */
92 1
                'is_affected' => false,
93
94
                /*
95
                 * select ... PROCEDURE ANALYSE( ... ) ...
96
                 */
97 1
                'is_analyse' => false,
98
99
                /*
100
                 * select COUNT( ... ) ...
101
                 */
102 1
                'is_count' => false,
103
104
                /*
105
                 * DELETE ...
106
                 */
107 1
                'is_delete' => false, // @deprecated; use `querytype`
108
109
                /*
110
                 * EXPLAIN ...
111
                 */
112 1
                'is_explain' => false, // @deprecated; use `querytype`
113
114
                /*
115
                 * select ... INTO OUTFILE ...
116
                 */
117 1
                'is_export' => false,
118
119
                /*
120
                 * select FUNC( ... ) ...
121
                 */
122 1
                'is_func' => false,
123
124
                /*
125
                 * select ... GROUP BY ...
126
                 * or
127
                 * select ... HAVING ...
128
                 */
129 1
                'is_group' => false,
130
131
                /*
132
                 * INSERT ...
133
                 * or
134
                 * REPLACE ...
135
                 * or
136
                 * TODO: LOAD DATA ...
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
137
                 */
138 1
                'is_insert' => false,
139
140
                /*
141
                 * ANALYZE ...
142
                 * or
143
                 * CHECK ...
144
                 * or
145
                 * CHECKSUM ...
146
                 * or
147
                 * OPTIMIZE ...
148
                 * or
149
                 * REPAIR ...
150
                 */
151 1
                'is_maint' => false,
152
153
                /*
154
                 * CALL ...
155
                 */
156 1
                'is_procedure' => false,
157
158
                /*
159
                 * REPLACE ...
160
                 */
161 1
                'is_replace' => false, // @deprecated; use `querytype`
162
163
                /*
164
                 * SELECT ...
165
                 */
166 1
                'is_select' => false, // @deprecated; use `querytype`
167
168
                /*
169
                 * SHOW ...
170
                 */
171 1
                'is_show' => false, // @deprecated; use `querytype`
172
173
                /*
174
                 * Contains a subquery.
175
                 */
176 1
                'is_subquery' => false,
177
178
                /*
179
                 * ... JOIN ...
180
                 */
181 1
                'join' => false,
182
183
                /*
184
                 * ... LIMIT ...
185
                 */
186 1
                'limit' => false,
187
188
                /*
189
                 * TODO
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
190
                 */
191 1
                'offset' => false,
192
193
                /*
194
                 * ... ORDER ...
195
                 */
196 1
                'order' => false,
197
198
                /*
199
                 * The type of the query (which is usually the first keyword of
200
                 * the statement).
201
                 */
202 1
                'querytype' => false,
203
204
                /*
205
                 * Whether a page reload is required.
206
                 */
207 1
                'reload' => false,
208
209
                /*
210
                 * SELECT ... FROM ...
211
                 */
212 1
                'select_from' => false,
213
214
                /*
215
                 * ... UNION ...
216
                 */
217 1
                'union' => false,
218 1
            );
219 1
        }
220
221 29
        if ($statement instanceof AlterStatement) {
222 1
            $flags['querytype'] = 'ALTER';
223 1
            $flags['reload'] = true;
224 29
        } elseif ($statement instanceof CreateStatement) {
225 1
            $flags['querytype'] = 'CREATE';
226 1
            $flags['reload'] = true;
227 28
        } elseif ($statement instanceof AnalyzeStatement) {
228 1
            $flags['querytype'] = 'ANALYZE';
229 1
            $flags['is_maint'] = true;
230 27
        } elseif ($statement instanceof CheckStatement) {
231 1
            $flags['querytype'] = 'CHECK';
232 1
            $flags['is_maint'] = true;
233 26
        } elseif ($statement instanceof ChecksumStatement) {
234 1
            $flags['querytype'] = 'CHECKSUM';
235 1
            $flags['is_maint'] = true;
236 25
        } elseif ($statement instanceof OptimizeStatement) {
237 1
            $flags['querytype'] = 'OPTIMIZE';
238 1
            $flags['is_maint'] = true;
239 24
        } elseif ($statement instanceof RepairStatement) {
240 1
            $flags['querytype'] = 'REPAIR';
241 1
            $flags['is_maint'] = true;
242 23
        } elseif ($statement instanceof CallStatement) {
243 1
            $flags['querytype'] = 'CALL';
244 1
            $flags['is_procedure'] = true;
245 22
        } elseif ($statement instanceof DeleteStatement) {
246 1
            $flags['querytype'] = 'DELETE';
247 1
            $flags['is_delete'] = true;
248 1
            $flags['is_affected'] = true;
249 21
        } elseif ($statement instanceof DropStatement) {
250 2
            $flags['querytype'] = 'DROP';
251 2
            $flags['reload'] = true;
252
253 2
            if (($statement->options->has('DATABASE')
254 1
                || ($statement->options->has('SCHEMA')))
255 2
            ) {
256 1
                $flags['drop_database'] = true;
257 1
            }
258 20
        } elseif ($statement instanceof ExplainStatement) {
259 1
            $flags['querytype'] = 'EXPLAIN';
260 1
            $flags['is_explain'] = true;
261 18
        } elseif ($statement instanceof InsertStatement) {
262 1
            $flags['querytype'] = 'INSERT';
263 1
            $flags['is_affected'] = true;
264 1
            $flags['is_insert'] = true;
265 17
        } elseif ($statement instanceof ReplaceStatement) {
266 1
            $flags['querytype'] = 'REPLACE';
267 1
            $flags['is_affected'] = true;
268 1
            $flags['is_replace'] = true;
269 1
            $flags['is_insert'] = true;
270 16
        } elseif ($statement instanceof SelectStatement) {
271 13
            $flags['querytype'] = 'SELECT';
272 13
            $flags['is_select'] = true;
273
274 13
            if (!empty($statement->from)) {
275 11
                $flags['select_from'] = true;
276 11
            }
277
278 13
            if ($statement->options->has('DISTINCT')) {
279 1
                $flags['distinct'] = true;
280 1
            }
281
282 13
            if ((!empty($statement->group)) || (!empty($statement->having))) {
283 2
                $flags['is_group'] = true;
284 2
            }
285
286 13
            if ((!empty($statement->into))
287 13
                && ($statement->into->type === 'OUTFILE')
288 13
            ) {
289 1
                $flags['is_export'] = true;
290 1
            }
291
292 13
            $expressions = $statement->expr;
293 13
            if (!empty($statement->join)) {
294 1
                foreach ($statement->join as $join) {
295 1
                    $expressions[] = $join->expr;
296 1
                }
297 1
            }
298
299 13
            foreach ($expressions as $expr) {
300 13
                if (!empty($expr->function)) {
301 1
                    if ($expr->function === 'COUNT') {
302 1
                        $flags['is_count'] = true;
303 1
                    } elseif (in_array($expr->function, static::$FUNCTIONS)) {
304 1
                        $flags['is_func'] = true;
305 1
                    }
306 1
                }
307 13
                if (!empty($expr->subquery)) {
308 1
                    $flags['is_subquery'] = true;
309 1
                }
310 13
            }
311
312 13
            if ((!empty($statement->procedure))
313 13
                && ($statement->procedure->name === 'ANALYSE')
314 13
            ) {
315 1
                $flags['is_analyse'] = true;
316 1
            }
317
318 13
            if (!empty($statement->group)) {
319 1
                $flags['group'] = true;
320 1
            }
321
322 13
            if (!empty($statement->having)) {
323 1
                $flags['having'] = true;
324 1
            }
325
326 13
            if (!empty($statement->union)) {
327 1
                $flags['union'] = true;
328 1
            }
329
330 13
            if (!empty($statement->join)) {
331 1
                $flags['join'] = true;
332 1
            }
333 15
        } elseif ($statement instanceof ShowStatement) {
334 1
            $flags['querytype'] = 'SHOW';
335 1
            $flags['is_show'] = true;
336 3
        } elseif ($statement instanceof UpdateStatement) {
337 1
            $flags['querytype'] = 'UPDATE';
338 1
            $flags['is_affected'] = true;
339 1
        }
340
341 29
        if (($statement instanceof SelectStatement)
342 17
            || ($statement instanceof UpdateStatement)
343 17
            || ($statement instanceof DeleteStatement)
344 29
        ) {
345 15
            if (!empty($statement->limit)) {
346 2
                $flags['limit'] = true;
347 2
            }
348 15
            if (!empty($statement->order)) {
349 2
                $flags['order'] = true;
350 2
            }
351 15
        }
352
353 29
        return $flags;
354
    }
355
356
    /**
357
     * Parses a query and gets all information about it.
358
     *
359
     * @param string $query the query to be parsed
360
     *
361
     * @return array The array returned is the one returned by
362
     *               `static::getFlags()`, with the following keys added:
363
     *               - parser - the parser used to analyze the query;
364
     *               - statement - the first statement resulted from parsing;
365
     *               - select_tables - the real name of the tables selected;
366
     *               if there are no table names in the `SELECT`
367
     *               expressions, the table names are fetched from the
368
     *               `FROM` expressions
369
     *               - select_expr - selected expressions
370
     */
371 1
    public static function getAll($query)
372
    {
373 1
        $parser = new Parser($query);
374
375 1
        if (empty($parser->statements[0])) {
376 1
            return static::getFlags(null, true);
377
        }
378
379 1
        $statement = $parser->statements[0];
380
381 1
        $ret = static::getFlags($statement, true);
382
383 1
        $ret['parser'] = $parser;
384 1
        $ret['statement'] = $statement;
385
386 1
        if ($statement instanceof SelectStatement) {
387 1
            $ret['select_tables'] = array();
388 1
            $ret['select_expr'] = array();
389
390
            // Finding tables' aliases and their associated real names.
391 1
            $tableAliases = array();
392 1
            foreach ($statement->from as $expr) {
393 1
                if ((isset($expr->table)) && ($expr->table !== '')
394 1
                    && (isset($expr->alias)) && ($expr->alias !== '')
395 1
                ) {
396 1
                    $tableAliases[$expr->alias] = array(
397 1
                        $expr->table,
398 1
                        isset($expr->database) ? $expr->database : null,
399
                    );
400 1
                }
401 1
            }
402
403
            // Trying to find selected tables only from the select expression.
404
            // Sometimes, this is not possible because the tables aren't defined
405
            // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film).
406 1
            foreach ($statement->expr as $expr) {
407 1
                if ((isset($expr->table)) && ($expr->table !== '')) {
408 1
                    if (isset($tableAliases[$expr->table])) {
409 1
                        $arr = $tableAliases[$expr->table];
410 1
                    } else {
411
                        $arr = array(
412 1
                            $expr->table,
413 1
                            ((isset($expr->database)) && ($expr->database !== '')) ?
414 1
                                $expr->database : null,
415 1
                        );
416
                    }
417 1
                    if (!in_array($arr, $ret['select_tables'])) {
418 1
                        $ret['select_tables'][] = $arr;
419 1
                    }
420 1
                } else {
421 1
                    $ret['select_expr'][] = $expr->expr;
422
                }
423 1
            }
424
425
            // If no tables names were found in the SELECT clause or if there
426
            // are expressions like * or COUNT(*), etc. tables names should be
427
            // extracted from the FROM clause.
428 1
            if (empty($ret['select_tables'])) {
429 1
                foreach ($statement->from as $expr) {
430 1
                    if ((isset($expr->table)) && ($expr->table !== '')) {
431
                        $arr = array(
432 1
                            $expr->table,
433 1
                            ((isset($expr->database)) && ($expr->database !== '')) ?
434 1
                                $expr->database : null,
435 1
                        );
436 1
                        if (!in_array($arr, $ret['select_tables'])) {
437 1
                            $ret['select_tables'][] = $arr;
438 1
                        }
439 1
                    }
440 1
                }
441 1
            }
442 1
        }
443
444 1
        return $ret;
445
    }
446
447
    /**
448
     * Gets a list of all tables used in this statement.
449
     *
450
     * @param Statement $statement statement to be scanned
451
     *
452
     * @return array
453
     */
454 7
    public static function getTables($statement)
455
    {
456 7
        $expressions = array();
457
458 7
        if (($statement instanceof InsertStatement)
459 6
            || ($statement instanceof ReplaceStatement)
460 7
        ) {
461 1
            $expressions = array($statement->into->dest);
462 7
        } elseif ($statement instanceof UpdateStatement) {
463 1
            $expressions = $statement->tables;
464 6
        } elseif (($statement instanceof SelectStatement)
465 5
            || ($statement instanceof DeleteStatement)
466 5
        ) {
467 1
            $expressions = $statement->from;
468 5
        } elseif (($statement instanceof AlterStatement)
469 4
            || ($statement instanceof TruncateStatement)
470 4
        ) {
471 1
            $expressions = array($statement->table);
472 4
        } elseif ($statement instanceof DropStatement) {
473 2
            if (!$statement->options->has('TABLE')) {
474
                // No tables are dropped.
475 1
                return array();
476
            }
477 1
            $expressions = $statement->fields;
478 2
        } elseif ($statement instanceof RenameStatement) {
479 1
            foreach ($statement->renames as $rename) {
480 1
                $expressions[] = $rename->old;
481 1
            }
482 1
        }
483
484 6
        $ret = array();
485 6
        foreach ($expressions as $expr) {
486 6
            if (!empty($expr->table)) {
487 6
                $expr->expr = null; // Force rebuild.
488 6
                $expr->alias = null; // Aliases are not required.
489 6
                $ret[] = Expression::build($expr);
490 6
            }
491 6
        }
492
493 6
        return $ret;
494
    }
495
496
    /**
497
     * Gets a specific clause.
498
     *
499
     * @param Statement  $statement the parsed query that has to be modified
500
     * @param TokensList $list      the list of tokens
501
     * @param string     $clause    the clause to be returned
502
     * @param int|string $type      The type of the search.
503
     *                              If int,
504
     *                              -1 for everything that was before
505
     *                              0 only for the clause
506
     *                              1 for everything after
507
     *                              If string, the name of the first clause that
508
     *                              should not be included.
509
     * @param bool       $skipFirst whether to skip the first keyword in clause
510
     *
511
     * @return string
512
     */
513 4
    public static function getClause($statement, $list, $clause, $type = 0, $skipFirst = true)
514
    {
515
        /**
516
         * The index of the current clause.
517
         *
518
         * @var int
519
         */
520 4
        $currIdx = 0;
521
522
        /**
523
         * The count of brackets.
524
         * We keep track of them so we won't insert the clause in a subquery.
525
         *
526
         * @var int
527
         */
528 4
        $brackets = 0;
529
530
        /**
531
         * The string to be returned.
532
         *
533
         * @var string
534
         */
535 4
        $ret = '';
536
537
        /**
538
         * The clauses of this type of statement and their index.
539
         *
540
         * @var array
541
         */
542 4
        $clauses = array_flip(array_keys($statement->getClauses()));
543
544
        /**
545
         * Lexer used for lexing the clause.
546
         *
547
         * @var Lexer
548
         */
549 4
        $lexer = new Lexer($clause);
550
551
        /**
552
         * The type of this clause.
553
         *
554
         * @var string
555
         */
556 4
        $clauseType = $lexer->list->getNextOfType(Token::TYPE_KEYWORD)->value;
557
558
        /**
559
         * The index of this clause.
560
         *
561
         * @var int
562
         */
563 4
        $clauseIdx = $clauses[$clauseType];
564
565 4
        $firstClauseIdx = $clauseIdx;
566 4
        $lastClauseIdx = $clauseIdx + 1;
567
568
        // Determining the behavior of this function.
569 4
        if ($type === -1) {
570 3
            $firstClauseIdx = -1; // Something small enough.
571 3
            $lastClauseIdx = $clauseIdx - 1;
572 4
        } elseif ($type === 1) {
573 3
            $firstClauseIdx = $clauseIdx + 1;
574 3
            $lastClauseIdx = 10000; // Something big enough.
575 4
        } elseif (is_string($type)) {
576 2
            if ($clauses[$type] > $clauseIdx) {
577 1
                $firstClauseIdx = $clauseIdx + 1;
578 1
                $lastClauseIdx = $clauses[$type] - 1;
579 1
            } else {
580 1
                $firstClauseIdx = $clauses[$type] + 1;
581 1
                $lastClauseIdx = $clauseIdx - 1;
582
            }
583 2
        }
584
585
        // This option is unavailable for multiple clauses.
586 4
        if ($type !== 0) {
587 4
            $skipFirst = false;
588 4
        }
589
590 4
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
591 4
            $token = $list->tokens[$i];
592
593 4
            if ($token->type === Token::TYPE_COMMENT) {
594 1
                continue;
595
            }
596
597 4 View Code Duplication
            if ($token->type === Token::TYPE_OPERATOR) {
598 4
                if ($token->value === '(') {
599 3
                    ++$brackets;
600 4
                } elseif ($token->value === ')') {
601 3
                    --$brackets;
602 3
                }
603 4
            }
604
605 4
            if ($brackets == 0) {
606
                // Checking if the section was changed.
607 4
                if (($token->type === Token::TYPE_KEYWORD)
608 4
                    && (isset($clauses[$token->value]))
609 4
                    && ($clauses[$token->value] >= $currIdx)
610 4
                ) {
611 4
                    $currIdx = $clauses[$token->value];
612 4
                    if (($skipFirst) && ($currIdx == $clauseIdx)) {
613
                        // This token is skipped (not added to the old
614
                        // clause) because it will be replaced.
615 1
                        continue;
616
                    }
617 4
                }
618 4
            }
619
620 4
            if (($firstClauseIdx <= $currIdx) && ($currIdx <= $lastClauseIdx)) {
621 4
                $ret .= $token->token;
622 4
            }
623 4
        }
624
625 4
        return trim($ret);
626
    }
627
628
    /**
629
     * Builds a query by rebuilding the statement from the tokens list supplied
630
     * and replaces a clause.
631
     *
632
     * It is a very basic version of a query builder.
633
     *
634
     * @param Statement  $statement the parsed query that has to be modified
635
     * @param TokensList $list      the list of tokens
636
     * @param string     $old       The type of the clause that should be
637
     *                              replaced. This can be an entire clause.
638
     * @param string     $new       The new clause. If this parameter is omitted
0 ignored issues
show
Documentation introduced by
Should the type for parameter $new not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
639
     *                              it is considered to be equal with `$old`.
640
     * @param bool       $onlyType  whether only the type of the clause should
641
     *                              be replaced or the entire clause
642
     *
643
     * @return string
644
     */
645 3
    public static function replaceClause($statement, $list, $old, $new = null, $onlyType = false)
646
    {
647
        // TODO: Update the tokens list and the statement.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
648
649 3
        if ($new === null) {
650 2
            $new = $old;
651 2
        }
652
653 3
        if ($onlyType) {
654 1
            return static::getClause($statement, $list, $old, -1, false) . ' ' .
655 1
                $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' .
656 1
                static::getClause($statement, $list, $old, 1, false);
657
        }
658
659 2
        return static::getClause($statement, $list, $old, -1, false) . ' ' .
660 2
            $new . ' ' . static::getClause($statement, $list, $old, 1, false);
661
    }
662
663
    /**
664
     * Builds a query by rebuilding the statement from the tokens list supplied
665
     * and replaces multiple clauses.
666
     *
667
     * @param Statement  $statement the parsed query that has to be modified
668
     * @param TokensList $list      the list of tokens
669
     * @param array      $ops       Clauses to be replaced. Contains multiple
670
     *                              arrays having two values: array($old, $new).
671
     *                              Clauses must be sorted.
672
     *
673
     * @return string
674
     */
675 1
    public static function replaceClauses($statement, $list, array $ops)
676
    {
677 1
        $count = count($ops);
678
679
        // Nothing to do.
680 1
        if ($count === 0) {
681 1
            return '';
682
        }
683
684
        /**
685
         * Value to be returned.
686
         *
687
         * @var string
688
         */
689 1
        $ret = '';
690
691
        // If there is only one clause, `replaceClause()` should be used.
692 1
        if ($count === 1) {
693 1
            return static::replaceClause(
694 1
                $statement,
695 1
                $list,
696 1
                $ops[0][0],
697 1
                $ops[0][1]
698 1
            );
699
        }
700
701
        // Adding everything before first replacement.
702 1
        $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' ';
703
704
        // Doing replacements.
705 1
        for ($i = 0; $i < $count; ++$i) {
706 1
            $ret .= $ops[$i][1] . ' ';
707
708
            // Adding everything between this and next replacement.
709 1
            if ($i + 1 !== $count) {
710 1
                $ret .= static::getClause($statement, $list, $ops[$i][0], $ops[$i + 1][0]) . ' ';
711 1
            }
712 1
        }
713
714
        // Adding everything after the last replacement.
715 1
        $ret .= static::getClause($statement, $list, $ops[$count - 1][0], 1);
716
717 1
        return $ret;
718
    }
719
720
    /**
721
     * Gets the first full statement in the query.
722
     *
723
     * @param string $query     the query to be analyzed
724
     * @param string $delimiter the delimiter to be used
0 ignored issues
show
Documentation introduced by
Should the type for parameter $delimiter not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
725
     *
726
     * @return array array containing the first full query, the
727
     *               remaining part of the query and the last
728
     *               delimiter
729
     */
730 1
    public static function getFirstStatement($query, $delimiter = null)
731
    {
732 1
        $lexer = new Lexer($query, false, $delimiter);
733 1
        $list = $lexer->list;
734
735
        /**
736
         * Whether a full statement was found.
737
         *
738
         * @var bool
739
         */
740 1
        $fullStatement = false;
741
742
        /**
743
         * The first full statement.
744
         *
745
         * @var string
746
         */
747 1
        $statement = '';
748
749 1
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
750 1
            $token = $list->tokens[$list->idx];
751
752 1
            if ($token->type === Token::TYPE_COMMENT) {
753 1
                continue;
754
            }
755
756 1
            $statement .= $token->token;
757
758 1
            if (($token->type === Token::TYPE_DELIMITER) && (!empty($token->token))) {
759 1
                $delimiter = $token->token;
760 1
                $fullStatement = true;
761 1
                break;
762
            }
763 1
        }
764
765
        // No statement was found so we return the entire query as being the
766
        // remaining part.
767 1
        if (!$fullStatement) {
768 1
            return array(null, $query, $delimiter);
769
        }
770
771
        // At least one query was found so we have to build the rest of the
772
        // remaining query.
773 1
        $query = '';
774 1
        for (++$list->idx; $list->idx < $list->count; ++$list->idx) {
775 1
            $query .= $list->tokens[$list->idx]->token;
776 1
        }
777
778 1
        return array(trim($statement), $query, $delimiter);
779
    }
780
781
    /**
782
     * Gets a starting offset of a specific clause.
783
     *
784
     * @param Statement  $statement the parsed query that has to be modified
785
     * @param TokensList $list      the list of tokens
786
     * @param string     $clause    the clause to be returned
787
     *
788
     * @return int
789
     */
790 136
    public static function getClauseStartOffset($statement, $list, $clause)
791
    {
792
        /**
793
         * The count of brackets.
794
         * We keep track of them so we won't insert the clause in a subquery.
795
         *
796
         * @var int
797
         */
798 136
        $brackets = 0;
799
800
        /**
801
         * The clauses of this type of statement and their index.
802
         *
803
         * @var array
804
         */
805 136
        $clauses = array_flip(array_keys($statement->getClauses()));
806
807 136
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
808 136
            $token = $list->tokens[$i];
809
810 136
            if ($token->type === Token::TYPE_COMMENT) {
811 10
                continue;
812
            }
813
814 136 View Code Duplication
            if ($token->type === Token::TYPE_OPERATOR) {
815 108
                if ($token->value === '(') {
816 42
                    ++$brackets;
817 108
                } elseif ($token->value === ')') {
818 42
                    --$brackets;
819 42
                }
820 108
            }
821
822 136
            if ($brackets == 0) {
823 136
                if (($token->type === Token::TYPE_KEYWORD)
824 136
                    && (isset($clauses[$token->value]))
825 136
                    && ($clause === $token->value)
826 136
                ) {
827 135
                    return $i;
828 128
                } elseif ($token->value === 'UNION') {
829 5
                    return -1;
830
                }
831 128
            }
832 128
        }
833
834 128
        return -1;
835
    }
836
}
837