Passed
Push — master ( da08ab...de8636 )
by William
13:04
created

AlterOperation::checkIfTokenQuotedSymbol()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Components;
6
7
use PhpMyAdmin\SqlParser\Component;
8
use PhpMyAdmin\SqlParser\Parser;
9
use PhpMyAdmin\SqlParser\Token;
10
use PhpMyAdmin\SqlParser\TokensList;
11
12
use function array_key_exists;
13
use function in_array;
14
use function is_numeric;
15
use function is_string;
16
use function trim;
17
18
/**
19
 * Parses an alter operation.
20
 */
21
final class AlterOperation implements Component
22
{
23
    /**
24
     * All database options.
25
     *
26
     * @var array<string, int|array<int, int|string>>
27
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
28
     */
29
    public static $databaseOptions = [
30
        'CHARACTER SET' => [
31
            1,
32
            'var',
33
        ],
34
        'CHARSET' => [
35
            1,
36
            'var',
37
        ],
38
        'DEFAULT CHARACTER SET' => [
39
            1,
40
            'var',
41
        ],
42
        'DEFAULT CHARSET' => [
43
            1,
44
            'var',
45
        ],
46
        'UPGRADE' => [
47
            1,
48
            'var',
49
        ],
50
        'COLLATE' => [
51
            2,
52
            'var',
53
        ],
54
        'DEFAULT COLLATE' => [
55
            2,
56
            'var',
57
        ],
58
    ];
59
60
    /**
61
     * All table options.
62
     *
63
     * @var array<string, int|array<int, int|string>>
64
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
65
     */
66
    public static $tableOptions = [
67
        'ENGINE' => [
68
            1,
69
            'var=',
70
        ],
71
        'ALGORITHM' => [
72
            1,
73
            'var=',
74
        ],
75
        'AUTO_INCREMENT' => [
76
            1,
77
            'var=',
78
        ],
79
        'AVG_ROW_LENGTH' => [
80
            1,
81
            'var',
82
        ],
83
        'COALESCE PARTITION' => [
84
            1,
85
            'var',
86
        ],
87
        'LOCK' => [
88
            1,
89
            'var=',
90
        ],
91
        'MAX_ROWS' => [
92
            1,
93
            'var',
94
        ],
95
        'ROW_FORMAT' => [
96
            1,
97
            'var',
98
        ],
99
        'COMMENT' => [
100
            1,
101
            'var',
102
        ],
103
        'ADD' => 1,
104
        'ALTER' => 1,
105
        'ANALYZE' => 1,
106
        'CHANGE' => 1,
107
        'CHARSET' => 1,
108
        'CHECK' => 1,
109
        'CONVERT' => 1,
110
        'DEFAULT CHARSET' => 1,
111
        'DISABLE' => 1,
112
        'DISCARD' => 1,
113
        'DROP' => 1,
114
        'ENABLE' => 1,
115
        'IMPORT' => 1,
116
        'MODIFY' => 1,
117
        'OPTIMIZE' => 1,
118
        'ORDER' => 1,
119
        'REBUILD' => 1,
120
        'REMOVE' => 1,
121
        'RENAME' => 1,
122
        'REORGANIZE' => 1,
123
        'REPAIR' => 1,
124
        'UPGRADE' => 1,
125
126
        'COLUMN' => 2,
127
        'CONSTRAINT' => 2,
128
        'DEFAULT' => 2,
129
        'BY' => 2,
130
        'FOREIGN' => 2,
131
        'FULLTEXT' => 2,
132
        'KEY' => 2,
133
        'KEYS' => 2,
134
        'PARTITION' => 2,
135
        'PARTITION BY' => 2,
136
        'PARTITIONING' => 2,
137
        'PRIMARY KEY' => 2,
138
        'SPATIAL' => 2,
139
        'TABLESPACE' => 2,
140
        'INDEX' => [
141
            2,
142
            'var',
143
        ],
144
145
        'CHARACTER SET' => 3,
146
        'TO' => [
147
            3,
148
            'var',
149
        ],
150
    ];
151
152
    /**
153
     * All user options.
154
     *
155
     * @var array<string, int|array<int, int|string>>
156
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
157
     */
158
    public static $userOptions = [
159
        'ATTRIBUTE' => [
160
            1,
161
            'var',
162
        ],
163
        'COMMENT' => [
164
            1,
165
            'var',
166
        ],
167
        'REQUIRE' => [
168
            1,
169
            'var',
170
        ],
171
172
        'IDENTIFIED VIA' => [
173
            2,
174
            'var',
175
        ],
176
        'IDENTIFIED WITH' => [
177
            2,
178
            'var',
179
        ],
180
        'PASSWORD' => [
181
            2,
182
            'var',
183
        ],
184
        'WITH' => [
185
            2,
186
            'var',
187
        ],
188
189
        'BY' => [
190
            4,
191
            'expr',
192
        ],
193
194
        'ACCOUNT' => 1,
195
        'DEFAULT' => 1,
196
197
        'LOCK' => 2,
198
        'UNLOCK' => 2,
199
200
        'IDENTIFIED' => 3,
201
    ];
202
203
    /**
204
     * All view options.
205
     *
206
     * @var array<string, int|array<int, int|string>>
207
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
208
     */
209
    public static $viewOptions = ['AS' => 1];
210
211
    /**
212
     * All event options.
213
     *
214
     * @var array<string, int|array<int, int|string>>
215
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
216
     */
217
    public static $eventOptions = [
218
        'ON SCHEDULE' => 1,
219
        'EVERY' => [
220
            2,
221
            'expr',
222
        ],
223
        'AT' => [
224
            2,
225
            'expr',
226
        ],
227
        'STARTS' => [
228
            3,
229
            'expr',
230
        ],
231
        'ENDS' => [
232
            4,
233
            'expr',
234
        ],
235
        'ON COMPLETION PRESERVE' => 5,
236
        'ON COMPLETION NOT PRESERVE' => 5,
237
        'RENAME' => 6,
238
        'TO' => [
239
            7,
240
            'expr',
241
            ['parseField' => 'table'],
242
        ],
243
        'ENABLE' => 8,
244
        'DISABLE' => 8,
245
        'DISABLE ON SLAVE' => 8,
246
        'COMMENT' => [
247
            9,
248
            'var',
249
        ],
250
        'DO' => 10,
251
    ];
252
253
    /**
254
     * Options of this operation.
255
     *
256
     * @var OptionsArray
257
     */
258
    public $options;
259
260
    /**
261
     * The altered field.
262
     *
263
     * @var Expression|string|null
264
     */
265
    public $field;
266
267
    /**
268
     * The partitions.
269
     *
270
     * @var Component[]|ArrayObj|null
271
     */
272
    public $partitions;
273
274
    /**
275
     * Unparsed tokens.
276
     *
277
     * @var Token[]|string
278
     */
279
    public $unknown = [];
280
281
    /**
282
     * @param OptionsArray              $options    options of alter operation
283
     * @param Expression|string|null    $field      altered field
284
     * @param Component[]|ArrayObj|null $partitions partitions definition found in the operation
285
     * @param Token[]                   $unknown    unparsed tokens found at the end of operation
286
     */
287 180
    public function __construct(
288
        $options = null,
289
        $field = null,
290
        $partitions = null,
291
        $unknown = []
292
    ) {
293 180
        $this->partitions = $partitions;
294 180
        $this->options = $options;
295 180
        $this->field = $field;
296 180
        $this->unknown = $unknown;
297
    }
298
299
    /**
300
     * @param Parser               $parser  the parser that serves as context
301
     * @param TokensList           $list    the list of tokens that are being parsed
302
     * @param array<string, mixed> $options parameters for parsing
303
     *
304
     * @return AlterOperation
305
     */
306 180
    public static function parse(Parser $parser, TokensList $list, array $options = [])
307
    {
308 180
        $ret = new static();
309
310
        /**
311
         * Counts brackets.
312
         *
313
         * @var int
314
         */
315 180
        $brackets = 0;
316
317
        /**
318
         * The state of the parser.
319
         *
320
         * Below are the states of the parser.
321
         *
322
         *      0 ---------------------[ options ]---------------------> 1
323
         *
324
         *      1 ----------------------[ field ]----------------------> 2
325
         *
326
         *      1 -------------[ PARTITION / PARTITION BY ]------------> 3
327
         *
328
         *      2 -------------------------[ , ]-----------------------> 0
329
         *
330
         * @var int
331
         */
332 180
        $state = 0;
333
334
        /**
335
         * partition state.
336
         *
337
         * @var int
338
         */
339 180
        $partitionState = 0;
340
341 180
        for (; $list->idx < $list->count; ++$list->idx) {
342
            /**
343
             * Token parsed at this moment.
344
             */
345 180
            $token = $list->tokens[$list->idx];
346
347
            // End of statement.
348 180
            if ($token->type === Token::TYPE_DELIMITER) {
349 164
                break;
350
            }
351
352
            // Skipping comments.
353 180
            if ($token->type === Token::TYPE_COMMENT) {
354 2
                continue;
355
            }
356
357
            // Skipping whitespaces.
358 180
            if ($token->type === Token::TYPE_WHITESPACE) {
359 64
                if ($state === 2) {
360
                    // When parsing the unknown part, the whitespaces are
361
                    // included to not break anything.
362 56
                    $ret->unknown[] = $token;
363 56
                    continue;
364
                }
365
            }
366
367 180
            if ($state === 0) {
368 180
                $ret->options = OptionsArray::parse($parser, $list, $options);
369
370
                // Not only when aliasing but also when parsing the body of an event, we just list the tokens of the
371
                // body in the unknown tokens list, as they define their own statements.
372 180
                if ($ret->options->has('AS') || $ret->options->has('DO')) {
373 6
                    for (; $list->idx < $list->count; ++$list->idx) {
374 6
                        if ($list->tokens[$list->idx]->type === Token::TYPE_DELIMITER) {
375 6
                            break;
376
                        }
377
378 6
                        $ret->unknown[] = $list->tokens[$list->idx];
379
                    }
380
381 6
                    break;
382
                }
383
384 174
                $state = 1;
385 174
                if ($ret->options->has('PARTITION') || $token->value === 'PARTITION BY') {
386 8
                    $state = 3;
387 174
                    $list->getPrevious(); // in order to check whether it's partition or partition by.
388
                }
389 116
            } elseif ($state === 1) {
390 108
                $ret->field = Expression::parse(
391 108
                    $parser,
392 108
                    $list,
393 108
                    [
394 108
                        'breakOnAlias' => true,
395 108
                        'parseField' => 'column',
396 108
                    ]
397 108
                );
398 108
                if ($ret->field === null) {
399
                    // No field was read. We go back one token so the next
400
                    // iteration will parse the same token, but in state 2.
401 44
                    --$list->idx;
402
                }
403
404
                // If the operation is a RENAME COLUMN, now we have detected the field to rename, we need to parse
405
                // again the options to get the new name of the column.
406 108
                if ($ret->options->has('RENAME') && $ret->options->has('COLUMN')) {
407 12
                    $nextOptions = OptionsArray::parse($parser, $list, $options);
408 12
                    $ret->options->merge($nextOptions);
409
                }
410
411 108
                $state = 2;
412 102
            } elseif ($state === 2) {
413 94
                if (is_string($token->value) || is_numeric($token->value)) {
414 94
                    $arrayKey = $token->value;
415
                } else {
416 2
                    $arrayKey = $token->token;
417
                }
418
419 94
                if ($token->type === Token::TYPE_OPERATOR) {
420 60
                    if ($token->value === '(') {
421 40
                        ++$brackets;
422 60
                    } elseif ($token->value === ')') {
423 40
                        --$brackets;
424 44
                    } elseif (($token->value === ',') && ($brackets === 0)) {
425 60
                        break;
426
                    }
427 82
                } elseif (! self::checkIfTokenQuotedSymbol($token)) {
428
                    // If the current token is "SET" or "ENUM", we want to avoid the token between their parenthesis in
429
                    // the unknown tokens.
430 74
                    if (in_array($token->value, ['SET', 'ENUM'], true)) {
431 18
                        $list->idx++; // Ignore the current token
432 18
                        $nextToken = $list->getNext();
433
434 18
                        if ($nextToken !== null && $nextToken->value === '(') {
435 12
                            $list->getNextOfTypeAndValue(Token::TYPE_OPERATOR, ')');
436 6
                        } elseif ($nextToken !== null && $nextToken->value === 'DEFAULT') {
437
                            // to avoid adding the `DEFAULT` token to the unknown tokens.
438 4
                            ++$list->idx;
439
                        } else {
440 2
                            $parser->error(
441 2
                                'A new statement was found, but no delimiter between it and the previous one.',
442 2
                                $token
443 2
                            );
444 18
                            break;
445
                        }
446 68
                    } elseif (! empty(Parser::$statementParsers[$token->value])) {
447
                        // We have reached the end of ALTER operation and suddenly found
448
                        // a start to new statement, but have not found a delimiter between them
449 2
                        $parser->error(
450 2
                            'A new statement was found, but no delimiter between it and the previous one.',
451 2
                            $token
452 2
                        );
453 2
                        break;
454
                    } elseif (
455 68
                        (array_key_exists($arrayKey, self::$databaseOptions)
456 68
                        || array_key_exists($arrayKey, self::$tableOptions))
457 68
                        && ! self::checkIfColumnDefinitionKeyword($arrayKey)
458
                    ) {
459
                        // This alter operation has finished, which means a comma
460
                        // was missing before start of new alter operation
461 4
                        $parser->error('Missing comma before start of a new alter operation.', $token);
462 4
                        break;
463
                    }
464
                }
465
466 80
                $ret->unknown[] = $token;
467 8
            } elseif ($state === 3) {
468 8
                if ($partitionState === 0) {
469 8
                    $list->idx++; // Ignore the current token
470 8
                    $nextToken = $list->getNext();
471
                    if (
472 8
                        ($token->type === Token::TYPE_KEYWORD)
473 8
                        && (($token->keyword === 'PARTITION BY')
474 8
                        || ($token->keyword === 'PARTITION' && $nextToken && $nextToken->value !== '('))
475
                    ) {
476 8
                        $partitionState = 1;
477 2
                    } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->keyword === 'PARTITION')) {
478 2
                        $partitionState = 2;
479
                    }
480
481 8
                    --$list->idx; // to decrease the idx by one, because the last getNext returned and increased it.
482
483
                    // reverting the effect of the getNext
484 8
                    $list->getPrevious();
485 8
                    $list->getPrevious();
486
487 8
                    ++$list->idx; // to index the idx by one, because the last getPrevious returned and decreased it.
488 8
                } elseif ($partitionState === 1) {
489
                    // Fetch the next token in a way the current index is reset to manage whitespaces in "field".
490 8
                    $currIdx = $list->idx;
491 8
                    ++$list->idx;
492 8
                    $nextToken = $list->getNext();
493 8
                    $list->idx = $currIdx;
494
                    // Building the expression used for partitioning.
495 8
                    if (empty($ret->field)) {
496 8
                        $ret->field = '';
497
                    }
498
499
                    if (
500 8
                        $token->type === Token::TYPE_OPERATOR
501 8
                        && $token->value === '('
502
                        && $nextToken
503 8
                        && $nextToken->keyword === 'PARTITION'
504
                    ) {
505 6
                        $partitionState = 2;
506 6
                        --$list->idx; // Current idx is on "(". We need a step back for ArrayObj::parse incoming.
507
                    } else {
508 8
                        $ret->field .= $token->type === Token::TYPE_WHITESPACE ? ' ' : $token->token;
509
                    }
510 6
                } elseif ($partitionState === 2) {
511 6
                    $ret->partitions = ArrayObj::parse(
512 6
                        $parser,
513 6
                        $list,
514 6
                        ['type' => PartitionDefinition::class]
515 6
                    );
516
                }
517
            }
518
        }
519
520 180
        if ($ret->options->isEmpty()) {
521 2
            $parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
522
        }
523
524 180
        --$list->idx;
525
526 180
        return $ret;
527
    }
528
529
    /**
530
     * @param AlterOperation $component the component to be built
531
     */
532 24
    public static function build($component): string
533
    {
534
        // Specific case of RENAME COLUMN that insert the field between 2 options.
535 24
        $afterFieldsOptions = new OptionsArray();
536 24
        if ($component->options->has('RENAME') && $component->options->has('COLUMN')) {
537 8
            $afterFieldsOptions = clone $component->options;
538 8
            $afterFieldsOptions->remove('RENAME');
539 8
            $afterFieldsOptions->remove('COLUMN');
540 8
            $component->options->remove('TO');
541
        }
542
543 24
        $ret = $component->options . ' ';
544 24
        if (isset($component->field) && ($component->field !== '')) {
545 18
            $ret .= $component->field . ' ';
546
        }
547
548 24
        $ret .= $afterFieldsOptions . TokensList::build($component->unknown);
549
550 24
        if (isset($component->partitions)) {
551 2
            $ret .= PartitionDefinition::build($component->partitions);
0 ignored issues
show
Bug introduced by
It seems like $component->partitions can also be of type PhpMyAdmin\SqlParser\Components\ArrayObj; however, parameter $component of PhpMyAdmin\SqlParser\Com...tionDefinition::build() does only seem to accept PhpMyAdmin\SqlParser\Com...nts\PartitionDefinition, maybe add an additional type check? ( Ignorable by Annotation )

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

551
            $ret .= PartitionDefinition::build(/** @scrutinizer ignore-type */ $component->partitions);
Loading history...
552
        }
553
554 24
        return trim($ret);
555
    }
556
557
    /**
558
     * Check if token's value is one of the common keywords
559
     * between column and table alteration
560
     *
561
     * @param string $tokenValue Value of current token
562
     */
563 32
    private static function checkIfColumnDefinitionKeyword($tokenValue): bool
564
    {
565 32
        $commonOptions = [
566 32
            'AUTO_INCREMENT',
567 32
            'COMMENT',
568 32
            'DEFAULT',
569 32
            'CHARACTER SET',
570 32
            'COLLATE',
571 32
            'PRIMARY',
572 32
            'UNIQUE',
573 32
            'PRIMARY KEY',
574 32
            'UNIQUE KEY',
575 32
        ];
576
577
        // Since these options can be used for
578
        // both table as well as a specific column in the table
579 32
        return in_array($tokenValue, $commonOptions);
580
    }
581
582
    /**
583
     * Check if token is symbol and quoted with backtick
584
     *
585
     * @param Token $token token to check
586
     */
587 82
    private static function checkIfTokenQuotedSymbol($token): bool
588
    {
589 82
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
590
    }
591
592
    public function __toString(): string
593
    {
594
        return static::build($this);
595
    }
596
}
597