Passed
Pull Request — master (#200)
by
unknown
06:48
created

CreateDefinition::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 17
ccs 10
cts 10
cp 1
rs 9.7
c 0
b 0
f 0
cc 3
nc 3
nop 5
crap 3
1
<?php
2
3
/**
4
 * Parses the create definition of a column or a key.
5
 *
6
 * Used for parsing `CREATE TABLE` statement.
7
 */
8
9
namespace PhpMyAdmin\SqlParser\Components;
10
11
use PhpMyAdmin\SqlParser\Component;
12
use PhpMyAdmin\SqlParser\Context;
13
use PhpMyAdmin\SqlParser\Parser;
14
use PhpMyAdmin\SqlParser\Token;
15
use PhpMyAdmin\SqlParser\TokensList;
16
17
/**
18
 * Parses the create definition of a column or a key.
19
 *
20
 * Used for parsing `CREATE TABLE` statement.
21
 *
22
 * @category   Components
23
 *
24
 * @license    https://www.gnu.org/licenses/gpl-2.0.txt GPL-2.0+
25
 */
26
class CreateDefinition extends Component
27
{
28
    /**
29
     * All field options.
30
     *
31
     * @var array
32
     */
33
    public static $FIELD_OPTIONS = array(
34
        // Tells the `OptionsArray` to not sort the options.
35
        // See the note below.
36
        '_UNSORTED' => true,
37
38
        'NOT NULL' => 1,
39
        'NULL' => 1,
40
        'DEFAULT' => array(2, 'expr', array('breakOnAlias' => true)),
41
        /* Following are not according to grammar, but MySQL happily accepts
42
         * these at any location */
43
        'CHARSET' => array(2, 'var'),
44
        'COLLATE' => array(3, 'var'),
45
        'AUTO_INCREMENT' => 3,
46
        'PRIMARY' => 4,
47
        'PRIMARY KEY' => 4,
48
        'UNIQUE' => 4,
49
        'UNIQUE KEY' => 4,
50
        'COMMENT' => array(5, 'var'),
51
        'COLUMN_FORMAT' => array(6, 'var'),
52
        'ON UPDATE' => array(7, 'expr'),
53
54
        // Generated columns options.
55
        'GENERATED ALWAYS' => 8,
56
        'AS' => array(9, 'expr', array('parenthesesDelimited' => true)),
57
        'VIRTUAL' => 10,
58
        'PERSISTENT' => 11,
59
        'STORED' => 11,
60
        // Common entries.
61
        //
62
        // NOTE: Some of the common options are not in the same order which
63
        // causes troubles when checking if the options are in the right order.
64
        // I should find a way to define multiple sets of options and make the
65
        // parser select the right set.
66
        //
67
        // 'UNIQUE'                        => 4,
68
        // 'UNIQUE KEY'                    => 4,
69
        // 'COMMENT'                       => array(5, 'var'),
70
        // 'NOT NULL'                      => 1,
71
        // 'NULL'                          => 1,
72
        // 'PRIMARY'                       => 4,
73
        // 'PRIMARY KEY'                   => 4,
74
    );
75
76
    /**
77
     * The name of the new column.
78
     *
79
     * @var string
80
     */
81
    public $name;
82
83
    /**
84
     * Whether this field is a constraint or not.
85
     *
86
     * @var bool
87
     */
88
    public $isConstraint;
89
90
    /**
91
     * The data type of thew new column.
92
     *
93
     * @var DataType
94
     */
95
    public $type;
96
97
    /**
98
     * The key.
99
     *
100
     * @var Key
101
     */
102
    public $key;
103
104
    /**
105
     * Check constraint
106
     *
107
     * @var check
108
     */
109
    public $check;
110
111
    /**
112
     * The table that is referenced.
113
     *
114
     * @var Reference
115
     */
116
    public $references;
117
118
    /**
119
     * The options of this field.
120
     *
121
     * @var OptionsArray
122
     */
123
    public $options;
124
125
    /**
126
     * Constructor.
127
     *
128
     * @param string       $name         the name of the field
0 ignored issues
show
Documentation introduced by
Should the type for parameter $name not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
129
     * @param OptionsArray $options      the options of this field
0 ignored issues
show
Documentation introduced by
Should the type for parameter $options not be OptionsArray|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
130
     * @param DataType|Key $type         the data type of this field or the key
0 ignored issues
show
Documentation introduced by
Should the type for parameter $type not be DataType|Key|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
131
     * @param bool         $isConstraint whether this field is a constraint or not
132
     * @param Reference    $references   references
0 ignored issues
show
Documentation introduced by
Should the type for parameter $references not be Reference|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
133
     */
134 33
    public function __construct(
135
        $name = null,
136
        $options = null,
137
        $type = null,
138
        $isConstraint = false,
139
        $references = null
140
    ) {
141 33
        $this->name = $name;
142 33
        $this->options = $options;
143 33
        if ($type instanceof DataType) {
144 1
            $this->type = $type;
145 33
        } elseif ($type instanceof Key) {
146 1
            $this->key = $type;
147 1
            $this->isConstraint = $isConstraint;
148 1
            $this->references = $references;
149
        }
150 33
    }
151
152
    /**
153
     * @param Parser     $parser  the parser that serves as context
154
     * @param TokensList $list    the list of tokens that are being parsed
155
     * @param array      $options parameters for parsing
156
     *
157
     * @return CreateDefinition[]
158
     */
159 33
    public static function parse(Parser $parser, TokensList $list, array $options = array())
160
    {
161 33
        $ret = array();
162
163 33
        $expr = new self();
164
165
        /**
166
         * The state of the parser.
167
         *
168
         * Below are the states of the parser.
169
         *
170
         *      0 -----------------------[ ( ]------------------------> 1
171
         *
172
         *      1 --------------------[ CONSTRAINT ]------------------> 1
173
         *      1 -----------------------[ key ]----------------------> 2
174
         *      1 -------------[ constraint / column name ]-----------> 2
175
         *
176
         *      2 --------------------[ data type ]-------------------> 3
177
         *
178
         *      3 ---------------------[ options ]--------------------> 4
179
         *
180
         *      4 --------------------[ REFERENCES ]------------------> 4
181
         *
182
         *      5 ------------------------[ , ]-----------------------> 1
183
         *      5 ------------------------[ ) ]-----------------------> 6 (-1)
184
         *
185
         * @var int
186
         */
187 33
        $state = 0;
188
189 33
        for (; $list->idx < $list->count; ++$list->idx) {
190
            /**
191
             * Token parsed at this moment.
192
             *
193
             * @var Token
194
             */
195 33
            $token = $list->tokens[$list->idx];
196
197
            // End of statement.
198 33
            if ($token->type === Token::TYPE_DELIMITER) {
199 2
                break;
200
            }
201
202
            // Skipping whitespaces and comments.
203 32
            if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
204 29
                continue;
205
            }
206
207 32
            if ($state === 0) {
208 32
                if (($token->type === Token::TYPE_OPERATOR) && ($token->value === '(')) {
209 31
                    $state = 1;
210
                } else {
211 1
                    $parser->error(
212 1
                        'An opening bracket was expected.',
213 1
                        $token
214
                    );
215
216 1
                    break;
217
                }
218 31
            } elseif ($state === 1) {
219 31
                if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'CONSTRAINT') {
220 6
                    $expr->isConstraint = true;
221 31
                } elseif (($token->type === Token::TYPE_KEYWORD) && ($token->flags & Token::FLAG_KEYWORD_KEY)) {
222 12
                    $expr->key = Key::parse($parser, $list);
223 12
                    $state = 4;
224 31
                } elseif (($token->type === Token::TYPE_KEYWORD) && $token->keyword === 'CHECK') {
225
                    $expr->check = Check::parse($parser, $list);
226
                    $state = 4;
227 31 View Code Duplication
                } elseif ($token->type === Token::TYPE_SYMBOL || $token->type === Token::TYPE_NONE) {
228 28
                    $expr->name = $token->value;
229 28
                    if (!$expr->isConstraint) {
230 28
                        $state = 2;
231
                    }
232 3
                } elseif ($token->type === Token::TYPE_KEYWORD) {
233 2
                    if ($token->flags & Token::FLAG_KEYWORD_RESERVED) {
234
                        // Reserved keywords can't be used
235
                        // as field names without backquotes
236 1
                        $parser->error(
237
                            'A symbol name was expected! '
238
                            . 'A reserved keyword can not be used '
239 1
                            . 'as a column name without backquotes.',
240 1
                            $token
241
                        );
242
243 1
                        return $ret;
244
                    }
245
246
                    // Non-reserved keywords are allowed without backquotes
247 1
                    $expr->name = $token->value;
248 1
                    $state = 2;
249
                } else {
250 1
                    $parser->error(
251 1
                        'A symbol name was expected!',
252 1
                        $token
253
                    );
254
255 1
                    return $ret;
256
                }
257 29
            } elseif ($state === 2) {
258 29
                $expr->type = DataType::parse($parser, $list);
259 29
                $state = 3;
260 29
            } elseif ($state === 3) {
261 29
                if (($token->type === Token::TYPE_KEYWORD) && $token->keyword === 'CHECK') {
262
                    $expr->check = Check::parse($parser, $list);
263
                    $state = 4;
264
                } else {
265 29
                    $expr->options = OptionsArray::parse($parser, $list, static::$FIELD_OPTIONS);
266 29
                    $state = 4;
267
                }
268 29
            } elseif ($state === 4) {
269 29
                if ($token->type === Token::TYPE_KEYWORD && $token->keyword === 'REFERENCES') {
270 5
                    ++$list->idx; // Skipping keyword 'REFERENCES'.
271 5
                    $expr->references = Reference::parse($parser, $list);
272
                } else {
273 29
                    --$list->idx;
274
                }
275 29
                $state = 5;
276 29
            } elseif ($state === 5) {
277 29 View Code Duplication
                if ((!empty($expr->type)) || (!empty($expr->key)) || (!empty($expr->check))) {
278 29
                    $ret[] = $expr;
279
                }
280 29
                $expr = new self();
281 29
                if ($token->value === ',') {
282 20
                    $state = 1;
283 28
                } elseif ($token->value === ')') {
284 27
                    $state = 6;
285 27
                    ++$list->idx;
286 27
                    break;
287
                } else {
288 1
                    $parser->error(
289 1
                        'A comma or a closing bracket was expected.',
290 1
                        $token
291
                    );
292 1
                    $state = 0;
293 1
                    break;
294
                }
295
            }
296
        }
297
298
        // Last iteration was not saved.
299 31 View Code Duplication
        if ((!empty($expr->type)) || (!empty($expr->key)) || (!empty($expr->check))) {
300 1
            $ret[] = $expr;
301
        }
302
303 31 View Code Duplication
        if (($state !== 0) && ($state !== 6)) {
304 1
            $parser->error(
305 1
                'A closing bracket was expected.',
306 1
                $list->tokens[$list->idx - 1]
307
            );
308
        }
309
310 31
        --$list->idx;
311
312 31
        return $ret;
313
    }
314
315
    /**
316
     * @param CreateDefinition|CreateDefinition[] $component the component to be built
317
     * @param array                               $options   parameters for building
318
     *
319
     * @return string
320
     */
321 9
    public static function build($component, array $options = array())
322
    {
323 9
        if (is_array($component)) {
324 7
            return "(\n  " . implode(",\n  ", $component) . "\n)";
325
        }
326
327 9
        $tmp = '';
328
329 9
        if ($component->isConstraint) {
330 1
            $tmp .= 'CONSTRAINT ';
331
        }
332
333 9
        if ((isset($component->name)) && ($component->name !== '')) {
334 8
            $tmp .= Context::escape($component->name) . ' ';
335
        }
336
337 9
        if (!empty($component->type)) {
338 7
            $tmp .= DataType::build(
339 7
                $component->type,
340 7
                array('lowercase' => true)
341 7
            ) . ' ';
342
        }
343
344 9
        if (!empty($component->key)) {
345 3
            $tmp .= $component->key . ' ';
346
        }
347
348 9
        if (!empty($component->references)) {
349 1
            $tmp .= 'REFERENCES ' . $component->references . ' ';
350
        }
351
352 9
        $tmp .= $component->options;
353
354 9
        return trim($tmp);
355
    }
356
}
357