Passed
Push — master ( 6c3f09...c54108 )
by William
03:43
created

AlterOperation::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

534
            $ret .= PartitionDefinition::build(/** @scrutinizer ignore-type */ $component->partitions);
Loading history...
535
        }
536
537 22
        return trim($ret);
538
    }
539
540
    /**
541
     * Check if token's value is one of the common keywords
542
     * between column and table alteration
543
     *
544
     * @param string $tokenValue Value of current token
545
     *
546
     * @return bool
547
     */
548 28
    private static function checkIfColumnDefinitionKeyword($tokenValue)
549
    {
550 28
        $commonOptions = [
551 28
            'AUTO_INCREMENT',
552 28
            'COMMENT',
553 28
            'DEFAULT',
554 28
            'CHARACTER SET',
555 28
            'COLLATE',
556 28
            'PRIMARY',
557 28
            'UNIQUE',
558 28
            'PRIMARY KEY',
559 28
            'UNIQUE KEY',
560 28
        ];
561
562
        // Since these options can be used for
563
        // both table as well as a specific column in the table
564 28
        return in_array($tokenValue, $commonOptions);
565
    }
566
567
    /**
568
     * Check if token is symbol and quoted with backtick
569
     *
570
     * @param Token $token token to check
571
     *
572
     * @return bool
573
     */
574 66
    private static function checkIfTokenQuotedSymbol($token)
575
    {
576 66
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
577
    }
578
579
    public function __toString(): string
580
    {
581
        return static::build($this);
582
    }
583
}
584