LoadStatement::parseFileOptions()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 2
nop 3
dl 0
loc 12
ccs 6
cts 6
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpMyAdmin\SqlParser\Statements;
6
7
use PhpMyAdmin\SqlParser\Components\ArrayObj;
8
use PhpMyAdmin\SqlParser\Components\Expression;
9
use PhpMyAdmin\SqlParser\Components\OptionsArray;
10
use PhpMyAdmin\SqlParser\Components\SetOperation;
11
use PhpMyAdmin\SqlParser\Parser;
12
use PhpMyAdmin\SqlParser\Parsers\ArrayObjs;
13
use PhpMyAdmin\SqlParser\Parsers\ExpressionArray;
14
use PhpMyAdmin\SqlParser\Parsers\Expressions;
15
use PhpMyAdmin\SqlParser\Parsers\OptionsArrays;
16
use PhpMyAdmin\SqlParser\Parsers\SetOperations;
17
use PhpMyAdmin\SqlParser\Statement;
18
use PhpMyAdmin\SqlParser\TokensList;
19
use PhpMyAdmin\SqlParser\TokenType;
20
21
use function strlen;
22
use function trim;
23
24
/**
25
 * `LOAD` statement.
26
 *
27
 * LOAD DATA [LOW_PRIORITY | CONCURRENT] [LOCAL] INFILE 'file_name'
28
 *   [REPLACE | IGNORE]
29
 *   INTO TABLE tbl_name
30
 *   [PARTITION (partition_name,...)]
31
 *   [CHARACTER SET charset_name]
32
 *   [{FIELDS | COLUMNS}
33
 *       [TERMINATED BY 'string']
34
 *       [[OPTIONALLY] ENCLOSED BY 'char']
35
 *       [ESCAPED BY 'char']
36
 *   ]
37
 *   [LINES
38
 *       [STARTING BY 'string']
39
 *       [TERMINATED BY 'string']
40
 *  ]
41
 *   [IGNORE number {LINES | ROWS}]
42
 *   [(col_name_or_user_var,...)]
43
 *   [SET col_name = expr,...]
44
 */
45
class LoadStatement extends Statement
46
{
47
    /**
48
     * Options for `LOAD` statements and their slot ID.
49
     *
50
     * @var array<string, int|array<int, int|string>>
51
     * @psalm-var array<string, (positive-int|array{positive-int, ('var'|'var='|'expr'|'expr=')})>
52
     */
53
    public static array $statementOptions = [
54
        'LOW_PRIORITY' => 1,
55
        'CONCURRENT' => 1,
56
        'LOCAL' => 2,
57
    ];
58
59
    /**
60
     * FIELDS/COLUMNS Options for `LOAD DATA...INFILE` statements.
61
     */
62
    private const STATEMENT_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
    private const STATEMENT_LINES_OPTIONS = [
82
        'STARTING BY' => [
83
            1,
84
            'expr',
85
        ],
86
        'TERMINATED BY' => [
87
            2,
88
            'expr',
89
        ],
90
    ];
91
92
    /**
93
     * File name being used to load data.
94
     */
95
    public Expression|null $fileName = null;
96
97
    /**
98
     * Table used as destination for this statement.
99
     */
100
    public Expression|null $table = null;
101
102
    /**
103
     * Partitions used as source for this statement.
104
     */
105
    public ArrayObj|null $partition = null;
106
107
    /**
108
     * Character set used in this statement.
109
     */
110
    public Expression|null $charsetName = null;
111
112
    /**
113
     * Options for FIELDS/COLUMNS keyword.
114
     *
115
     * @see LoadStatement::STATEMENT_FIELDS_OPTIONS
116
     */
117
    public OptionsArray|null $fieldsOptions = null;
118
119
    /**
120
     * Whether to use `FIELDS` or `COLUMNS` while building.
121
     */
122
    public string|null $fieldsKeyword = null;
123
124
    /**
125
     * Options for OPTIONS keyword.
126
     *
127
     * @see LoadStatement::STATEMENT_LINES_OPTIONS
128
     */
129
    public OptionsArray|null $linesOptions = null;
130
131
    /**
132
     * Column names or user variables.
133
     *
134
     * @var Expression[]|null
135
     */
136
    public array|null $columnNamesOrUserVariables = null;
137
138
    /**
139
     * SET clause's updated values(optional).
140
     *
141
     * @var SetOperation[]|null
142
     */
143
    public array|null $set = null;
144
145
    /**
146
     * Ignore 'number' LINES/ROWS.
147
     */
148
    public Expression|null $ignoreNumber = null;
149
150
    /**
151
     * REPLACE/IGNORE Keyword.
152
     */
153
    public string|null $replaceIgnore = null;
154
155
    /**
156
     * LINES/ROWS Keyword.
157
     */
158
    public string|null $linesRows = null;
159
160 2
    public function build(): string
161
    {
162 2
        $ret = 'LOAD DATA ' . $this->options
163 2
            . ' INFILE ' . $this->fileName;
164
165 2
        if ($this->replaceIgnore !== null) {
166 2
            $ret .= ' ' . trim($this->replaceIgnore);
167
        }
168
169 2
        $ret .= ' INTO TABLE ' . $this->table;
170
171 2
        if ($this->partition !== null && strlen((string) $this->partition) > 0) {
172 2
            $ret .= ' PARTITION ' . $this->partition->build();
173
        }
174
175 2
        if ($this->charsetName !== null) {
176 2
            $ret .= ' CHARACTER SET ' . $this->charsetName;
177
        }
178
179 2
        if ($this->fieldsKeyword !== null) {
180 2
            $ret .= ' ' . $this->fieldsKeyword . ' ' . $this->fieldsOptions;
181
        }
182
183 2
        if ($this->linesOptions !== null && strlen((string) $this->linesOptions) > 0) {
184 2
            $ret .= ' LINES ' . $this->linesOptions;
185
        }
186
187 2
        if ($this->ignoreNumber !== null) {
188 2
            $ret .= ' IGNORE ' . $this->ignoreNumber . ' ' . $this->linesRows;
189
        }
190
191 2
        if ($this->columnNamesOrUserVariables !== null && $this->columnNamesOrUserVariables !== []) {
192 2
            $ret .= ' ' . Expressions::buildAll($this->columnNamesOrUserVariables);
193
        }
194
195 2
        if ($this->set !== null && $this->set !== []) {
196 2
            $ret .= ' SET ' . SetOperations::buildAll($this->set);
197
        }
198
199 2
        return $ret;
200
    }
201
202
    /**
203
     * @param Parser     $parser the instance that requests parsing
204
     * @param TokensList $list   the list of tokens to be parsed
205
     */
206 30
    public function parse(Parser $parser, TokensList $list): void
207
    {
208 30
        ++$list->idx; // Skipping `LOAD DATA`.
209
210
        // parse any options if provided
211 30
        $this->options = OptionsArrays::parse($parser, $list, static::$statementOptions);
212 30
        ++$list->idx;
213
214
        /**
215
         * The state of the parser.
216
         */
217 30
        $state = 0;
218
219 30
        for (; $list->idx < $list->count; ++$list->idx) {
220
            /**
221
             * Token parsed at this moment.
222
             */
223 30
            $token = $list->tokens[$list->idx];
224
225
            // End of statement.
226 30
            if ($token->type === TokenType::Delimiter) {
227 18
                break;
228
            }
229
230
            // Skipping whitespaces and comments.
231 30
            if (($token->type === TokenType::Whitespace) || ($token->type === TokenType::Comment)) {
232 26
                continue;
233
            }
234
235 30
            if ($state === 0) {
236 30
                if ($token->type === TokenType::Keyword && $token->keyword !== 'INFILE') {
237 2
                    $parser->error('Unexpected keyword.', $token);
238 2
                    break;
239
                }
240
241 28
                if ($token->type !== TokenType::Keyword) {
242 2
                    $parser->error('Unexpected token.', $token);
243 2
                    break;
244
                }
245
246 26
                ++$list->idx;
247 26
                $this->fileName = Expressions::parse(
248 26
                    $parser,
249 26
                    $list,
250 26
                    ['parseField' => 'file'],
251 26
                );
252 26
                $state = 1;
253 26
            } elseif ($state === 1) {
254 26
                if ($token->type === TokenType::Keyword) {
255 26
                    if ($token->keyword === 'REPLACE' || $token->keyword === 'IGNORE') {
256 16
                        $this->replaceIgnore = trim($token->keyword);
257 26
                    } elseif ($token->keyword === 'INTO') {
258 26
                        $state = 2;
259
                    }
260
                }
261 26
            } elseif ($state === 2) {
262 26
                if ($token->type !== TokenType::Keyword || $token->keyword !== 'TABLE') {
263 2
                    $parser->error('Unexpected token.', $token);
264 2
                    break;
265
                }
266
267 24
                ++$list->idx;
268 24
                $this->table = Expressions::parse($parser, $list, ['parseField' => 'table', 'breakOnAlias' => true]);
269 24
                $state = 3;
270 18
            } elseif ($state >= 3 && $state <= 7) {
271 18
                if ($token->type === TokenType::Keyword) {
272 16
                    $newState = $this->parseKeywordsAccordingToState($parser, $list, $state);
273 16
                    if ($newState === $state) {
274
                        // Avoid infinite loop
275 4
                        break;
276
                    }
277 8
                } elseif ($token->type === TokenType::Operator && $token->token === '(') {
278 6
                    $this->columnNamesOrUserVariables
279 6
                        = ExpressionArray::parse($parser, $list);
280 6
                    $state = 7;
281
                } else {
282 2
                    $parser->error('Unexpected token.', $token);
283 2
                    break;
284
                }
285
            }
286
        }
287
288 30
        --$list->idx;
289
    }
290
291
    /**
292
     * @param Parser     $parser  The parser
293
     * @param TokensList $list    A token list
294
     * @param string     $keyword The keyword
295
     */
296 12
    public function parseFileOptions(Parser $parser, TokensList $list, string $keyword = 'FIELDS'): void
297
    {
298 12
        ++$list->idx;
299
300 12
        if ($keyword === 'FIELDS' || $keyword === 'COLUMNS') {
301
            // parse field options
302 12
            $this->fieldsOptions = OptionsArrays::parse($parser, $list, self::STATEMENT_FIELDS_OPTIONS);
303
304 12
            $this->fieldsKeyword = $keyword;
305
        } else {
306
            // parse line options
307 6
            $this->linesOptions = OptionsArrays::parse($parser, $list, self::STATEMENT_LINES_OPTIONS);
308
        }
309
    }
310
311 16
    public function parseKeywordsAccordingToState(Parser $parser, TokensList $list, int $state): int
312
    {
313 16
        $token = $list->tokens[$list->idx];
314
315
        switch ($state) {
316 16
            case 3:
317 16
                if ($token->keyword === 'PARTITION') {
318 4
                    ++$list->idx;
319 4
                    $this->partition = ArrayObjs::parse($parser, $list);
320
321 4
                    return 4;
322
                }
323
324
                // no break
325 6
            case 4:
326 16
                if ($token->keyword === 'CHARACTER SET') {
327 6
                    ++$list->idx;
328 6
                    $this->charsetName = Expressions::parse($parser, $list);
329
330 6
                    return 5;
331
                }
332
333
                // no break
334 6
            case 5:
335 16
                if ($token->keyword === 'FIELDS' || $token->keyword === 'COLUMNS' || $token->keyword === 'LINES') {
336 12
                    $this->parseFileOptions($parser, $list, $token->value);
337
338 12
                    return 6;
339
                }
340
341
                // no break
342 6
            case 6:
343 14
                if ($token->keyword === 'IGNORE') {
344 10
                    ++$list->idx;
345
346 10
                    $this->ignoreNumber = Expressions::parse($parser, $list);
347 10
                    $nextToken = $list->getNextOfType(TokenType::Keyword);
348
349
                    if (
350 10
                        $nextToken->type === TokenType::Keyword
351 10
                        && (($nextToken->keyword === 'LINES')
352 10
                        || ($nextToken->keyword === 'ROWS'))
353
                    ) {
354 10
                        $this->linesRows = $nextToken->token;
355
                    }
356
357 10
                    return 7;
358
                }
359
360
                // no break
361 6
            case 7:
362 10
                if ($token->keyword === 'SET') {
363 6
                    ++$list->idx;
364 6
                    $this->set = SetOperations::parse($parser, $list);
365
366 6
                    return 8;
367
                }
368
369
                // no break
370
            default:
371
        }
372
373 4
        return $state;
374
    }
375
}
376