Passed
Push — master ( b16987...8b6d77 )
by Maurício
03:49 queued 13s
created

AlterOperations::parse()   F

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
     * @param Parser               $parser  the parser that serves as context
237
     * @param TokensList           $list    the list of tokens that are being parsed
238
     * @param array<string, mixed> $options parameters for parsing
239
     */
240 186
    public static function parse(Parser $parser, TokensList $list, array $options = []): AlterOperation
241
    {
242 186
        $ret = new AlterOperation();
243
244
        /**
245
         * Counts brackets.
246
         */
247 186
        $brackets = 0;
248
249
        /**
250
         * The state of the parser.
251
         *
252
         * Below are the states of the parser.
253
         *
254
         *      0 ---------------------[ options ]---------------------> 1
255
         *
256
         *      1 ----------------------[ field ]----------------------> 2
257
         *
258
         *      1 -------------[ PARTITION / PARTITION BY ]------------> 3
259
         *
260
         *      2 -------------------------[ , ]-----------------------> 0
261
         */
262 186
        $state = 0;
263
264
        /**
265
         * partition state.
266
         */
267 186
        $partitionState = 0;
268
269 186
        for (; $list->idx < $list->count; ++$list->idx) {
270
            /**
271
             * Token parsed at this moment.
272
             */
273 186
            $token = $list->tokens[$list->idx];
274
275
            // End of statement.
276 186
            if ($token->type === TokenType::Delimiter) {
277 170
                break;
278
            }
279
280
            // Skipping comments.
281 186
            if ($token->type === TokenType::Comment) {
282 2
                continue;
283
            }
284
285
            // Skipping whitespaces.
286 186
            if ($token->type === TokenType::Whitespace) {
287 76
                if ($state === 2) {
288
                    // When parsing the unknown part, the whitespaces are
289
                    // included to not break anything.
290 68
                    $ret->unknown[] = $token;
291 68
                    continue;
292
                }
293
            }
294
295 186
            if ($state === 0) {
296 186
                $ret->options = OptionsArrays::parse($parser, $list, $options);
297
298
                // Not only when aliasing but also when parsing the body of an event, we just list the tokens of the
299
                // body in the unknown tokens list, as they define their own statements.
300 186
                if ($ret->options->has('AS') || $ret->options->has('DO')) {
301 6
                    for (; $list->idx < $list->count; ++$list->idx) {
302 6
                        if ($list->tokens[$list->idx]->type === TokenType::Delimiter) {
303 6
                            break;
304
                        }
305
306 6
                        $ret->unknown[] = $list->tokens[$list->idx];
307
                    }
308
309 6
                    break;
310
                }
311
312 180
                $state = 1;
313 180
                if ($ret->options->has('PARTITION') || $token->value === 'PARTITION BY') {
314 8
                    $state = 3;
315 8
                    $list->getPrevious(); // in order to check whether it's partition or partition by.
316
                }
317 122
            } elseif ($state === 1) {
318 114
                $ret->field = Expressions::parse(
319 114
                    $parser,
320 114
                    $list,
321 114
                    [
322 114
                        'breakOnAlias' => true,
323 114
                        'parseField' => 'column',
324 114
                    ],
325 114
                );
326 114
                if ($ret->field === null) {
327
                    // No field was read. We go back one token so the next
328
                    // iteration will parse the same token, but in state 2.
329 44
                    --$list->idx;
330
                }
331
332
                // If the operation is a RENAME COLUMN, now we have detected the field to rename, we need to parse
333
                // again the options to get the new name of the column.
334 114
                if ($ret->options->has('RENAME') && $ret->options->has('COLUMN')) {
335 12
                    $nextOptions = OptionsArrays::parse($parser, $list, $options);
336 12
                    $ret->options->merge($nextOptions);
337
                }
338
339 114
                $state = 2;
340 108
            } elseif ($state === 2) {
341 100
                if (is_string($token->value) || is_int($token->value)) {
342 100
                    $arrayKey = $token->value;
343
                } else {
344 2
                    $arrayKey = $token->token;
345
                }
346
347 100
                if ($token->type === TokenType::Operator) {
348 74
                    if ($token->value === '(') {
349 54
                        ++$brackets;
350 74
                    } elseif ($token->value === ')') {
351 54
                        --$brackets;
352 48
                    } elseif (($token->value === ',') && ($brackets === 0)) {
353 36
                        break;
354
                    }
355 88
                } elseif (! self::checkIfTokenQuotedSymbol($token) && $token->type !== TokenType::String) {
356 80
                    if (isset(Parser::STATEMENT_PARSERS[$arrayKey]) && Parser::STATEMENT_PARSERS[$arrayKey] !== '') {
357 12
                        $list->idx++; // Ignore the current token
358 12
                        $nextToken = $list->getNext();
359
360 12
                        if ($token->value === 'SET' && $nextToken !== null && $nextToken->value === '(') {
361
                            // To avoid adding the tokens between the SET() parentheses to the unknown tokens
362 4
                            $list->getNextOfTypeAndValue(TokenType::Operator, ')');
363 8
                        } elseif ($token->value === 'SET' && $nextToken !== null && $nextToken->value === 'DEFAULT') {
364
                            // to avoid adding the `DEFAULT` token to the unknown tokens.
365 4
                            ++$list->idx;
366
                        } else {
367
                            // We have reached the end of ALTER operation and suddenly found
368
                            // a start to new statement, but have not found a delimiter between them
369 4
                            $parser->error(
370 4
                                'A new statement was found, but no delimiter between it and the previous one.',
371 4
                                $token,
372 4
                            );
373 4
                            break;
374
                        }
375
                    } elseif (
376 72
                        (array_key_exists($arrayKey, self::DATABASE_OPTIONS)
377 72
                        || array_key_exists($arrayKey, self::TABLE_OPTIONS))
378 72
                        && ! self::checkIfColumnDefinitionKeyword($arrayKey)
379
                    ) {
380
                        // This alter operation has finished, which means a comma
381
                        // was missing before start of new alter operation
382 4
                        $parser->error('Missing comma before start of a new alter operation.', $token);
383 4
                        break;
384
                    }
385
                }
386
387 86
                $ret->unknown[] = $token;
388 8
            } elseif ($state === 3) {
389 8
                if ($partitionState === 0) {
390 8
                    $list->idx++; // Ignore the current token
391 8
                    $nextToken = $list->getNext();
392
                    if (
393 8
                        ($token->type === TokenType::Keyword)
394 8
                        && (($token->keyword === 'PARTITION BY')
395 8
                        || ($token->keyword === 'PARTITION' && $nextToken && $nextToken->value !== '('))
396
                    ) {
397 8
                        $partitionState = 1;
398 2
                    } elseif (($token->type === TokenType::Keyword) && ($token->keyword === 'PARTITION')) {
399 2
                        $partitionState = 2;
400
                    }
401
402 8
                    --$list->idx; // to decrease the idx by one, because the last getNext returned and increased it.
403
404
                    // reverting the effect of the getNext
405 8
                    $list->getPrevious();
406 8
                    $list->getPrevious();
407
408 8
                    ++$list->idx; // to index the idx by one, because the last getPrevious returned and decreased it.
409 8
                } elseif ($partitionState === 1) {
410
                    // Fetch the next token in a way the current index is reset to manage whitespaces in "field".
411 8
                    $currIdx = $list->idx;
412 8
                    ++$list->idx;
413 8
                    $nextToken = $list->getNext();
414 8
                    $list->idx = $currIdx;
415
                    // Building the expression used for partitioning.
416 8
                    if (empty($ret->field)) {
417 8
                        $ret->field = '';
418
                    }
419
420
                    if (
421 8
                        $token->type === TokenType::Operator
422 8
                        && $token->value === '('
423
                        && $nextToken
424 8
                        && $nextToken->keyword === 'PARTITION'
425
                    ) {
426 6
                        $partitionState = 2;
427 6
                        --$list->idx; // Current idx is on "(". We need a step back for ArrayObj::parse incoming.
428
                    } else {
429 8
                        $ret->field .= $token->type === TokenType::Whitespace ? ' ' : $token->token;
430
                    }
431 6
                } elseif ($partitionState === 2) {
432 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...
433 6
                        $parser,
434 6
                        $list,
435 6
                        ['type' => PartitionDefinitions::class],
436 6
                    );
437
                }
438
            }
439
        }
440
441 186
        if ($ret->options->isEmpty()) {
442 2
            $parser->error('Unrecognized alter operation.', $list->tokens[$list->idx]);
443
        }
444
445 186
        --$list->idx;
446
447 186
        return $ret;
448
    }
449
450
    /**
451
     * Check if token's value is one of the common keywords
452
     * between column and table alteration
453
     *
454
     * @param string $tokenValue Value of current token
455
     */
456 34
    private static function checkIfColumnDefinitionKeyword(string $tokenValue): bool
457
    {
458 34
        $commonOptions = [
459 34
            'AUTO_INCREMENT',
460 34
            'COMMENT',
461 34
            'DEFAULT',
462 34
            'CHARACTER SET',
463 34
            'COLLATE',
464 34
            'PRIMARY',
465 34
            'UNIQUE',
466 34
            'PRIMARY KEY',
467 34
            'UNIQUE KEY',
468 34
        ];
469
470
        // Since these options can be used for
471
        // both table as well as a specific column in the table
472 34
        return in_array($tokenValue, $commonOptions);
473
    }
474
475
    /**
476
     * Check if token is symbol and quoted with backtick
477
     *
478
     * @param Token $token token to check
479
     */
480 88
    private static function checkIfTokenQuotedSymbol(Token $token): bool
481
    {
482 88
        return $token->type === TokenType::Symbol && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
483
    }
484
}
485