Passed
Push — master ( 4d09f0...b17b21 )
by William
03:35 queued 54s
created

AlterOperation   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 517
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 1 Features 0
Metric Value
wmc 56
eloc 233
c 4
b 1
f 0
dl 0
loc 517
ccs 125
cts 125
cp 1
rs 5.5199

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A checkIfColumnDefinitionKeyword() 0 17 1
D parse() 0 192 48
A checkIfTokenQuotedSymbol() 0 3 2
A build() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like AlterOperation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

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

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

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

496
            $ret .= PartitionDefinition::build(/** @scrutinizer ignore-type */ $component->partitions);
Loading history...
497
        }
498
499 24
        return $ret;
500
    }
501
502
    /**
503
     * Check if token's value is one of the common keywords
504
     * between column and table alteration
505
     *
506
     * @param string $tokenValue Value of current token
507
     *
508
     * @return bool
509
     */
510 56
    private static function checkIfColumnDefinitionKeyword($tokenValue)
511
    {
512 56
        $commonOptions = [
513 56
            'AUTO_INCREMENT',
514 56
            'COMMENT',
515 56
            'DEFAULT',
516 56
            'CHARACTER SET',
517 56
            'COLLATE',
518 56
            'PRIMARY',
519 56
            'UNIQUE',
520 56
            'PRIMARY KEY',
521 56
            'UNIQUE KEY',
522 56
        ];
523
524
        // Since these options can be used for
525
        // both table as well as a specific column in the table
526 56
        return in_array($tokenValue, $commonOptions);
527
    }
528
529
    /**
530
     * Check if token is symbol and quoted with backtick
531
     *
532
     * @param Token $token token to check
533
     *
534
     * @return bool
535
     */
536 116
    private static function checkIfTokenQuotedSymbol($token)
537
    {
538 116
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
539
    }
540
}
541