Passed
Push — master ( 290431...b210e2 )
by Isaac
03:57
created

LoadStatement   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 389
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 156
dl 0
loc 389
ccs 129
cts 129
cp 1
rs 6
c 0
b 0
f 0
wmc 55

4 Methods

Rating   Name   Duplication   Size   Complexity  
D parse() 0 100 23
C build() 0 40 13
A parseFileOptions() 0 19 3
C parseKeywordsAccordingToState() 0 65 16

How to fix   Complexity   

Complex Class

Complex classes like LoadStatement often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use LoadStatement, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * `LOAD` statement.
4
 */
5
6
declare(strict_types=1);
7
8
namespace PhpMyAdmin\SqlParser\Statements;
9
10
use PhpMyAdmin\SqlParser\Components\ArrayObj;
11
use PhpMyAdmin\SqlParser\Components\Expression;
12
use PhpMyAdmin\SqlParser\Components\ExpressionArray;
13
use PhpMyAdmin\SqlParser\Components\OptionsArray;
14
use PhpMyAdmin\SqlParser\Components\SetOperation;
15
use PhpMyAdmin\SqlParser\Parser;
16
use PhpMyAdmin\SqlParser\Statement;
17
use PhpMyAdmin\SqlParser\Token;
18
use PhpMyAdmin\SqlParser\TokensList;
19
use function count;
20
use function strlen;
21
use function trim;
22
23
/**
24
 * `LOAD` statement.
25
 *
26
 * LOAD DATA [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name'
27
 *   [REPLACE | IGNORE]
28
 *   INTO TABLE tbl_name
29
 *   [PARTITION (partition_name,...)]
30
 *   [CHARACTER SET charset_name]
31
 *   [{FIELDS | COLUMNS}
32
 *       [TERMINATED BY 'string']
33
 *       [[OPTIONALLY] ENCLOSED BY 'char']
34
 *       [ESCAPED BY 'char']
35
 *   ]
36
 *   [LINES
37
 *       [STARTING BY 'string']
38
 *       [TERMINATED BY 'string']
39
 *  ]
40
 *   [IGNORE number {LINES | ROWS}]
41
 *   [(col_name_or_user_var,...)]
42
 *   [SET col_name = expr,...]
43
 */
44
class LoadStatement extends Statement
45
{
46
    /**
47
     * Options for `LOAD` statements and their slot ID.
48
     *
49
     * @var array
50
     */
51
    public static $OPTIONS = [
52
        'LOW_PRIORITY' => 1,
53
        'CONCURRENT' => 1,
54
        'LOCAL' => 2,
55
    ];
56
57
    /**
58
     * FIELDS/COLUMNS Options for `LOAD DATA...INFILE` statements.
59
     *
60
     * @var array
61
     */
62
    public static $FIELDS_OPTIONS = [
63
        'TERMINATED BY' => [
64
            1,
65
            'expr',
66
        ],
67
        'OPTIONALLY' => 2,
68
        'ENCLOSED BY' => [
69
            3,
70
            'expr',
71
        ],
72
        'ESCAPED BY' => [
73
            4,
74
            'expr',
75
        ],
76
    ];
77
78
    /**
79
     * LINES Options for `LOAD DATA...INFILE` statements.
80
     *
81
     * @var array
82
     */
83
    public static $LINES_OPTIONS = [
84
        'STARTING BY' => [
85
            1,
86
            'expr',
87
        ],
88
        'TERMINATED BY' => [
89
            2,
90
            'expr',
91
        ],
92
    ];
93
94
    /**
95
     * File name being used to load data.
96
     *
97
     * @var Expression
98
     */
99
    public $file_name;
100
101
    /**
102
     * Table used as destination for this statement.
103
     *
104
     * @var Expression
105
     */
106
    public $table;
107
108
    /**
109
     * Partitions used as source for this statement.
110
     *
111
     * @var ArrayObj
112
     */
113
    public $partition;
114
115
    /**
116
     * Character set used in this statement.
117
     *
118
     * @var Expression
119
     */
120
    public $charset_name;
121
122
    /**
123
     * Options for FIELDS/COLUMNS keyword.
124
     *
125
     * @see static::$FIELDS_OPTIONS
126
     *
127
     * @var OptionsArray
128
     */
129
    public $fields_options;
130
131
    /**
132
     * Whether to use `FIELDS` or `COLUMNS` while building.
133
     *
134
     * @var string
135
     */
136
    public $fields_keyword;
137
138
    /**
139
     * Options for OPTIONS keyword.
140
     *
141
     * @see static::$LINES_OPTIONS
142
     *
143
     * @var OptionsArray
144
     */
145
    public $lines_options;
146
147
    /**
148
     * Column names or user variables.
149
     *
150
     * @var Expression[]
151
     */
152
    public $col_name_or_user_var;
153
154
    /**
155
     * SET clause's updated values(optional).
156
     *
157
     * @var SetOperation[]
158
     */
159
    public $set;
160
161
    /**
162
     * Ignore 'number' LINES/ROWS.
163
     *
164
     * @var Expression
165
     */
166
    public $ignore_number;
167
168
    /**
169
     * REPLACE/IGNORE Keyword.
170
     *
171
     * @var string
172
     */
173
    public $replace_ignore;
174
175
    /**
176
     * LINES/ROWS Keyword.
177
     *
178
     * @var string
179
     */
180
    public $lines_rows;
181
182
    /**
183
     * @return string
184
     */
185 4
    public function build()
186
    {
187 4
        $ret = 'LOAD DATA ' . $this->options
188 4
            . ' INFILE ' . $this->file_name;
189
190 4
        if ($this->replace_ignore !== null) {
191 4
            $ret .= ' ' . trim($this->replace_ignore);
192
        }
193
194 4
        $ret .= ' INTO TABLE ' . $this->table;
195
196 4
        if ($this->partition !== null && strlen((string) $this->partition) > 0) {
197 4
            $ret .= ' PARTITION ' . ArrayObj::build($this->partition);
198
        }
199
200 4
        if ($this->charset_name !== null) {
201 4
            $ret .= ' CHARACTER SET ' . $this->charset_name;
202
        }
203
204 4
        if ($this->fields_keyword !== null) {
205 4
            $ret .= ' ' . $this->fields_keyword . ' ' . $this->fields_options;
206
        }
207
208 4
        if ($this->lines_options !== null && strlen((string) $this->lines_options) > 0) {
209 4
            $ret .= ' LINES ' . $this->lines_options;
210
        }
211
212 4
        if ($this->ignore_number !== null) {
213 4
            $ret .= ' IGNORE ' . $this->ignore_number . ' ' . $this->lines_rows;
214
        }
215
216 4
        if ($this->col_name_or_user_var !== null && count($this->col_name_or_user_var) > 0) {
217 4
            $ret .= ' ' . ExpressionArray::build($this->col_name_or_user_var);
218
        }
219
220 4
        if ($this->set !== null && count($this->set) > 0) {
221 4
            $ret .= ' SET ' . SetOperation::build($this->set);
222
        }
223
224 4
        return $ret;
225
    }
226
227
    /**
228
     * @param Parser     $parser the instance that requests parsing
229
     * @param TokensList $list   the list of tokens to be parsed
230
     */
231 60
    public function parse(Parser $parser, TokensList $list)
232
    {
233 60
        ++$list->idx; // Skipping `LOAD DATA`.
234
235
        // parse any options if provided
236 60
        $this->options = OptionsArray::parse(
237 60
            $parser,
238 60
            $list,
239 60
            static::$OPTIONS
240
        );
241 60
        ++$list->idx;
242
243
        /**
244
         * The state of the parser.
245
         *
246
         * @var int
247
         */
248 60
        $state = 0;
249
250 60
        for (; $list->idx < $list->count; ++$list->idx) {
251
            /**
252
             * Token parsed at this moment.
253
             *
254
             * @var Token
255
             */
256 60
            $token = $list->tokens[$list->idx];
257
258
            // End of statement.
259 60
            if ($token->type === Token::TYPE_DELIMITER) {
260 36
                break;
261
            }
262
263
            // Skipping whitespaces and comments.
264 60
            if (($token->type === Token::TYPE_WHITESPACE) || ($token->type === Token::TYPE_COMMENT)) {
265 52
                continue;
266
            }
267
268 60
            if ($state === 0) {
269 60
                if ($token->type === Token::TYPE_KEYWORD
270 60
                    && $token->keyword !== 'INFILE'
271
                ) {
272 4
                    $parser->error('Unexpected keyword.', $token);
273 4
                    break;
274 56
                } elseif ($token->type !== Token::TYPE_KEYWORD) {
275 4
                    $parser->error('Unexpected token.', $token);
276 4
                    break;
277
                }
278
279 52
                ++$list->idx;
280 52
                $this->file_name = Expression::parse(
281 52
                    $parser,
282 52
                    $list,
283 52
                    ['parseField' => 'file']
284
                );
285 52
                $state = 1;
286 52
            } elseif ($state === 1) {
287 52
                if ($token->type === Token::TYPE_KEYWORD) {
288 52
                    if ($token->keyword === 'REPLACE'
289 52
                     || $token->keyword === 'IGNORE') {
290 32
                        $this->replace_ignore = trim($token->keyword);
291 52
                    } elseif ($token->keyword === 'INTO') {
292 52
                        $state = 2;
293
                    }
294
                }
295 52
            } elseif ($state === 2) {
296 52
                if ($token->type === Token::TYPE_KEYWORD
297 52
                    && $token->keyword === 'TABLE'
298
                ) {
299 48
                    ++$list->idx;
300 48
                    $this->table = Expression::parse($parser, $list, ['parseField' => 'table']);
301 48
                    $state = 3;
302
                } else {
303 4
                    $parser->error('Unexpected token.', $token);
304 52
                    break;
305
                }
306 36
            } elseif ($state >= 3 && $state <= 7) {
307 36
                if ($token->type === Token::TYPE_KEYWORD) {
308 32
                    $newState = $this->parseKeywordsAccordingToState(
309 32
                        $parser,
310 8
                        $list,
311 8
                        $state
312
                    );
313 32
                    if ($newState === $state) {
314
                        // Avoid infinite loop
315 32
                        break;
316
                    }
317 16
                } elseif ($token->type === Token::TYPE_OPERATOR
318 16
                    && $token->token === '('
319
                ) {
320 12
                    $this->col_name_or_user_var
321 12
                        = ExpressionArray::parse($parser, $list);
322 12
                    $state = 7;
323
                } else {
324 4
                    $parser->error('Unexpected token.', $token);
325 4
                    break;
326
                }
327
            }
328
        }
329
330 60
        --$list->idx;
331 60
    }
332
333
    /**
334
     * @param Parser     $parser  The parser
335
     * @param TokensList $list    A token list
336
     * @param string     $keyword The keyword
337
     */
338 24
    public function parseFileOptions(Parser $parser, TokensList $list, $keyword = 'FIELDS'): void
339
    {
340 24
        ++$list->idx;
341
342 24
        if ($keyword === 'FIELDS' || $keyword === 'COLUMNS') {
343
            // parse field options
344 24
            $this->fields_options = OptionsArray::parse(
345 24
                $parser,
346 24
                $list,
347 24
                static::$FIELDS_OPTIONS
348
            );
349
350 24
            $this->fields_keyword = $keyword;
351
        } else {
352
            // parse line options
353 12
            $this->lines_options = OptionsArray::parse(
354 12
                $parser,
355 12
                $list,
356 12
                static::$LINES_OPTIONS
357
            );
358
        }
359 24
    }
360
361
    /**
362
     * @param Parser     $parser
363
     * @param TokensList $list
364
     * @param int        $state
365
     *
366
     * @return int
367
     */
368 32
    public function parseKeywordsAccordingToState($parser, $list, $state)
369
    {
370 32
        $token = $list->tokens[$list->idx];
371
372 32
        switch ($state) {
373 32
            case 3:
374 32
                if ($token->keyword === 'PARTITION') {
375 8
                    ++$list->idx;
376 8
                    $this->partition = ArrayObj::parse($parser, $list);
377
378 8
                    return 4;
379
                }
380
381
                // no break
382 12
            case 4:
383 32
                if ($token->keyword === 'CHARACTER SET') {
384 12
                    ++$list->idx;
385 12
                    $this->charset_name = Expression::parse($parser, $list);
386
387 12
                    return 5;
388
                }
389
390
                // no break
391 12
            case 5:
392 32
                if ($token->keyword === 'FIELDS'
393 28
                    || $token->keyword === 'COLUMNS'
394 32
                    || $token->keyword === 'LINES'
395
                ) {
396 24
                    $this->parseFileOptions($parser, $list, $token->value);
397
398 24
                    return 6;
399
                }
400
401
                // no break
402 12
            case 6:
403 28
                if ($token->keyword === 'IGNORE') {
404 20
                    ++$list->idx;
405
406 20
                    $this->ignore_number = Expression::parse($parser, $list);
407 20
                    $nextToken = $list->getNextOfType(Token::TYPE_KEYWORD);
408
409 20
                    if ($nextToken->type === Token::TYPE_KEYWORD
410 20
                        && (($nextToken->keyword === 'LINES')
411 20
                        || ($nextToken->keyword === 'ROWS'))
412
                    ) {
413 20
                        $this->lines_rows = $nextToken->token;
414
                    }
415
416 20
                    return 7;
417
                }
418
419
                // no break
420 12
            case 7:
421 20
                if ($token->keyword === 'SET') {
422 12
                    ++$list->idx;
423 12
                    $this->set = SetOperation::parse($parser, $list);
424
425 12
                    return 8;
426
                }
427
428
                // no break
429
            default:
430
        }
431
432 8
        return $state;
433
    }
434
}
435