Passed
Push — master ( 3ad940...0fb1da )
by William
03:43 queued 12s
created

AlterOperation::build()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 2
nop 2
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 3
rs 10
c 0
b 0
f 0
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 84
    public function __construct(
163
        $options = null,
164
        $field = null,
165
        $unknown = []
166
    ) {
167 84
        $this->options = $options;
168 84
        $this->field = $field;
169 84
        $this->unknown = $unknown;
170 84
    }
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 84
    public static function parse(Parser $parser, TokensList $list, array $options = [])
180
    {
181 84
        $ret = new static();
182
183
        /**
184
         * Counts brackets.
185
         *
186
         * @var int
187
         */
188 84
        $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 84
        $state = 0;
204
205 84
        for (; $list->idx < $list->count; ++$list->idx) {
206
            /**
207
             * Token parsed at this moment.
208
             *
209
             * @var Token
210
             */
211 84
            $token = $list->tokens[$list->idx];
212
213
            // End of statement.
214 84
            if ($token->type === Token::TYPE_DELIMITER) {
215 64
                break;
216
            }
217
218
            // Skipping comments.
219 84
            if ($token->type === Token::TYPE_COMMENT) {
220 4
                continue;
221
            }
222
223
            // Skipping whitespaces.
224 84
            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 84
            if ($state === 0) {
235 84
                $ret->options = OptionsArray::parse($parser, $list, $options);
236
237 84
                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 80
                $state = 1;
250 68
            } elseif ($state === 1) {
251 68
                $ret->field = Expression::parse(
252 68
                    $parser,
253 68
                    $list,
254
                    [
255 68
                        'breakOnAlias' => true,
256
                        'parseField' => 'column',
257
                    ]
258
                );
259 68
                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 68
                $state = 2;
266 60
            } elseif ($state === 2) {
267 60
                $array_key = '';
268 60
                if (is_string($token->value) || is_numeric($token->value)) {
269 60
                    $array_key = $token->value;
270
                } else {
271 4
                    $array_key = $token->token;
272
                }
273 60
                if ($token->type === Token::TYPE_OPERATOR) {
274 48
                    if ($token->value === '(') {
275 44
                        ++$brackets;
276 48
                    } elseif ($token->value === ')') {
277 44
                        --$brackets;
278 20
                    } elseif (($token->value === ',') && ($brackets === 0)) {
279 48
                        break;
280
                    }
281 60
                } elseif (! self::checkIfTokenQuotedSymbol($token)) {
282 48
                    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
286 8
                        if (! ($token->value === 'SET' && $list->tokens[$list->idx - 1]->value === 'CHARACTER')) {
287 8
                            $parser->error(
288 8
                                'A new statement was found, but no delimiter between it and the previous one.',
289 2
                                $token
290
                            );
291 8
                            break;
292
                        }
293 24
                    } elseif ((array_key_exists($array_key, self::$DB_OPTIONS)
294 48
                        || array_key_exists($array_key, self::$TABLE_OPTIONS))
295 48
                        && ! self::checkIfColumnDefinitionKeyword($array_key)
296
                    ) {
297
                        // This alter operation has finished, which means a comma
298
                        // was missing before start of new alter operation
299 8
                        $parser->error(
300 8
                            'Missing comma before start of a new alter operation.',
301 2
                            $token
302
                        );
303 8
                        break;
304
                    }
305
                }
306 60
                $ret->unknown[] = $token;
307
            }
308
        }
309
310 84
        if ($ret->options->isEmpty()) {
311 4
            $parser->error(
312 4
                'Unrecognized alter operation.',
313 4
                $list->tokens[$list->idx]
314
            );
315
        }
316
317 84
        --$list->idx;
318
319 84
        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
     */
328 4
    public static function build($component, array $options = [])
329
    {
330 4
        $ret = $component->options . ' ';
331 4
        if (isset($component->field) && ($component->field !== '')) {
332 4
            $ret .= $component->field . ' ';
333
        }
334
335 4
        $ret .= TokensList::build($component->unknown);
336
337 4
        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
     */
348 44
    private static function checkIfColumnDefinitionKeyword($tokenValue)
349
    {
350 11
        $common_options = [
351 33
            '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
        // both table as well as a specific column in the table
364 44
        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 60
    private static function checkIfTokenQuotedSymbol($token)
375
    {
376 60
        return $token->type === Token::TYPE_SYMBOL && $token->flags === Token::FLAG_SYMBOL_BACKTICK;
377
    }
378
}
379