Completed
Pull Request — master (#143)
by Deven
03:58
created

Query   F

Complexity

Total Complexity 128

Size/Duplication

Total Lines 817
Duplicated Lines 2.94 %

Coupling/Cohesion

Components 1
Dependencies 15

Test Coverage

Coverage 93.17%

Importance

Changes 0
Metric Value
wmc 128
c 0
b 0
f 0
lcom 1
cbo 15
dl 24
loc 817
ccs 259
cts 278
cp 0.9317
rs 1.263

9 Methods

Rating   Name   Duplication   Size   Complexity  
F _getFlagsSelect() 0 67 20
D getFlags() 10 84 26
C getAll() 0 75 23
C getTables() 0 41 14
F getClause() 7 114 19
A replaceClause() 0 17 3
B replaceClauses() 0 44 5
C getFirstStatement() 0 50 7
C getClauseStartOffset() 7 46 11

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Query often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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

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

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