AlterOperations::parse()   F
last analyzed

Complexity

Conditions 56
Paths 36

Size

Total Lines 208
Code Lines 110

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 109
CRAP Score 56

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 56
eloc 110
nc 36
nop 3
dl 0
loc 208
ccs 109
cts 109
cp 1
crap 56
rs 3.3333
c 1
b 0
f 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\Parsers;
6
7
use PhpMyAdmin\SqlParser\Components\AlterOperation;
8
use PhpMyAdmin\SqlParser\Parseable;
9
use PhpMyAdmin\SqlParser\Parser;
10
use PhpMyAdmin\SqlParser\Token;
11
use PhpMyAdmin\SqlParser\TokensList;
12
use PhpMyAdmin\SqlParser\TokenType;
13
14
use function array_key_exists;
15
use function in_array;
16
use function is_int;
17
use function is_string;
18
19
/**
20
 * Parses an alter operation.
21
 */
22
final class AlterOperations implements Parseable
23
{
24
    /**
25
     * All database options.
26
     */
27
    public const DATABASE_OPTIONS = [
28
        'CHARACTER SET' => [
29
            1,
30
            'var',
31
        ],
32
        'CHARSET' => [
33
            1,
34
            'var',
35
        ],
36
        'DEFAULT CHARACTER SET' => [
37
            1,
38
            'var',
39
        ],
40
        'DEFAULT CHARSET' => [
41
            1,
42
            'var',
43
        ],
44
        'UPGRADE' => [
45
            1,
46
            'var',
47
        ],
48
        'COLLATE' => [
49
            2,
50
            'var',
51
        ],
52
        'DEFAULT COLLATE' => [
53
            2,
54
            'var',
55
        ],
56
    ];
57
58
    /**
59
     * All table options.
60
     */
61
    public const TABLE_OPTIONS = [
62
        'ENGINE' => [
63
            1,
64
            'var=',
65
        ],
66
        'ALGORITHM' => [
67
            1,
68
            'var=',
69
        ],
70
        'AUTO_INCREMENT' => [
71
            1,
72
            'var=',
73
        ],
74
        'AVG_ROW_LENGTH' => [
75
            1,
76
            'var',
77
        ],
78
        'COALESCE PARTITION' => [
79
            1,
80
            'var',
81
        ],
82
        'LOCK' => [
83
            1,
84
            'var=',
85
        ],
86
        'MAX_ROWS' => [
87
            1,
88
            'var',
89
        ],
90
        'ROW_FORMAT' => [
91
            1,
92
            'var',
93
        ],
94
        'COMMENT' => [
95
            1,
96
            'var',
97
        ],
98
        'ADD' => 1,
99
        'ALTER' => 1,
100
        'ANALYZE' => 1,
101
        'CHANGE' => 1,
102
        'CHARSET' => 1,
103
        'CHECK' => 1,
104
        'CONVERT' => 1,
105
        'DEFAULT CHARSET' => 1,
106
        'DISABLE' => 1,
107
        'DISCARD' => 1,
108
        'DROP' => 1,
109
        'ENABLE' => 1,
110
        'IMPORT' => 1,
111
        'MODIFY' => 1,
112
        'OPTIMIZE' => 1,
113
        'ORDER' => 1,
114
        'REBUILD' => 1,
115
        'REMOVE' => 1,
116
        'RENAME' => 1,
117
        'REORGANIZE' => 1,
118
        'REPAIR' => 1,
119
        'UPGRADE' => 1,
120
121
        'COLUMN' => 2,
122
        'CONSTRAINT' => 2,
123
        'DEFAULT' => 2,
124
        'BY' => 2,
125
        'FOREIGN' => 2,
126
        'FULLTEXT' => 2,
127
        'KEY' => 2,
128
        'KEYS' => 2,
129
        'PARTITION' => 2,
130
        'PARTITION BY' => 2,
131
        'PARTITIONING' => 2,
132
        'PRIMARY KEY' => 2,
133
        'SPATIAL' => 2,
134
        'TABLESPACE' => 2,
135
        'INDEX' => [
136
            2,
137
            'var',
138
        ],
139
140
        'CHARACTER SET' => 3,
141
        'TO' => [
142
            3,
143
            'var',
144
        ],
145
    ];
146
147
    /**
148
     * All user options.
149
     */
150
    public const USER_OPTIONS = [
151
        'ATTRIBUTE' => [
152
            1,
153
            'var',
154
        ],
155
        'COMMENT' => [
156
            1,
157
            'var',
158
        ],
159
        'REQUIRE' => [
160
            1,
161
            'var',
162
        ],
163
164
        'IDENTIFIED VIA' => [
165
            2,
166
            'var',
167
        ],
168
        'IDENTIFIED WITH' => [
169
            2,
170
            'var',
171
        ],
172
        'PASSWORD' => [
173
            2,
174
            'var',
175
        ],
176
        'WITH' => [
177
            2,
178
            'var',
179
        ],
180
181
        'BY' => [
182
            4,
183
            'expr',
184
        ],
185
186
        'ACCOUNT' => 1,
187
        'DEFAULT' => 1,
188
189
        'LOCK' => 2,
190
        'UNLOCK' => 2,
191
192
        'IDENTIFIED' => 3,
193
    ];
194
195
    /**
196
     * All view options.
197
     */
198
    public const VIEW_OPTIONS = ['AS' => 1];
199
200
    /**
201
     * All event options.
202
     */
203
    public const EVENT_OPTIONS = [
204
        'ON SCHEDULE' => 1,
205
        'EVERY' => [
206
            2,
207
            'expr',
208
        ],
209
        'AT' => [
210
            2,
211
            'expr',
212
        ],
213
        'STARTS' => [
214
            3,
215
            'expr',
216
        ],
217
        'ENDS' => [
218
            4,
219
            'expr',
220
        ],
221
        'ON COMPLETION PRESERVE' => 5,
222
        'ON COMPLETION NOT PRESERVE' => 5,
223
        'RENAME' => 6,
224
        'TO' => [7, 'expr', ['parseField' => 'table', 'breakOnAlias' => true]],
225
        'ENABLE' => 8,
226
        'DISABLE' => 8,
227
        'DISABLE ON SLAVE' => 8,
228
        'COMMENT' => [
229
            9,
230
            'var',
231
        ],
232
        'DO' => 10,
233
    ];
234
235
    /**
236
     * All routine (procedure or function) options.
237
     */
238
    public const ROUTINE_OPTIONS = [
239
        'COMMENT' => [1, 'var'],
240
        'LANGUAGE SQL' => 2,
241
        'CONTAINS SQL' => 3,
242
        'NO SQL' => 3,
243
        'READS SQL DATA' => 3,
244
        'MODIFIES SQL DATA' => 3,
245
        'SQL SECURITY' => 4,
246
        'DEFINER' => 5,
247
        'INVOKER' => 5,
248
    ];
249
250
    /**
251
     * @param Parser               $parser  the parser that serves as context
252
     * @param TokensList           $list    the list of tokens that are being parsed
253
     * @param array<string, mixed> $options parameters for parsing
254
     */
255 236
    public static function parse(Parser $parser, TokensList $list, array $options = []): AlterOperation
256
    {
257 236
        $ret = new AlterOperation();
258
259
        /**
260
         * Counts brackets.
261
         */
262 236
        $brackets = 0;
263
264
        /**
265
         * The state of the parser.
266
         *
267
         * Below are the states of the parser.
268
         *
269
         *      0 ---------------------[ options ]---------------------> 1
270
         *
271
         *      1 ----------------------[ field ]----------------------> 2
272
         *
273
         *      1 -------------[ PARTITION / PARTITION BY ]------------> 3
274
         *
275
         *      2 -------------------------[ , ]-----------------------> 0
276
         */
277 236
        $state = 0;
278
279
        /**
280
         * partition state.
281
         */
282 236
        $partitionState = 0;
283
284 236
        for (; $list->idx < $list->count; ++$list->idx) {
285
            /**
286
             * Token parsed at this moment.
287
             */
288 236
            $token = $list->tokens[$list->idx];
289
290
            // End of statement.
291 236
            if ($token->type === TokenType::Delimiter) {
292 218
                break;
293
            }
294
295
            // Skipping comments.
296 236
            if ($token->type === TokenType::Comment) {
297 2
                continue;
298
            }
299
300
            // Skipping whitespaces.
301 236
            if ($token->type === TokenType::Whitespace) {
302 78
                if ($state === 2) {
303
                    // When parsing the unknown part, the whitespaces are
304
                    // included to not break anything.
305 70
                    $ret->unknown[] = $token;
306 70
                    continue;
307
                }
308
            }
309
310 236
            if ($state === 0) {
311 236
                $ret->options = OptionsArrays::parse($parser, $list, $options);
312
313
                // Not only when aliasing but also when parsing the body of an event, we just list the tokens of the
314
                // body in the unknown tokens list, as they define their own statements.
315 236
                if ($ret->options->has('AS') || $ret->options->has('DO')) {
316 6
                    for (; $list->idx < $list->count; ++$list->idx) {
317 6
                        if ($list->tokens[$list->idx]->type === TokenType::Delimiter) {
318 6
                            break;
319
                        }
320
321 6
                        $ret->unknown[] = $list->tokens[$list->idx];
322
                    }
323
324 6
                    break;
325
                }
326
327 230
                $state = 1;
328 230
                if ($ret->options->has('PARTITION') || $token->value === 'PARTITION BY') {
329 8
                    $state = 3;
330 8
                    $list->getPrevious(); // in order to check whether it's partition or partition by.
331
                }
332 124
            } elseif ($state === 1) {
333 116
                $ret->field = Expressions::parse(
334 116
                    $parser,
335 116
                    $list,
336 116
                    [
337 116
                        'breakOnAlias' => true,
338 116
                        'parseField' => 'column',
339 116
                    ],
340 116
                );
341 116
                if ($ret->field === null) {
342
                    // No field was read. We go back one token so the next
343
                    // iteration will parse the same token, but in state 2.
344 44
                    --$list->idx;
345
                }
346
347
                // If the operation is a RENAME COLUMN, now we have detected the field to rename, we need to parse
348
                // again the options to get the new name of the column.
349 116
                if ($ret->options->has('RENAME') && $ret->options->has('COLUMN')) {
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

349
                if ($ret->options->/** @scrutinizer ignore-call */ has('RENAME') && $ret->options->has('COLUMN')) {

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...
350 12
                    $nextOptions = OptionsArrays::parse($parser, $list, $options);
351 12
                    $ret->options->merge($nextOptions);
352
                }
353
354 116
                $state = 2;
355 110
            } elseif ($state === 2) {
356 102
                if (is_string($token->value) || is_int($token->value)) {
357 102
                    $arrayKey = $token->value;
358
                } else {
359 2
                    $arrayKey = $token->token;
360
                }
361
362 102
                if ($token->type === TokenType::Operator) {
363 74
                    if ($token->value === '(') {
364 54
                        ++$brackets;
365 74
                    } elseif ($token->value === ')') {
366 54
                        --$brackets;
367 48
                    } elseif (($token->value === ',') && ($brackets === 0)) {
368 36
                        break;
369
                    }
370 90
                } elseif (! self::checkIfTokenQuotedSymbol($token) && $token->type !== TokenType::String) {
371 82
                    if (isset(Parser::STATEMENT_PARSERS[$arrayKey]) && Parser::STATEMENT_PARSERS[$arrayKey] !== '') {
372 14
                        $list->idx++; // Ignore the current token
373 14
                        $nextToken = $list->getNext();
374
375 14
                        if ($token->value === 'SET' && $nextToken !== null && $nextToken->value === '(') {
376
                            // To avoid adding the tokens between the SET() parentheses to the unknown tokens
377 4
                            $list->getNextOfTypeAndValue(TokenType::Operator, ')');
378 10
                        } elseif ($token->value === 'SET' && $nextToken !== null && $nextToken->value === 'DEFAULT') {
379
                            // to avoid adding the `DEFAULT` token to the unknown tokens.
380 4
                            ++$list->idx;
381
                        } else {
382
                            // We have reached the end of ALTER operation and suddenly found
383
                            // a start to new statement, but have not found a delimiter between them
384 6
                            $parser->error(
385 6
                                'A new statement was found, but no delimiter between it and the previous one.',
386 6
                                $token,
387 6
                            );
388 6
                            break;
389
                        }
390
                    } elseif (
391 74
                        (array_key_exists($arrayKey, self::DATABASE_OPTIONS)
392 74
                        || array_key_exists($arrayKey, self::TABLE_OPTIONS))
393 74
                        && ! self::checkIfColumnDefinitionKeyword($arrayKey)
394
                    ) {
395
                        // This alter operation has finished, which means a comma
396
                        // was missing before start of new alter operation
397 4
                        $parser->error('Missing comma before start of a new alter operation.', $token);
398 4
                        break;
399
                    }
400
                }
401
402 88
                $ret->unknown[] = $token;
403 8
            } elseif ($state === 3) {
404 8
                if ($partitionState === 0) {
405 8
                    $list->idx++; // Ignore the current token
406 8
                    $nextToken = $list->getNext();
407
                    if (
408 8
                        ($token->type === TokenType::Keyword)
409 8
                        && (($token->keyword === 'PARTITION BY')
410 8
                        || ($token->keyword === 'PARTITION' && $nextToken && $nextToken->value !== '('))
411
                    ) {
412 8
                        $partitionState = 1;
413 2
                    } elseif (($token->type === TokenType::Keyword) && ($token->keyword === 'PARTITION')) {
414 2
                        $partitionState = 2;
415
                    }
416
417 8
                    --$list->idx; // to decrease the idx by one, because the last getNext returned and increased it.
418
419
                    // reverting the effect of the getNext
420 8
                    $list->getPrevious();
421 8
                    $list->getPrevious();
422
423 8
                    ++$list->idx; // to index the idx by one, because the last getPrevious returned and decreased it.
424 8
                } elseif ($partitionState === 1) {
425
                    // Fetch the next token in a way the current index is reset to manage whitespaces in "field".
426 8
                    $currIdx = $list->idx;
427 8
                    ++$list->idx;
428 8
                    $nextToken = $list->getNext();
429 8
                    $list->idx = $currIdx;
430
                    // Building the expression used for partitioning.
431 8
                    if (empty($ret->field)) {
432 8
                        $ret->field = '';
433
                    }
434
435
                    if (
436 8
                        $token->type === TokenType::Operator
437 8
                        && $token->value === '('
438
                        && $nextToken
439 8
                        && $nextToken->keyword === 'PARTITION'
440
                    ) {
441 6
                        $partitionState = 2;
442 6
                        --$list->idx; // Current idx is on "(". We need a step back for ArrayObj::parse incoming.
443
                    } else {
444 8
                        $ret->field .= $token->type === TokenType::Whitespace ? ' ' : $token->token;
445
                    }
446 6
                } elseif ($partitionState === 2) {
447 6
                    $ret->partitions = ArrayObjs::parse(
0 ignored issues
show
Documentation Bug introduced by
It seems like PhpMyAdmin\SqlParser\Par...ionDefinitions::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...
448 6
                        $parser,
449 6
                        $list,
450 6
                        ['type' => PartitionDefinitions::class],
451 6
                    );
452
                }
453
            }
454
        }
455
456 236
        if ($ret->options->isEmpty()) {
457 2
            $parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
458
        }
459
460 236
        --$list->idx;
461
462 236
        return $ret;
463
    }
464
465
    /**
466
     * Check if token's value is one of the common keywords
467
     * between column and table alteration
468
     *
469
     * @param string $tokenValue Value of current token
470
     */
471 36
    private static function checkIfColumnDefinitionKeyword(string $tokenValue): bool
472
    {
473 36
        $commonOptions = [
474 36
            'AUTO_INCREMENT',
475 36
            'COMMENT',
476 36
            'DEFAULT',
477 36
            'CHARACTER SET',
478 36
            'COLLATE',
479 36
            'PRIMARY',
480 36
            'UNIQUE',
481 36
            'PRIMARY KEY',
482 36
            'UNIQUE KEY',
483 36
        ];
484
485
        // Since these options can be used for
486
        // both table as well as a specific column in the table
487 36
        return in_array($tokenValue, $commonOptions);
488
    }
489
490
    /**
491
     * Check if token is symbol and quoted with backtick
492
     *
493
     * @param Token $token token to check
494
     */
495 90
    private static function checkIfTokenQuotedSymbol(Token $token): bool
496
    {
497 90
        return $token->type === TokenType::Symbol && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
498
    }
499
}
500