Query::getFlagsSelect()   F
last analyzed

Complexity

Conditions 20
Paths 9216

Size

Total Lines 66
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 20

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 20
eloc 34
c 1
b 0
f 0
nc 9216
nop 2
dl 0
loc 66
ccs 35
cts 35
cp 1
crap 20
rs 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
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Utils;
6
7
use PhpMyAdmin\SqlParser\Lexer;
8
use PhpMyAdmin\SqlParser\Parser;
9
use PhpMyAdmin\SqlParser\Statement;
10
use PhpMyAdmin\SqlParser\Statements\AlterStatement;
11
use PhpMyAdmin\SqlParser\Statements\AnalyzeStatement;
12
use PhpMyAdmin\SqlParser\Statements\CallStatement;
13
use PhpMyAdmin\SqlParser\Statements\CheckStatement;
14
use PhpMyAdmin\SqlParser\Statements\ChecksumStatement;
15
use PhpMyAdmin\SqlParser\Statements\CreateStatement;
16
use PhpMyAdmin\SqlParser\Statements\DeleteStatement;
17
use PhpMyAdmin\SqlParser\Statements\DropStatement;
18
use PhpMyAdmin\SqlParser\Statements\ExplainStatement;
19
use PhpMyAdmin\SqlParser\Statements\InsertStatement;
20
use PhpMyAdmin\SqlParser\Statements\LoadStatement;
21
use PhpMyAdmin\SqlParser\Statements\OptimizeStatement;
22
use PhpMyAdmin\SqlParser\Statements\RenameStatement;
23
use PhpMyAdmin\SqlParser\Statements\RepairStatement;
24
use PhpMyAdmin\SqlParser\Statements\ReplaceStatement;
25
use PhpMyAdmin\SqlParser\Statements\SelectStatement;
26
use PhpMyAdmin\SqlParser\Statements\SetStatement;
27
use PhpMyAdmin\SqlParser\Statements\ShowStatement;
28
use PhpMyAdmin\SqlParser\Statements\TruncateStatement;
29
use PhpMyAdmin\SqlParser\Statements\UpdateStatement;
30
use PhpMyAdmin\SqlParser\TokensList;
31
use PhpMyAdmin\SqlParser\TokenType;
32
33
use function array_flip;
34
use function array_keys;
35
use function count;
36
use function in_array;
37
use function is_string;
38
use function trim;
39
40
/**
41
 * Statement utilities.
42
 */
43
class Query
44
{
45
    /**
46
     * Functions that set the flag `is_func`.
47
     *
48
     * @var string[]
49
     */
50
    public static array $functions = [
51
        'SUM',
52
        'AVG',
53
        'STD',
54
        'STDDEV',
55
        'MIN',
56
        'MAX',
57
        'BIT_OR',
58
        'BIT_AND',
59
    ];
60
61
    /**
62
     * Gets an array with flags select statement has.
63
     *
64
     * @param SelectStatement $statement the statement to be processed
65
     * @param StatementFlags  $flags     flags set so far
66
     */
67 26
    private static function getFlagsSelect(SelectStatement $statement, StatementFlags $flags): void
68
    {
69 26
        $flags->queryType = StatementType::Select;
70
        /** @psalm-suppress DeprecatedProperty */
71 26
        $flags->isSelect = true;
0 ignored issues
show
Deprecated Code introduced by
The property PhpMyAdmin\SqlParser\Uti...atementFlags::$isSelect has been deprecated: Use {@see self::$queryType} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

71
        /** @scrutinizer ignore-deprecated */ $flags->isSelect = true;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
72
73 26
        if ($statement->from !== []) {
74 22
            $flags->selectFrom = true;
75
        }
76
77 26
        if ($statement->options->has('DISTINCT')) {
0 ignored issues
show
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

77
        if ($statement->options->/** @scrutinizer ignore-call */ has('DISTINCT')) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
78 2
            $flags->distinct = true;
79
        }
80
81 26
        if (! empty($statement->group) || ! empty($statement->having)) {
82 4
            $flags->isGroup = true;
83
        }
84
85 26
        if (! empty($statement->into) && ($statement->into->type === 'OUTFILE')) {
86 2
            $flags->isExport = true;
87
        }
88
89 26
        $expressions = $statement->expr;
90 26
        if (! empty($statement->join)) {
91 2
            foreach ($statement->join as $join) {
92 2
                $expressions[] = $join->expr;
93
            }
94
        }
95
96 26
        foreach ($expressions as $expr) {
97 26
            if (! empty($expr->function)) {
98 2
                if ($expr->function === 'COUNT') {
99 2
                    $flags->isCount = true;
100 2
                } elseif (in_array($expr->function, static::$functions)) {
101 2
                    $flags->isFunc = true;
102
                }
103
            }
104
105 26
            if (empty($expr->subquery)) {
106 24
                continue;
107
            }
108
109 2
            $flags->isSubQuery = true;
110
        }
111
112 26
        if (! empty($statement->procedure) && ($statement->procedure->name === 'ANALYSE')) {
113 2
            $flags->isAnalyse = true;
114
        }
115
116 26
        if (! empty($statement->group)) {
117 2
            $flags->group = true;
118
        }
119
120 26
        if (! empty($statement->having)) {
121 2
            $flags->having = true;
122
        }
123
124 26
        if ($statement->union !== []) {
125 2
            $flags->union = true;
126
        }
127
128 26
        if (empty($statement->join)) {
129 24
            return;
130
        }
131
132 2
        $flags->join = true;
133
    }
134
135
    /**
136
     * Gets an array with flags this statement has.
137
     *
138
     * @param Statement|null $statement the statement to be processed
139
     */
140 64
    public static function getFlags(Statement|null $statement): StatementFlags
141
    {
142 64
        $flags = new StatementFlags();
143
144 64
        if ($statement instanceof AlterStatement) {
145 2
            $flags->queryType = StatementType::Alter;
146 2
            $flags->reload = true;
147 62
        } elseif ($statement instanceof CreateStatement) {
148 2
            $flags->queryType = StatementType::Create;
149 2
            $flags->reload = true;
150 60
        } elseif ($statement instanceof AnalyzeStatement) {
151 2
            $flags->queryType = StatementType::Analyze;
152 2
            $flags->isMaint = true;
153 58
        } elseif ($statement instanceof CheckStatement) {
154 2
            $flags->queryType = StatementType::Check;
155 2
            $flags->isMaint = true;
156 56
        } elseif ($statement instanceof ChecksumStatement) {
157 2
            $flags->queryType = StatementType::Checksum;
158 2
            $flags->isMaint = true;
159 54
        } elseif ($statement instanceof OptimizeStatement) {
160 2
            $flags->queryType = StatementType::Optimize;
161 2
            $flags->isMaint = true;
162 52
        } elseif ($statement instanceof RepairStatement) {
163 2
            $flags->queryType = StatementType::Repair;
164 2
            $flags->isMaint = true;
165 50
        } elseif ($statement instanceof CallStatement) {
166 2
            $flags->queryType = StatementType::Call;
167 2
            $flags->isProcedure = true;
168 48
        } elseif ($statement instanceof DeleteStatement) {
169 2
            $flags->queryType = StatementType::Delete;
170
            /** @psalm-suppress DeprecatedProperty */
171 2
            $flags->isDelete = true;
0 ignored issues
show
Deprecated Code introduced by
The property PhpMyAdmin\SqlParser\Uti...atementFlags::$isDelete has been deprecated: Use {@see self::$queryType} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

171
            /** @scrutinizer ignore-deprecated */ $flags->isDelete = true;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
172 2
            $flags->isAffected = true;
173 46
        } elseif ($statement instanceof DropStatement) {
174 4
            $flags->queryType = StatementType::Drop;
175 4
            $flags->reload = true;
176
177 4
            if ($statement->options->has('DATABASE') || $statement->options->has('SCHEMA')) {
178 3
                $flags->dropDatabase = true;
179
            }
180 42
        } elseif ($statement instanceof ExplainStatement) {
181 2
            $flags->queryType = StatementType::Explain;
182
            /** @psalm-suppress DeprecatedProperty */
183 2
            $flags->isExplain = true;
0 ignored issues
show
Deprecated Code introduced by
The property PhpMyAdmin\SqlParser\Uti...tementFlags::$isExplain has been deprecated: Use {@see self::$queryType} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

183
            /** @scrutinizer ignore-deprecated */ $flags->isExplain = true;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
184 40
        } elseif ($statement instanceof InsertStatement) {
185 2
            $flags->queryType = StatementType::Insert;
186 2
            $flags->isAffected = true;
187 2
            $flags->isInsert = true;
188 38
        } elseif ($statement instanceof LoadStatement) {
189 2
            $flags->queryType = StatementType::Load;
190 2
            $flags->isAffected = true;
191 2
            $flags->isInsert = true;
192 36
        } elseif ($statement instanceof ReplaceStatement) {
193 2
            $flags->queryType = StatementType::Replace;
194 2
            $flags->isAffected = true;
195
            /** @psalm-suppress DeprecatedProperty */
196 2
            $flags->isReplace = true;
0 ignored issues
show
Deprecated Code introduced by
The property PhpMyAdmin\SqlParser\Uti...tementFlags::$isReplace has been deprecated: Use {@see self::$queryType} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

196
            /** @scrutinizer ignore-deprecated */ $flags->isReplace = true;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
197 2
            $flags->isInsert = true;
198 34
        } elseif ($statement instanceof SelectStatement) {
199 26
            self::getFlagsSelect($statement, $flags);
200 8
        } elseif ($statement instanceof ShowStatement) {
201 2
            $flags->queryType = StatementType::Show;
202
            /** @psalm-suppress DeprecatedProperty */
203 2
            $flags->isShow = true;
0 ignored issues
show
Deprecated Code introduced by
The property PhpMyAdmin\SqlParser\Utils\StatementFlags::$isShow has been deprecated: Use {@see self::$queryType} instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

203
            /** @scrutinizer ignore-deprecated */ $flags->isShow = true;

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
204 6
        } elseif ($statement instanceof UpdateStatement) {
205 2
            $flags->queryType = StatementType::Update;
206 2
            $flags->isAffected = true;
207 4
        } elseif ($statement instanceof SetStatement) {
208 2
            $flags->queryType = StatementType::Set;
209
        }
210
211
        if (
212 64
            ($statement instanceof SelectStatement)
213 38
            || ($statement instanceof UpdateStatement)
214 64
            || ($statement instanceof DeleteStatement)
215
        ) {
216 30
            if (! empty($statement->limit)) {
217 4
                $flags->limit = true;
218
            }
219
220 30
            if (! empty($statement->order)) {
221 4
                $flags->order = true;
222
            }
223
        }
224
225 64
        return $flags;
226
    }
227
228
    /**
229
     * Parses a query and gets all information about it.
230
     *
231
     * @param string $query the query to be parsed
232
     */
233 4
    public static function getAll(string $query): StatementInfo
234
    {
235 4
        $parser = new Parser($query);
236
237 4
        if ($parser->statements === []) {
238 2
            return new StatementInfo($parser, null, static::getFlags(null), [], []);
239
        }
240
241 2
        $statement = $parser->statements[0];
242 2
        $flags = static::getFlags($statement);
243 2
        $selectTables = [];
244 2
        $selectExpressions = [];
245
246 2
        if ($statement instanceof SelectStatement) {
247
            // Finding tables' aliases and their associated real names.
248 2
            $tableAliases = [];
249 2
            foreach ($statement->from as $expr) {
250 2
                if (! isset($expr->table, $expr->alias) || ($expr->table === '') || ($expr->alias === '')) {
251 2
                    continue;
252
                }
253
254 2
                $tableAliases[$expr->alias] = [
255 2
                    $expr->table,
256 2
                    $expr->database ?? null,
257 2
                ];
258
            }
259
260
            // Trying to find selected tables only from the select expression.
261
            // Sometimes, this is not possible because the tables aren't defined
262
            // explicitly (e.g. SELECT * FROM film, SELECT film_id FROM film).
263 2
            foreach ($statement->expr as $expr) {
264 2
                if (isset($expr->table) && ($expr->table !== '')) {
265 2
                    if (isset($tableAliases[$expr->table])) {
266 2
                        $arr = $tableAliases[$expr->table];
267
                    } else {
268 2
                        $arr = [
269 2
                            $expr->table,
270 2
                            isset($expr->database) && ($expr->database !== '') ?
271 2
                                $expr->database : null,
272 2
                        ];
273
                    }
274
275 2
                    if (! in_array($arr, $selectTables)) {
276 2
                        $selectTables[] = $arr;
277
                    }
278
                } else {
279 2
                    $selectExpressions[] = $expr->expr;
280
                }
281
            }
282
283
            // If no tables names were found in the SELECT clause or if there
284
            // are expressions like * or COUNT(*), etc. tables names should be
285
            // extracted from the FROM clause.
286 2
            if ($selectTables === []) {
287 2
                foreach ($statement->from as $expr) {
288 2
                    if (! isset($expr->table) || ($expr->table === '')) {
289
                        continue;
290
                    }
291
292 2
                    $arr = [
293 2
                        $expr->table,
294 2
                        isset($expr->database) && ($expr->database !== '') ?
295 2
                            $expr->database : null,
296 2
                    ];
297 2
                    if (in_array($arr, $selectTables)) {
298
                        continue;
299
                    }
300
301 2
                    $selectTables[] = $arr;
302
                }
303
            }
304
        }
305
306 2
        return new StatementInfo($parser, $statement, $flags, $selectTables, $selectExpressions);
307
    }
308
309
    /**
310
     * Gets a list of all tables used in this statement.
311
     *
312
     * @param Statement $statement statement to be scanned
313
     *
314
     * @return array<int, string>
315
     */
316 20
    public static function getTables(Statement $statement): array
317
    {
318 20
        $expressions = [];
319
320 20
        if (($statement instanceof InsertStatement) || ($statement instanceof ReplaceStatement)) {
321 4
            $expressions = [$statement->into->dest];
322 16
        } elseif ($statement instanceof UpdateStatement) {
323 4
            $expressions = $statement->tables;
324 12
        } elseif (($statement instanceof SelectStatement) || ($statement instanceof DeleteStatement)) {
325 4
            $expressions = $statement->from;
326 8
        } elseif (($statement instanceof AlterStatement) || ($statement instanceof TruncateStatement)) {
327 2
            $expressions = [$statement->table];
328 6
        } elseif ($statement instanceof DropStatement) {
329 4
            if (! $statement->options->has('TABLE')) {
330
                // No tables are dropped.
331 2
                return [];
332
            }
333
334 2
            $expressions = $statement->fields;
335 2
        } elseif ($statement instanceof RenameStatement) {
336 2
            foreach ($statement->renames as $rename) {
337 2
                $expressions[] = $rename->old;
338
            }
339
        }
340
341 18
        $ret = [];
342 18
        foreach ($expressions as $expr) {
343 18
            if (empty($expr->table)) {
344
                continue;
345
            }
346
347 18
            $expr->expr = null; // Force rebuild.
348 18
            $expr->alias = null; // Aliases are not required.
349 18
            $ret[] = $expr->build();
350
        }
351
352 18
        return $ret;
353
    }
354
355
    /**
356
     * Gets a specific clause.
357
     *
358
     * @param Statement  $statement the parsed query that has to be modified
359
     * @param TokensList $list      the list of tokens
360
     * @param string     $clause    the clause to be returned
361
     * @param int|string $type      The type of the search.
362
     *                              If int,
363
     *                              -1 for everything that was before
364
     *                              0 only for the clause
365
     *                              1 for everything after
366
     *                              If string, the name of the first clause that
367
     *                              should not be included.
368
     * @param bool       $skipFirst whether to skip the first keyword in clause
369
     */
370 10
    public static function getClause(
371
        Statement $statement,
372
        TokensList $list,
373
        string $clause,
374
        int|string $type = 0,
375
        bool $skipFirst = true,
376
    ): string {
377
        /**
378
         * The index of the current clause.
379
         */
380 10
        $currIdx = 0;
381
382
        /**
383
         * The count of brackets.
384
         * We keep track of them so we won't insert the clause in a subquery.
385
         */
386 10
        $brackets = 0;
387
388
        /**
389
         * The string to be returned.
390
         */
391 10
        $ret = '';
392
393
        /**
394
         * The clauses of this type of statement and their index.
395
         */
396 10
        $clauses = array_flip(array_keys($statement->getClauses()));
397
398
        /**
399
         * Lexer used for lexing the clause.
400
         */
401 10
        $lexer = new Lexer($clause);
402
403
        /**
404
         * The type of this clause.
405
         */
406 10
        $clauseType = $lexer->list->getNextOfType(TokenType::Keyword)->keyword;
407
408
        /**
409
         * The index of this clause.
410
         */
411 10
        $clauseIdx = $clauses[$clauseType] ?? -1;
412
413 10
        $firstClauseIdx = $clauseIdx;
414 10
        $lastClauseIdx = $clauseIdx;
415
416
        // Determining the behavior of this function.
417 10
        if ($type === -1) {
418 8
            $firstClauseIdx = -1; // Something small enough.
419 8
            $lastClauseIdx = $clauseIdx - 1;
420 10
        } elseif ($type === 1) {
421 8
            $firstClauseIdx = $clauseIdx + 1;
422 8
            $lastClauseIdx = 10000; // Something big enough.
423 6
        } elseif (is_string($type) && isset($clauses[$type])) {
424 4
            if ($clauses[$type] > $clauseIdx) {
425 4
                $firstClauseIdx = $clauseIdx + 1;
426 4
                $lastClauseIdx = $clauses[$type] - 1;
427
            } else {
428 2
                $firstClauseIdx = $clauses[$type] + 1;
429 2
                $lastClauseIdx = $clauseIdx - 1;
430
            }
431
        }
432
433
        // This option is unavailable for multiple clauses.
434 10
        if ($type !== 0) {
435 10
            $skipFirst = false;
436
        }
437
438 10
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
439 10
            $token = $list->tokens[$i];
440
441 10
            if ($token->type === TokenType::Comment) {
442 2
                continue;
443
            }
444
445 10
            if ($token->type === TokenType::Operator) {
446 8
                if ($token->value === '(') {
447 8
                    ++$brackets;
448 8
                } elseif ($token->value === ')') {
449 8
                    --$brackets;
450
                }
451
            }
452
453 10
            if ($brackets === 0) {
454
                // Checking if the section was changed.
455
                if (
456 10
                    ($token->type === TokenType::Keyword)
457 10
                    && isset($clauses[$token->keyword])
458 10
                    && ($clauses[$token->keyword] >= $currIdx)
459
                ) {
460 8
                    $currIdx = $clauses[$token->keyword];
461 8
                    if ($skipFirst && ($currIdx === $clauseIdx)) {
462
                        // This token is skipped (not added to the old
463
                        // clause) because it will be replaced.
464 4
                        continue;
465
                    }
466
                }
467
            }
468
469 10
            if (($firstClauseIdx > $currIdx) || ($currIdx > $lastClauseIdx)) {
470 10
                continue;
471
            }
472
473 10
            $ret .= $token->token;
474
        }
475
476 10
        return trim($ret);
477
    }
478
479
    /**
480
     * Builds a query by rebuilding the statement from the tokens list supplied
481
     * and replaces a clause.
482
     *
483
     * It is a very basic version of a query builder.
484
     *
485
     * @param Statement  $statement the parsed query that has to be modified
486
     * @param TokensList $list      the list of tokens
487
     * @param string     $old       The type of the clause that should be
488
     *                              replaced. This can be an entire clause.
489
     * @param string     $new       The new clause. If this parameter is omitted
490
     *                              it is considered to be equal with `$old`.
491
     * @param bool       $onlyType  whether only the type of the clause should
492
     *                              be replaced or the entire clause
493
     */
494 8
    public static function replaceClause(
495
        Statement $statement,
496
        TokensList $list,
497
        string $old,
498
        string|null $new = null,
499
        bool $onlyType = false,
500
    ): string {
501
        // TODO: Update the tokens list and the statement.
502
503 8
        if ($new === null) {
504 4
            $new = $old;
505
        }
506
507 8
        if ($onlyType) {
508 2
            return static::getClause($statement, $list, $old, -1, false) . ' ' .
509 2
                $new . ' ' . static::getClause($statement, $list, $old, 0) . ' ' .
510 2
                static::getClause($statement, $list, $old, 1, false);
511
        }
512
513 6
        return static::getClause($statement, $list, $old, -1, false) . ' ' .
514 6
            $new . ' ' . static::getClause($statement, $list, $old, 1, false);
515
    }
516
517
    /**
518
     * Builds a query by rebuilding the statement from the tokens list supplied
519
     * and replaces multiple clauses.
520
     *
521
     * @param Statement                      $statement the parsed query that has to be modified
522
     * @param TokensList                     $list      the list of tokens
523
     * @param array<int, array<int, string>> $ops       Clauses to be replaced. Contains multiple
524
     *                              arrays having two values: [$old, $new].
525
     *                              Clauses must be sorted.
526
     */
527 2
    public static function replaceClauses(Statement $statement, TokensList $list, array $ops): string
528
    {
529 2
        $count = count($ops);
530
531
        // Nothing to do.
532 2
        if ($count === 0) {
533 2
            return '';
534
        }
535
536
        /**
537
         * Value to be returned.
538
         */
539 2
        $ret = '';
540
541
        // If there is only one clause, `replaceClause()` should be used.
542 2
        if ($count === 1) {
543 2
            return static::replaceClause($statement, $list, $ops[0][0], $ops[0][1]);
544
        }
545
546
        // Adding everything before first replacement.
547 2
        $ret .= static::getClause($statement, $list, $ops[0][0], -1) . ' ';
548
549
        // Doing replacements.
550 2
        foreach ($ops as $i => $clause) {
551 2
            $ret .= $clause[1] . ' ';
552
553
            // Adding everything between this and next replacement.
554 2
            if ($i + 1 === $count) {
555 2
                continue;
556
            }
557
558 2
            $ret .= static::getClause($statement, $list, $clause[0], $ops[$i + 1][0]) . ' ';
559
        }
560
561
        // Adding everything after the last replacement.
562 2
        return $ret . static::getClause($statement, $list, $ops[$count - 1][0], 1);
563
    }
564
565
    /**
566
     * Gets the first full statement in the query.
567
     *
568
     * @param string $query     the query to be analyzed
569
     * @param string $delimiter the delimiter to be used
570
     *
571
     * @return array<int, string|null> array containing the first full query,
572
     *                                 the remaining part of the query and the last delimiter
573
     * @psalm-return array{string|null, string, string|null}
574
     */
575 2
    public static function getFirstStatement(string $query, string|null $delimiter = null): array
576
    {
577 2
        $lexer = new Lexer($query, false, $delimiter);
578 2
        $list = $lexer->list;
579
580
        /**
581
         * Whether a full statement was found.
582
         */
583 2
        $fullStatement = false;
584
585
        /**
586
         * The first full statement.
587
         */
588 2
        $statement = '';
589
590 2
        for ($list->idx = 0; $list->idx < $list->count; ++$list->idx) {
591 2
            $token = $list->tokens[$list->idx];
592
593 2
            if ($token->type === TokenType::Comment) {
594 2
                continue;
595
            }
596
597 2
            $statement .= $token->token;
598
599 2
            if (($token->type === TokenType::Delimiter) && ! empty($token->token)) {
600 2
                $delimiter = $token->token;
601 2
                $fullStatement = true;
602 2
                break;
603
            }
604
        }
605
606
        // No statement was found so we return the entire query as being the
607
        // remaining part.
608 2
        if (! $fullStatement) {
609 2
            return [
610 2
                null,
611 2
                $query,
612 2
                $delimiter,
613 2
            ];
614
        }
615
616
        // At least one query was found so we have to build the rest of the
617
        // remaining query.
618 2
        $query = '';
619 2
        for (++$list->idx; $list->idx < $list->count; ++$list->idx) {
620 2
            $query .= $list->tokens[$list->idx]->token;
621
        }
622
623 2
        return [
624 2
            trim($statement),
625 2
            $query,
626 2
            $delimiter,
627 2
        ];
628
    }
629
630
    /**
631
     * Gets a starting offset of a specific clause.
632
     *
633
     * @param Statement  $statement the parsed query that has to be modified
634
     * @param TokensList $list      the list of tokens
635
     * @param string     $clause    the clause to be returned
636
     */
637 520
    public static function getClauseStartOffset(Statement $statement, TokensList $list, string $clause): int
638
    {
639
        /**
640
         * The count of brackets.
641
         * We keep track of them so we won't insert the clause in a subquery.
642
         */
643 520
        $brackets = 0;
644
645
        /**
646
         * The clauses of this type of statement and their index.
647
         */
648 520
        $clauses = array_flip(array_keys($statement->getClauses()));
649
650 520
        for ($i = $statement->first; $i <= $statement->last; ++$i) {
651 520
            $token = $list->tokens[$i];
652
653 520
            if ($token->type === TokenType::Comment) {
654 64
                continue;
655
            }
656
657 520
            if ($token->type === TokenType::Operator) {
658 414
                if ($token->value === '(') {
659 168
                    ++$brackets;
660 412
                } elseif ($token->value === ')') {
661 168
                    --$brackets;
662
                }
663
            }
664
665 520
            if ($brackets !== 0) {
666 172
                continue;
667
            }
668
669
            if (
670 518
                ($token->type === TokenType::Keyword)
671 518
                && isset($clauses[$token->keyword])
672 518
                && ($clause === $token->keyword)
673
            ) {
674 512
                return $i;
675
            }
676
677 518
            if ($token->keyword === 'UNION') {
678 10
                return -1;
679
            }
680
        }
681
682 520
        return -1;
683
    }
684
}
685