Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
117 / 117 |
|
100.00% |
14 / 14 |
CRAP | |
100.00% |
1 / 1 |
Data | |
100.00% |
117 / 117 |
|
100.00% |
14 / 14 |
61 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setDivide | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getDivide | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getTable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSql | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
onConflict | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
onDuplicateKeyUpdate | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isSerialized | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
serialize | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
20 | |||
writeToFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
streamToFile | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
23 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatConflicts | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (http://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@nolainteractive.com> |
7 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
8 | * @license http://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Db\Sql; |
15 | |
16 | use Pop\Db\Adapter\AbstractAdapter; |
17 | |
18 | /** |
19 | * Data class to output data to a valid SQL file |
20 | * |
21 | * @category Pop |
22 | * @package Pop\Db |
23 | * @author Nick Sagona, III <dev@nolainteractive.com> |
24 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
25 | * @license http://www.popphp.org/license New BSD License |
26 | * @version 6.5.0 |
27 | */ |
28 | class Data extends AbstractSql |
29 | { |
30 | |
31 | /** |
32 | * Database table |
33 | * @var string |
34 | */ |
35 | protected string $table = 'pop_db_data'; |
36 | |
37 | /** |
38 | * Divide INSERT groups by # (0 creates one big INSERT statement, 1 creates an INSERT statement per row) |
39 | * @var int |
40 | */ |
41 | protected int $divide = 1; |
42 | |
43 | /** |
44 | * Conflict key for UPSERT |
45 | * @var ?string |
46 | */ |
47 | protected ?string $conflictKey = null; |
48 | |
49 | /** |
50 | * Conflict columns for UPSERT |
51 | * @var array |
52 | */ |
53 | protected array $conflictColumns = []; |
54 | |
55 | /** |
56 | * SQL string |
57 | * @var ?string |
58 | */ |
59 | protected ?string $sql = null; |
60 | |
61 | /** |
62 | * Constructor |
63 | * |
64 | * Instantiate the SQL object |
65 | * |
66 | * @param AbstractAdapter $db |
67 | * @param string $table |
68 | * @param int $divide |
69 | */ |
70 | public function __construct(AbstractAdapter $db, string $table = 'pop_db_data', int $divide = 1) |
71 | { |
72 | parent::__construct($db); |
73 | $this->setDivide($divide); |
74 | $this->setTable($table); |
75 | } |
76 | |
77 | /** |
78 | * Set the INSERT divide |
79 | * |
80 | * @param int $divide |
81 | * @return Data |
82 | */ |
83 | public function setDivide(int $divide): Data |
84 | { |
85 | $this->divide = $divide; |
86 | return $this; |
87 | } |
88 | |
89 | /** |
90 | * Get the INSERT divide |
91 | * |
92 | * @return int |
93 | */ |
94 | public function getDivide(): int |
95 | { |
96 | return $this->divide; |
97 | } |
98 | |
99 | /** |
100 | * Set the database table |
101 | * |
102 | * @param string $table |
103 | * @return Data |
104 | */ |
105 | public function setTable(string $table): Data |
106 | { |
107 | $this->table = $table; |
108 | return $this; |
109 | } |
110 | |
111 | /** |
112 | * Get the database table |
113 | * |
114 | * @return string |
115 | */ |
116 | public function getTable(): string |
117 | { |
118 | return $this->table; |
119 | } |
120 | |
121 | /** |
122 | * Get SQL string |
123 | * |
124 | * @return ?string |
125 | */ |
126 | public function getSql(): ?string |
127 | { |
128 | return $this->sql; |
129 | } |
130 | |
131 | /** |
132 | * Set what to do on a insert conflict (UPSERT - PostgreSQL & SQLite) |
133 | * |
134 | * @param array $columns |
135 | * @param ?string $key |
136 | * @return Data |
137 | */ |
138 | public function onConflict(array $columns, ?string $key = null): Data |
139 | { |
140 | $this->conflictColumns = $columns; |
141 | $this->conflictKey = $key; |
142 | return $this; |
143 | } |
144 | |
145 | /** |
146 | * Set columns to handle duplicates/conflicts (UPSERT - MySQL-ism) |
147 | * |
148 | * @param array $columns |
149 | * @return Data |
150 | */ |
151 | public function onDuplicateKeyUpdate(array $columns): Data |
152 | { |
153 | $this->onConflict($columns); |
154 | return $this; |
155 | } |
156 | |
157 | /** |
158 | * Check if data was serialized into SQL |
159 | * |
160 | * @return bool |
161 | */ |
162 | public function isSerialized(): bool |
163 | { |
164 | return ($this->sql !== null); |
165 | } |
166 | |
167 | /** |
168 | * Serialize the data into INSERT statements |
169 | * |
170 | * @param array $data |
171 | * @param mixed $omit |
172 | * @param bool $nullEmpty |
173 | * @param bool $forceQuote |
174 | * @return ?string |
175 | */ |
176 | public function serialize(array $data, mixed $omit = null, bool $nullEmpty = false, bool $forceQuote = false): ?string |
177 | { |
178 | if ($omit !== null) { |
179 | $omit = (!is_array($omit)) ? [$omit] : $omit; |
180 | } |
181 | |
182 | $this->sql = ''; |
183 | $table = $this->quoteId($this->table); |
184 | $columns = array_keys(reset($data)); |
185 | |
186 | if (!empty($omit)) { |
187 | foreach ($omit as $o) { |
188 | if (in_array($o, $columns)) { |
189 | unset($columns[array_search($o, $columns)]); |
190 | } |
191 | } |
192 | } |
193 | |
194 | $columns = array_map([$this, 'quoteId'], $columns); |
195 | $insert = "INSERT INTO " . $table . " (" . implode(', ', $columns) . ") VALUES" . PHP_EOL; |
196 | $onUpdate = $this->formatConflicts(); |
197 | |
198 | foreach ($data as $i => $row) { |
199 | if (!empty($omit)) { |
200 | foreach ($omit as $o) { |
201 | if (isset($row[$o])) { |
202 | unset($row[$o]); |
203 | } |
204 | } |
205 | } |
206 | $value = "(" . implode(', ', array_map(function($value) use ($forceQuote) { |
207 | return $this->quote($value, $forceQuote); |
208 | }, $row)) . ")"; |
209 | if ($nullEmpty) { |
210 | $value = str_replace(["('',", " '', ", ", '')"], ["(NULL,", ' NULL, ', ', NULL)'], $value); |
211 | } |
212 | |
213 | switch ($this->divide) { |
214 | case 0: |
215 | if ($i == 0) { |
216 | $this->sql .= $insert; |
217 | } |
218 | $this->sql .= $value; |
219 | $this->sql .= ($i == (count($data) - 1)) ? $onUpdate . ';' : ','; |
220 | $this->sql .= PHP_EOL; |
221 | break; |
222 | case 1: |
223 | $this->sql .= $insert . $value . $onUpdate . ';' . PHP_EOL; |
224 | break; |
225 | default: |
226 | if (($i % $this->divide) == 0) { |
227 | $this->sql .= $insert . $value . (($i == (count($data) - 1)) ? $onUpdate . ';' : ',') . PHP_EOL; |
228 | } else { |
229 | $this->sql .= $value; |
230 | $this->sql .= (((($i + 1) % $this->divide) == 0) || ($i == (count($data) - 1))) ? $onUpdate . ';' : ','; |
231 | $this->sql .= PHP_EOL; |
232 | } |
233 | } |
234 | } |
235 | |
236 | return $this->sql; |
237 | } |
238 | |
239 | /** |
240 | * Output SQL to a file |
241 | * |
242 | * @param string $to |
243 | * @param ?string $header |
244 | * @param ?string $footer |
245 | * @return void |
246 | */ |
247 | public function writeToFile(string $to, ?string $header = null, ?string $footer = null): void |
248 | { |
249 | file_put_contents($to, $header . $this->sql . $footer); |
250 | } |
251 | |
252 | /** |
253 | * Serialize the data into INSERT statements |
254 | * |
255 | * @param array $data |
256 | * @param ?string $to |
257 | * @param mixed $omit |
258 | * @param bool $nullEmpty |
259 | * @param ?string $header |
260 | * @param ?string $footer |
261 | * @return void |
262 | */ |
263 | public function streamToFile( |
264 | array $data, ?string $to, mixed $omit = null, bool $nullEmpty = false, ?string $header = null, ?string $footer = null |
265 | ): void |
266 | { |
267 | if (!file_exists($to)) { |
268 | touch($to); |
269 | } |
270 | |
271 | $handle = fopen($to, 'a'); |
272 | |
273 | if ($header !== null) { |
274 | fwrite($handle, $header); |
275 | } |
276 | |
277 | if ($omit !== null) { |
278 | $omit = (!is_array($omit)) ? [$omit] : $omit; |
279 | } |
280 | |
281 | $table = $this->quoteId($this->table); |
282 | $columns = array_keys(reset($data)); |
283 | |
284 | if (!empty($omit)) { |
285 | foreach ($omit as $o) { |
286 | if (in_array($o, $columns)) { |
287 | unset($columns[array_search($o, $columns)]); |
288 | } |
289 | } |
290 | } |
291 | |
292 | $columns = array_map([$this, 'quoteId'], $columns); |
293 | $insert = "INSERT INTO " . $table . " (" . implode(', ', $columns) . ") VALUES" . PHP_EOL; |
294 | $onUpdate = $this->formatConflicts(); |
295 | |
296 | foreach ($data as $i => $row) { |
297 | if (!empty($omit)) { |
298 | foreach ($omit as $o) { |
299 | if (isset($row[$o])) { |
300 | unset($row[$o]); |
301 | } |
302 | } |
303 | } |
304 | $value = "(" . implode(', ', array_map([$this, 'quote'], $row)) . ")"; |
305 | if ($nullEmpty) { |
306 | $value = str_replace(["'', ", ", '')"], ['NULL, ', ', NULL)'], $value); |
307 | } |
308 | |
309 | switch ($this->divide) { |
310 | case 0: |
311 | if ($i == 0) { |
312 | fwrite($handle, $insert); |
313 | } |
314 | fwrite($handle, $value); |
315 | fwrite($handle, ($i == (count($data) - 1)) ? $onUpdate . ';' : ','); |
316 | fwrite($handle, PHP_EOL); |
317 | break; |
318 | case 1: |
319 | fwrite($handle, $insert . $value . ';' . PHP_EOL); |
320 | break; |
321 | default: |
322 | if (($i % $this->divide) == 0) { |
323 | fwrite($handle, $insert . $value . (($i == (count($data) - 1)) ? $onUpdate . ';' : ',') . PHP_EOL); |
324 | } else { |
325 | fwrite($handle, $value); |
326 | fwrite($handle, ((((($i + 1) % $this->divide) == 0) || ($i == (count($data) - 1))) ? $onUpdate . ';' : ',')); |
327 | fwrite($handle, PHP_EOL); |
328 | } |
329 | } |
330 | } |
331 | |
332 | |
333 | if ($footer !== null) { |
334 | fwrite($handle, $footer); |
335 | } |
336 | |
337 | fclose($handle); |
338 | } |
339 | |
340 | /** |
341 | * __toString magic method |
342 | * |
343 | * @return string |
344 | */ |
345 | public function __toString(): string |
346 | { |
347 | return $this->sql; |
348 | } |
349 | |
350 | /** |
351 | * Method to format conflicts (UPSERT) |
352 | * |
353 | * @return string |
354 | */ |
355 | protected function formatConflicts(): string |
356 | { |
357 | $onUpdate = ''; |
358 | |
359 | if (!empty($this->conflictColumns)) { |
360 | $updates = []; |
361 | switch ($this->dbType) { |
362 | case self::MYSQL: |
363 | foreach ($this->conflictColumns as $conflictColumn) { |
364 | $updates[] = $this->quoteId($conflictColumn) . ' = VALUES(' . $conflictColumn .')'; |
365 | } |
366 | $onUpdate = PHP_EOL . ' ON DUPLICATE KEY UPDATE ' . implode(', ', $updates); |
367 | break; |
368 | case self::SQLITE: |
369 | case self::PGSQL: |
370 | foreach ($this->conflictColumns as $conflictColumn) { |
371 | $updates[] = $this->quoteId($conflictColumn) . ' = excluded.' . $conflictColumn; |
372 | } |
373 | $onUpdate = PHP_EOL . ' ON CONFLICT (' . $this->quoteId($this->conflictKey) . ') DO UPDATE SET ' |
374 | . implode(', ', $updates); |
375 | break; |
376 | } |
377 | } |
378 | |
379 | return $onUpdate; |
380 | } |
381 | |
382 | } |