Passed
Push — master ( cdeeca...9b5c0a )
by William
09:37 queued 10s
created

AlterOperation   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 153
dl 0
loc 355
ccs 77
cts 77
cp 1
rs 9.6
c 2
b 0
f 0
wmc 35

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A checkIfColumnDefinitionKeyword() 0 17 1
D parse() 0 141 28
A checkIfTokenQuotedSymbol() 0 3 2
A build() 0 10 3
1
<?php
2
/**
3
 * Parses an alter operation.
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\SqlParser\Components;
9
10
use PhpMyAdmin\SqlParser\Component;
11
use PhpMyAdmin\SqlParser\Parser;
12
use PhpMyAdmin\SqlParser\Token;
13
use PhpMyAdmin\SqlParser\TokensList;
14
use function array_key_exists;
15
use function in_array;
16
use function is_numeric;
17
use function is_string;
18
19
/**
20
 * Parses an alter operation.
21
 */
22
class AlterOperation extends Component
23
{
24
    /**
25
     * All database options.
26
     *
27
     * @var array
28
     */
29
    public static $DB_OPTIONS = [
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
64
     */
65
    public static $TABLE_OPTIONS = [
66
        'ENGINE' => [
67
            1,
68
            'var=',
69
        ],
70
        'AUTO_INCREMENT' => [
71
            1,
72
            'var=',
73
        ],
74
        'AVG_ROW_LENGTH' => [
75
            1,
76
            'var',
77
        ],
78
        'MAX_ROWS' => [
79
            1,
80
            'var',
81
        ],
82
        'ROW_FORMAT' => [
83
            1,
84
            'var',
85
        ],
86
        'COMMENT' => [
87
            1,
88
            'var',
89
        ],
90
        'ADD' => 1,
91
        'ALTER' => 1,
92
        'ANALYZE' => 1,
93
        'CHANGE' => 1,
94
        'CHECK' => 1,
95
        'COALESCE' => 1,
96
        'CONVERT' => 1,
97
        'DISABLE' => 1,
98
        'DISCARD' => 1,
99
        'DROP' => 1,
100
        'ENABLE' => 1,
101
        'IMPORT' => 1,
102
        'MODIFY' => 1,
103
        'OPTIMIZE' => 1,
104
        'ORDER' => 1,
105
        'PARTITION' => 1,
106
        'REBUILD' => 1,
107
        'REMOVE' => 1,
108
        'RENAME' => 1,
109
        'REORGANIZE' => 1,
110
        'REPAIR' => 1,
111
        'UPGRADE' => 1,
112
113
        'COLUMN' => 2,
114
        'CONSTRAINT' => 2,
115
        'DEFAULT' => 2,
116
        'TO' => 2,
117
        'BY' => 2,
118
        'FOREIGN' => 2,
119
        'FULLTEXT' => 2,
120
        'KEY' => 2,
121
        'KEYS' => 2,
122
        'PARTITIONING' => 2,
123
        'PRIMARY KEY' => 2,
124
        'SPATIAL' => 2,
125
        'TABLESPACE' => 2,
126
        'INDEX' => 2,
127
    ];
128
129
    /**
130
     * All view options.
131
     *
132
     * @var array
133
     */
134
    public static $VIEW_OPTIONS = ['AS' => 1];
135
136
    /**
137
     * Options of this operation.
138
     *
139
     * @var OptionsArray
140
     */
141
    public $options;
142
143
    /**
144
     * The altered field.
145
     *
146
     * @var Expression
147
     */
148
    public $field;
149
150
    /**
151
     * Unparsed tokens.
152
     *
153
     * @var Token[]|string
154
     */
155
    public $unknown = [];
156
157
    /**
158
     * @param OptionsArray $options options of alter operation
159
     * @param Expression   $field   altered field
160
     * @param array        $unknown unparsed tokens found at the end of operation
161
     */
162 80
    public function __construct(
163
        $options = null,
164
        $field = null,
165
        $unknown = []
166
    ) {
167 80
        $this->options = $options;
168 80
        $this->field = $field;
169 80
        $this->unknown = $unknown;
170 80
    }
171
172
    /**
173
     * @param Parser     $parser  the parser that serves as context
174
     * @param TokensList $list    the list of tokens that are being parsed
175
     * @param array      $options parameters for parsing
176
     *
177
     * @return AlterOperation
178
     */
179 80
    public static function parse(Parser $parser, TokensList $list, array $options = [])
180
    {
181 80
        $ret = new static();
182
183
        /**
184
         * Counts brackets.
185
         *
186
         * @var int
187
         */
188 80
        $brackets = 0;
189
190
        /**
191
         * The state of the parser.
192
         *
193
         * Below are the states of the parser.
194
         *
195
         *      0 ---------------------[ options ]---------------------> 1
196
         *
197
         *      1 ----------------------[ field ]----------------------> 2
198
         *
199
         *      2 -------------------------[ , ]-----------------------> 0
200
         *
201
         * @var int
202
         */
203 80
        $state = 0;
204
205 80
        for (; $list->idx < $list->count; ++$list->idx) {
206
            /**
207
             * Token parsed at this moment.
208
             *
209
             * @var Token
210
             */
211 80
            $token = $list->tokens[$list->idx];
212
213
            // End of statement.
214 80
            if ($token->type === Token::TYPE_DELIMITER) {
215 60
                break;
216
            }
217
218
            // Skipping comments.
219 80
            if ($token->type === Token::TYPE_COMMENT) {
220 4
                continue;
221
            }
222
223
            // Skipping whitespaces.
224 80
            if ($token->type === Token::TYPE_WHITESPACE) {
225 48
                if ($state === 2) {
226
                    // When parsing the unknown part, the whitespaces are
227
                    // included to not break anything.
228 48
                    $ret->unknown[] = $token;
229
                }
230
231 48
                continue;
232
            }
233
234 80
            if ($state === 0) {
235 80
                $ret->options = OptionsArray::parse($parser, $list, $options);
236
237 80
                if ($ret->options->has('AS')) {
238 4
                    for (; $list->idx < $list->count; ++$list->idx) {
239 4
                        if ($list->tokens[$list->idx]->type === Token::TYPE_DELIMITER) {
240 4
                            break;
241
                        }
242
243 4
                        $ret->unknown[] = $list->tokens[$list->idx];
244
                    }
245
246 4
                    break;
247
                }
248
249 76
                $state = 1;
250 64
            } elseif ($state === 1) {
251 64
                $ret->field = Expression::parse(
252 64
                    $parser,
253 64
                    $list,
254
                    [
255 64
                        'breakOnAlias' => true,
256
                        'parseField' => 'column',
257
                    ]
258
                );
259 64
                if ($ret->field === null) {
260
                    // No field was read. We go back one token so the next
261
                    // iteration will parse the same token, but in state 2.
262 12
                    --$list->idx;
263
                }
264
265 64
                $state = 2;
266 56
            } elseif ($state === 2) {
267 56
                $array_key = '';
268 56
                if (is_string($token->value) || is_numeric($token->value)) {
269 56
                    $array_key = $token->value;
270
                } else {
271 4
                    $array_key = $token->token;
272
                }
273 56
                if ($token->type === Token::TYPE_OPERATOR) {
274 44
                    if ($token->value === '(') {
275 40
                        ++$brackets;
276 44
                    } elseif ($token->value === ')') {
277 40
                        --$brackets;
278 20
                    } elseif (($token->value === ',') && ($brackets === 0)) {
279 44
                        break;
280
                    }
281 56
                } elseif (! self::checkIfTokenQuotedSymbol($token)) {
282
                    if (! empty(Parser::$STATEMENT_PARSERS[$token->value])) {
283
                        // We have reached the end of ALTER operation and suddenly found
284
                        // a start to new statement, but have not find a delimiter between them
285 8
286 8
                        if (! ($token->value === 'SET' && $list->tokens[$list->idx - 1]->value === 'CHARACTER')) {
287 8
                            $parser->error(
288 4
                                'A new statement was found, but no delimiter between it and the previous one.',
289
                                $token
290 8
                            );
291
                            break;
292 42
                        }
293 56
                    } elseif ((array_key_exists($array_key, self::$DB_OPTIONS)
294 56
                        || array_key_exists($array_key, self::$TABLE_OPTIONS))
295
                        && ! self::checkIfColumnDefinitionKeyword($array_key)
296
                    ) {
297
                        // This alter operation has finished, which means a comma
298 8
                        // was missing before start of new alter operation
299 8
                        $parser->error(
300 4
                            'Missing comma before start of a new alter operation.',
301
                            $token
302 8
                        );
303
                        break;
304
                    }
305 56
                }
306
                $ret->unknown[] = $token;
307
            }
308
        }
309 80
310 4
        if ($ret->options->isEmpty()) {
311 4
            $parser->error(
312 4
                'Unrecognized alter operation.',
313
                $list->tokens[$list->idx]
314
            );
315
        }
316 80
317
        --$list->idx;
318 80
319
        return $ret;
320
    }
321
322
    /**
323
     * @param AlterOperation $component the component to be built
324
     * @param array          $options   parameters for building
325
     *
326
     * @return string
327 4
     */
328
    public static function build($component, array $options = [])
329 4
    {
330 4
        $ret = $component->options . ' ';
331 4
        if (isset($component->field) && ($component->field !== '')) {
332
            $ret .= $component->field . ' ';
333
        }
334 4
335
        $ret .= TokensList::build($component->unknown);
336 4
337
        return $ret;
338
    }
339
340
    /**
341
     * Check if token's value is one of the common keywords
342
     * between column and table alteration
343
     *
344
     * @param string $tokenValue Value of current token
345
     *
346
     * @return bool
347 44
     */
348
    private static function checkIfColumnDefinitionKeyword($tokenValue)
349
    {
350 44
        $common_options = [
351
            'AUTO_INCREMENT',
352
            'COMMENT',
353
            'DEFAULT',
354
            'CHARACTER SET',
355
            'COLLATE',
356
            'PRIMARY',
357
            'UNIQUE',
358
            'PRIMARY KEY',
359
            'UNIQUE KEY',
360
        ];
361
362
        // Since these options can be used for
363 44
        // both table as well as a specific column in the table
364
        return in_array($tokenValue, $common_options);
365
    }
366
367
    /**
368
     * Check if token is symbol and quoted with backtick
369
     *
370
     * @param Token $token token to check
371
     *
372
     * @return bool
373
     */
374
    private static function checkIfTokenQuotedSymbol($token)
375
    {
376
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
377
    }
378
}
379