Passed
Pull Request — master (#324)
by William
10:10
created

AlterOperation::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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