Passed
Push — master ( 8d7732...4077a8 )
by Maurício
03:59 queued 12s
created

AlterOperation::parse()   F

Complexity

Conditions 56
Paths 36

Size

Total Lines 214
Code Lines 110

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 109
CRAP Score 56

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 56
eloc 110
c 2
b 0
f 0
nc 36
nop 3
dl 0
loc 214
ccs 109
cts 109
cp 1
crap 56
rs 3.3333

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
481 6
                        $parser,
482 6
                        $list,
483 6
                        ['type' => PartitionDefinition::class]
484 6
                    );
485
                }
486
            }
487
        }
488
489 186
        if ($ret->options->isEmpty()) {
490 2
            $parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
491
        }
492
493 186
        --$list->idx;
494
495 186
        return $ret;
496
    }
497
498 24
    public function build(): string
499
    {
500
        // Specific case of RENAME COLUMN that insert the field between 2 options.
501 24
        $afterFieldsOptions = new OptionsArray();
502 24
        if ($this->options->has('RENAME') && $this->options->has('COLUMN')) {
503 8
            $afterFieldsOptions = clone $this->options;
504 8
            $afterFieldsOptions->remove('RENAME');
505 8
            $afterFieldsOptions->remove('COLUMN');
506 8
            $this->options->remove('TO');
507
        }
508
509 24
        $ret = $this->options . ' ';
510 24
        if (isset($this->field) && ($this->field !== '')) {
511 18
            $ret .= $this->field . ' ';
512
        }
513
514 24
        $ret .= $afterFieldsOptions . TokensList::buildFromArray($this->unknown);
515
516 24
        if (isset($this->partitions)) {
517 2
            $ret .= PartitionDefinition::buildAll($this->partitions);
0 ignored issues
show
Bug introduced by
It seems like $this->partitions can also be of type null; however, parameter $component of PhpMyAdmin\SqlParser\Com...nDefinition::buildAll() does only seem to accept array, 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

517
            $ret .= PartitionDefinition::buildAll(/** @scrutinizer ignore-type */ $this->partitions);
Loading history...
518
        }
519
520 24
        return trim($ret);
521
    }
522
523
    /**
524
     * Check if token's value is one of the common keywords
525
     * between column and table alteration
526
     *
527
     * @param string $tokenValue Value of current token
528
     */
529 34
    private static function checkIfColumnDefinitionKeyword($tokenValue): bool
530
    {
531 34
        $commonOptions = [
532 34
            'AUTO_INCREMENT',
533 34
            'COMMENT',
534 34
            'DEFAULT',
535 34
            'CHARACTER SET',
536 34
            'COLLATE',
537 34
            'PRIMARY',
538 34
            'UNIQUE',
539 34
            'PRIMARY KEY',
540 34
            'UNIQUE KEY',
541 34
        ];
542
543
        // Since these options can be used for
544
        // both table as well as a specific column in the table
545 34
        return in_array($tokenValue, $commonOptions);
546
    }
547
548
    /**
549
     * Check if token is symbol and quoted with backtick
550
     *
551
     * @param Token $token token to check
552
     */
553 88
    private static function checkIfTokenQuotedSymbol($token): bool
554
    {
555 88
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
556
    }
557
558
    public function __toString(): string
559
    {
560
        return $this->build();
561
    }
562
}
563