Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
91 / 91
100.00% covered (success)
100.00%
18 / 18
CRAP
100.00% covered (success)
100.00%
1 / 1
LoadData
100.00% covered (success)
100.00%
91 / 91
100.00% covered (success)
100.00%
18 / 18
40
100.00% covered (success)
100.00%
1 / 1
 renderOptions
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
5
 infile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderInfile
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 intoTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderIntoTable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 charset
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderCharset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 columnsTerminatedBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 columnsEnclosedBy
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 columnsEscapedBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderColumns
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
8
 linesStartingBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 linesTerminatedBy
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderLines
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 ignoreLines
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 renderIgnoreLines
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 sql
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 run
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework Database Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\Database\Manipulation;
11
12use InvalidArgumentException;
13use LogicException;
14
15/**
16 * Class LoadData.
17 *
18 * @see https://mariadb.com/kb/en/load-data-infile/
19 *
20 * @package database
21 */
22class LoadData extends Statement
23{
24    use Traits\Set;
25
26    /**
27     * @see https://mariadb.com/kb/en/high_priority-and-low_priority/
28     *
29     * @var string
30     */
31    public const OPT_LOW_PRIORITY = 'LOW_PRIORITY';
32    /**
33     * @see https://mariadb.com/kb/en/load-data-infile/#priority-and-concurrency
34     *
35     * @var string
36     */
37    public const OPT_CONCURRENT = 'CONCURRENT';
38    /**
39     * @see https://mariadb.com/kb/en/load-data-infile/#load-data-local-infile
40     *
41     * @var string
42     */
43    public const OPT_LOCAL = 'LOCAL';
44
45    protected function renderOptions() : ?string
46    {
47        if ( ! $this->hasOptions()) {
48            return null;
49        }
50        $options = $this->sql['options'];
51        foreach ($options as &$option) {
52            $input = $option;
53            $option = \strtoupper($option);
54            if ( ! \in_array($option, [
55                static::OPT_LOW_PRIORITY,
56                static::OPT_CONCURRENT,
57                static::OPT_LOCAL,
58            ], true)) {
59                throw new InvalidArgumentException("Invalid option: {$input}");
60            }
61        }
62        unset($option);
63        $intersection = \array_intersect(
64            $options,
65            [static::OPT_LOW_PRIORITY, static::OPT_CONCURRENT]
66        );
67        if (\count($intersection) > 1) {
68            throw new LogicException('Options LOW_PRIORITY and CONCURRENT can not be used together');
69        }
70        return \implode(' ', $options);
71    }
72
73    /**
74     * @param string $filename
75     *
76     * @return static
77     */
78    public function infile(string $filename) : static
79    {
80        $this->sql['infile'] = $filename;
81        return $this;
82    }
83
84    protected function renderInfile() : string
85    {
86        if (empty($this->sql['infile'])) {
87            throw new LogicException('INFILE statement is required');
88        }
89        $filename = $this->database->quote($this->sql['infile']);
90        return " INFILE {$filename}";
91    }
92
93    /**
94     * @param string $table
95     *
96     * @return static
97     */
98    public function intoTable(string $table) : static
99    {
100        $this->sql['table'] = $table;
101        return $this;
102    }
103
104    protected function renderIntoTable() : string
105    {
106        if (empty($this->sql['table'])) {
107            throw new LogicException('Table is required');
108        }
109        return ' INTO TABLE ' . $this->database->protectIdentifier($this->sql['table']);
110    }
111
112    /**
113     * @param string $charset
114     *
115     * @see https://mariadb.com/kb/en/supported-character-sets-and-collations/
116     *
117     * @return static
118     */
119    public function charset(string $charset) : static
120    {
121        $this->sql['charset'] = $charset;
122        return $this;
123    }
124
125    protected function renderCharset() : ?string
126    {
127        if ( ! isset($this->sql['charset'])) {
128            return null;
129        }
130        return " CHARACTER SET {$this->sql['charset']}";
131    }
132
133    /**
134     * @param string $str
135     *
136     * @return static
137     */
138    public function columnsTerminatedBy(string $str) : static
139    {
140        $this->sql['columns_terminated_by'] = $this->database->quote($str);
141        return $this;
142    }
143
144    /**
145     * @param string $char
146     * @param bool $optionally
147     *
148     * @return static
149     */
150    public function columnsEnclosedBy(string $char, bool $optionally = false) : static
151    {
152        $this->sql['columns_enclosed_by'] = $this->database->quote($char);
153        $this->sql['columns_enclosed_by_opt'] = $optionally;
154        return $this;
155    }
156
157    /**
158     * @param string $char
159     *
160     * @return static
161     */
162    public function columnsEscapedBy(string $char) : static
163    {
164        $this->sql['columns_escaped_by'] = $this->database->quote($char);
165        return $this;
166    }
167
168    protected function renderColumns() : ?string
169    {
170        if ( ! isset($this->sql['columns_terminated_by'])
171            && ! isset($this->sql['columns_enclosed_by'])
172            && ! isset($this->sql['columns_escaped_by'])) {
173            return null;
174        }
175        $part = ' COLUMNS' . \PHP_EOL;
176        if (isset($this->sql['columns_terminated_by'])) {
177            $part .= '  TERMINATED BY ' . $this->sql['columns_terminated_by'] . \PHP_EOL;
178        }
179        if (isset($this->sql['columns_enclosed_by'])) {
180            if (isset($this->sql['columns_enclosed_by_opt'])) {
181                $part .= '  OPTIONALLY';
182            }
183            $part .= ' ENCLOSED BY ' . $this->sql['columns_enclosed_by'] . \PHP_EOL;
184        }
185        if (isset($this->sql['columns_escaped_by'])) {
186            $part .= '  ESCAPED BY ' . $this->sql['columns_escaped_by'] . \PHP_EOL;
187        }
188        return $part;
189    }
190
191    /**
192     * @param string $str
193     *
194     * @return static
195     */
196    public function linesStartingBy(string $str) : static
197    {
198        $this->sql['lines_starting_by'] = $this->database->quote($str);
199        return $this;
200    }
201
202    /**
203     * @param string $str
204     *
205     * @return static
206     */
207    public function linesTerminatedBy(string $str) : static
208    {
209        $this->sql['lines_terminated_by'] = $this->database->quote($str);
210        return $this;
211    }
212
213    protected function renderLines() : ?string
214    {
215        if ( ! isset($this->sql['lines_starting_by'])
216            && ! isset($this->sql['lines_terminated_by'])) {
217            return null;
218        }
219        $part = ' LINES' . \PHP_EOL;
220        if (isset($this->sql['lines_starting_by'])) {
221            $part .= '  STARTING BY ' . $this->sql['lines_starting_by'] . \PHP_EOL;
222        }
223        if (isset($this->sql['lines_terminated_by'])) {
224            $part .= '  TERMINATED BY ' . $this->sql['lines_terminated_by'] . \PHP_EOL;
225        }
226        return $part;
227    }
228
229    /**
230     * @param int $number
231     *
232     * @return static
233     */
234    public function ignoreLines(int $number) : static
235    {
236        $this->sql['ignore_lines'] = $number;
237        return $this;
238    }
239
240    protected function renderIgnoreLines() : ?string
241    {
242        if ( ! isset($this->sql['ignore_lines'])) {
243            return null;
244        }
245        return " IGNORE {$this->sql['ignore_lines']} LINES";
246    }
247
248    /**
249     * @return string
250     */
251    public function sql() : string
252    {
253        $sql = 'LOAD DATA' . \PHP_EOL;
254        $part = $this->renderOptions();
255        if ($part) {
256            $sql .= $part . \PHP_EOL;
257        }
258        $sql .= $this->renderInfile() . \PHP_EOL;
259        $sql .= $this->renderIntoTable() . \PHP_EOL;
260        $part = $this->renderCharset();
261        if ($part) {
262            $sql .= $part . \PHP_EOL;
263        }
264        $sql .= $this->renderColumns();
265        $sql .= $this->renderLines();
266        $part = $this->renderIgnoreLines();
267        if ($part) {
268            $sql .= $part . \PHP_EOL;
269        }
270        return $sql;
271    }
272
273    public function run() : int|string
274    {
275        return $this->database->exec($this->sql());
276    }
277}