Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
76 / 76 |
|
100.00% |
10 / 10 |
CRAP | |
100.00% |
1 / 1 |
Insert | |
100.00% |
76 / 76 |
|
100.00% |
10 / 10 |
28 | |
100.00% |
1 / 1 |
renderOptions | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
5 | |||
into | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
renderInto | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
columns | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
renderColumns | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
onDuplicateKeyUpdate | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
renderOnDuplicateKeyUpdate | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
checkRowStatementsConflict | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
sql | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
7 | |||
run | |
100.00% |
1 / 1 |
|
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 | */ |
10 | namespace Framework\Database\Manipulation; |
11 | |
12 | use Closure; |
13 | use InvalidArgumentException; |
14 | use LogicException; |
15 | |
16 | /** |
17 | * Class Insert. |
18 | * |
19 | * @see https://mariadb.com/kb/en/insert/ |
20 | * |
21 | * @package database |
22 | */ |
23 | class Insert extends Statement |
24 | { |
25 | use Traits\Select; |
26 | use Traits\Set; |
27 | use Traits\Values; |
28 | |
29 | /** |
30 | * @see https://mariadb.com/kb/en/insert-delayed/ |
31 | * |
32 | * @var string |
33 | */ |
34 | public const OPT_DELAYED = 'DELAYED'; |
35 | /** |
36 | * Convert errors to warnings, which will not stop inserts of additional rows. |
37 | * |
38 | * @see https://mariadb.com/kb/en/insert-ignore/ |
39 | * |
40 | * @var string |
41 | */ |
42 | public const OPT_IGNORE = 'IGNORE'; |
43 | /** |
44 | * @see https://mariadb.com/kb/en/high_priority-and-low_priority/ |
45 | * |
46 | * @var string |
47 | */ |
48 | public const OPT_HIGH_PRIORITY = 'HIGH_PRIORITY'; |
49 | /** |
50 | * @see https://mariadb.com/kb/en/high_priority-and-low_priority/ |
51 | * |
52 | * @var string |
53 | */ |
54 | public const OPT_LOW_PRIORITY = 'LOW_PRIORITY'; |
55 | |
56 | protected function renderOptions() : ?string |
57 | { |
58 | if ( ! $this->hasOptions()) { |
59 | return null; |
60 | } |
61 | $options = $this->sql['options']; |
62 | foreach ($options as &$option) { |
63 | $input = $option; |
64 | $option = \strtoupper($option); |
65 | if ( ! \in_array($option, [ |
66 | static::OPT_DELAYED, |
67 | static::OPT_IGNORE, |
68 | static::OPT_LOW_PRIORITY, |
69 | static::OPT_HIGH_PRIORITY, |
70 | ], true)) { |
71 | throw new InvalidArgumentException("Invalid option: {$input}"); |
72 | } |
73 | } |
74 | unset($option); |
75 | $intersection = \array_intersect( |
76 | $options, |
77 | [static::OPT_DELAYED, static::OPT_HIGH_PRIORITY, static::OPT_LOW_PRIORITY] |
78 | ); |
79 | if (\count($intersection) > 1) { |
80 | throw new LogicException( |
81 | 'Options LOW_PRIORITY, DELAYED or HIGH_PRIORITY can not be used together' |
82 | ); |
83 | } |
84 | $options = \implode(' ', $options); |
85 | return " {$options}"; |
86 | } |
87 | |
88 | /** |
89 | * Sets the INTO table. |
90 | * |
91 | * @param string $table Table name |
92 | * |
93 | * @return static |
94 | */ |
95 | public function into(string $table) : static |
96 | { |
97 | $this->sql['into'] = $table; |
98 | return $this; |
99 | } |
100 | |
101 | /** |
102 | * Renders the "INTO $table" clause. |
103 | * |
104 | * @throws LogicException if INTO was not set |
105 | * |
106 | * @return string |
107 | */ |
108 | protected function renderInto() : string |
109 | { |
110 | if ( ! isset($this->sql['into'])) { |
111 | throw new LogicException('INTO table must be set'); |
112 | } |
113 | return ' INTO ' . $this->renderIdentifier($this->sql['into']); |
114 | } |
115 | |
116 | /** |
117 | * Sets the INTO columns. |
118 | * |
119 | * @param string $column Column name |
120 | * @param string ...$columns Extra column names |
121 | * |
122 | * @return static |
123 | */ |
124 | public function columns(string $column, string ...$columns) : static |
125 | { |
126 | $this->sql['columns'] = [$column, ...$columns]; |
127 | return $this; |
128 | } |
129 | |
130 | /** |
131 | * Renders the INTO $table "(...$columns)" part. |
132 | * |
133 | * @return string|null The imploded columns or null if none was set |
134 | */ |
135 | protected function renderColumns() : ?string |
136 | { |
137 | if ( ! isset($this->sql['columns'])) { |
138 | return null; |
139 | } |
140 | $columns = []; |
141 | foreach ($this->sql['columns'] as $column) { |
142 | $columns[] = $this->renderIdentifier($column); |
143 | } |
144 | $columns = \implode(', ', $columns); |
145 | return " ({$columns})"; |
146 | } |
147 | |
148 | /** |
149 | * Sets the ON DUPLICATE KEY UPDATE part. |
150 | * |
151 | * @param array<string,Closure|float|int|string|null>|object $columns Column name |
152 | * as key/property, column value/expression as value |
153 | * |
154 | * @see https://mariadb.com/kb/en/insert-on-duplicate-key-update/ |
155 | * |
156 | * @return static |
157 | */ |
158 | public function onDuplicateKeyUpdate(array | object $columns) : static |
159 | { |
160 | $this->sql['on_duplicate'] = (array) $columns; |
161 | return $this; |
162 | } |
163 | |
164 | /** |
165 | * Renders the ON DUPLICATE KEY UPDATE part. |
166 | * |
167 | * @return string|null The part or null if it was not set |
168 | */ |
169 | protected function renderOnDuplicateKeyUpdate() : ?string |
170 | { |
171 | if ( ! isset($this->sql['on_duplicate'])) { |
172 | return null; |
173 | } |
174 | $onDuplicate = []; |
175 | foreach ($this->sql['on_duplicate'] as $column => $value) { |
176 | $onDuplicate[] = $this->renderAssignment($column, $value); |
177 | } |
178 | $onDuplicate = \implode(', ', $onDuplicate); |
179 | return " ON DUPLICATE KEY UPDATE {$onDuplicate}"; |
180 | } |
181 | |
182 | /** |
183 | * Check for conflicts in the INSERT statement. |
184 | * |
185 | * @throws LogicException if it has conflicts |
186 | */ |
187 | protected function checkRowStatementsConflict() : void |
188 | { |
189 | if ( ! isset($this->sql['values']) |
190 | && ! isset($this->sql['select']) |
191 | && ! $this->hasSet() |
192 | ) { |
193 | throw new LogicException( |
194 | 'The INSERT INTO must be followed by VALUES, SET or SELECT statement' |
195 | ); |
196 | } |
197 | } |
198 | |
199 | /** |
200 | * Renders the INSERT statement. |
201 | * |
202 | * @return string |
203 | */ |
204 | public function sql() : string |
205 | { |
206 | $sql = 'INSERT' . \PHP_EOL; |
207 | $part = $this->renderOptions(); |
208 | if ($part) { |
209 | $sql .= $part . \PHP_EOL; |
210 | } |
211 | $sql .= $this->renderInto() . \PHP_EOL; |
212 | $part = $this->renderColumns(); |
213 | if ($part) { |
214 | $sql .= $part . \PHP_EOL; |
215 | } |
216 | $this->checkRowStatementsConflict(); |
217 | $part = $this->renderValues(); |
218 | if ($part) { |
219 | $sql .= $part . \PHP_EOL; |
220 | } |
221 | $part = $this->renderSetCheckingConflicts(); |
222 | if ($part) { |
223 | $sql .= $part . \PHP_EOL; |
224 | } |
225 | $part = $this->renderSelect(); |
226 | if ($part) { |
227 | $sql .= $part . \PHP_EOL; |
228 | } |
229 | $part = $this->renderOnDuplicateKeyUpdate(); |
230 | if ($part) { |
231 | $sql .= $part . \PHP_EOL; |
232 | } |
233 | return $sql; |
234 | } |
235 | |
236 | /** |
237 | * Runs the INSERT statement. |
238 | * |
239 | * @return int|string The number of affected rows |
240 | */ |
241 | public function run() : int|string |
242 | { |
243 | return $this->database->exec($this->sql()); |
244 | } |
245 | } |