Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
76.12% |
102 / 134 |
|
58.33% |
14 / 24 |
CRAP | |
0.00% |
0 / 1 |
Sqlite | |
76.12% |
102 / 134 |
|
58.33% |
14 / 24 |
139.65 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
connect | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
4.25 | |||
setOptions | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
hasOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
dbFileExists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
beginTransaction | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
commit | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
rollback | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
isSuccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
12 | |||
query | |
64.71% |
11 / 17 |
|
0.00% |
0 / 1 |
10.81 | |||
prepare | |
53.33% |
8 / 15 |
|
0.00% |
0 / 1 |
9.66 | |||
bindParams | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
10.54 | |||
bindParam | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
bindValue | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
execute | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
10.54 | |||
fetch | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
fetchAll | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
disconnect | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
escape | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLastId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNumberOfRows | |
70.00% |
7 / 10 |
|
0.00% |
0 / 1 |
5.68 | |||
getNumberOfAffectedRows | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getVersion | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getTables | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 |
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\Adapter; |
15 | |
16 | /** |
17 | * SQLite database adapter class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Db |
21 | * @author Nick Sagona, III <dev@nolainteractive.com> |
22 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
23 | * @license http://www.popphp.org/license New BSD License |
24 | * @version 6.5.0 |
25 | */ |
26 | class Sqlite extends AbstractAdapter |
27 | { |
28 | |
29 | /** |
30 | * SQLite flags |
31 | * @var ?int |
32 | */ |
33 | protected ?int $flags = null; |
34 | |
35 | /** |
36 | * SQLite key |
37 | * @var ?string |
38 | */ |
39 | protected ?string $key = null; |
40 | |
41 | /** |
42 | * Last SQL query |
43 | * @var ?string |
44 | */ |
45 | protected ?string $lastSql = null; |
46 | |
47 | /** |
48 | * Last result |
49 | * @var mixed |
50 | */ |
51 | protected mixed $lastResult = null; |
52 | |
53 | /** |
54 | * Constructor |
55 | * |
56 | * Instantiate the SQLite database connection object using SQLite3 |
57 | * |
58 | * @param array $options |
59 | */ |
60 | public function __construct(array $options = []) |
61 | { |
62 | if (!empty($options)) { |
63 | $this->connect($options); |
64 | } |
65 | } |
66 | |
67 | /** |
68 | * Connect to the database |
69 | * |
70 | * @param array $options |
71 | * @return Sqlite |
72 | */ |
73 | public function connect(array $options = []): Sqlite |
74 | { |
75 | if (!empty($options)) { |
76 | $this->setOptions($options); |
77 | } else if (!$this->hasOptions()) { |
78 | $this->throwError('Error: The database file was not passed.'); |
79 | } else if (!$this->dbFileExists()) { |
80 | $this->throwError("Error: The database file '" . $this->options['database'] . "'does not exists."); |
81 | } |
82 | |
83 | $this->connection = new \SQLite3($this->options['database'], $this->flags, (string)$this->key); |
84 | |
85 | return $this; |
86 | } |
87 | |
88 | /** |
89 | * Set database connection options |
90 | * |
91 | * @param array $options |
92 | * @return Sqlite |
93 | */ |
94 | public function setOptions(array $options): Sqlite |
95 | { |
96 | $this->options = $options; |
97 | |
98 | if (!$this->hasOptions()) { |
99 | $this->throwError('Error: The database file was not passed.'); |
100 | } else if (!$this->dbFileExists()) { |
101 | $this->throwError("Error: The database file '" . $this->options['database'] . "'does not exists."); |
102 | } |
103 | |
104 | $this->flags = (isset($this->options['flags'])) ? $this->options['flags'] : SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE; |
105 | $this->key = (isset($this->options['key'])) ? $this->options['key'] : null; |
106 | |
107 | return $this; |
108 | } |
109 | |
110 | /** |
111 | * Has database connection options |
112 | * |
113 | * @return bool |
114 | */ |
115 | public function hasOptions(): bool |
116 | { |
117 | return (isset($this->options['database'])); |
118 | } |
119 | |
120 | /** |
121 | * Does the database file exist |
122 | * |
123 | * @return bool |
124 | */ |
125 | public function dbFileExists(): bool |
126 | { |
127 | return (isset($this->options['database']) && file_exists($this->options['database'])); |
128 | } |
129 | |
130 | /** |
131 | * Begin a transaction |
132 | * |
133 | * @return Sqlite |
134 | */ |
135 | public function beginTransaction(): Sqlite |
136 | { |
137 | $this->getTransactionManager()->enter( |
138 | beginFunc: function () { $this->query('BEGIN TRANSACTION'); }, |
139 | savepointFunc: function (string $sp) { $this->query('SAVEPOINT ' . $sp); }, |
140 | ); |
141 | |
142 | return $this; |
143 | } |
144 | |
145 | /** |
146 | * Commit a transaction |
147 | * |
148 | * @return Sqlite |
149 | */ |
150 | public function commit(): Sqlite |
151 | { |
152 | $this->getTransactionManager()->leave(true, |
153 | commitFunc: function () { $this->query('COMMIT'); }, |
154 | savepointReleaseFunc: function (string $sp) { $this->query('RELEASE SAVEPOINT ' . $sp); }, |
155 | ); |
156 | |
157 | return $this; |
158 | } |
159 | |
160 | /** |
161 | * Rollback a transaction |
162 | * |
163 | * @return Sqlite |
164 | */ |
165 | public function rollback(): Sqlite |
166 | { |
167 | $this->getTransactionManager()->leave(false, |
168 | rollbackFunc: function () { $this->query('ROLLBACK'); }, |
169 | savepointRollbackFunc: function (string $sp) { $this->query('ROLLBACK TO SAVEPOINT ' . $sp); }, |
170 | ); |
171 | |
172 | return $this; |
173 | } |
174 | |
175 | /** |
176 | * Check if transaction is success |
177 | * |
178 | * @return bool |
179 | */ |
180 | public function isSuccess(): bool |
181 | { |
182 | return ((($this->result !== null) && ($this->result !== false)) && (!$this->hasError())); |
183 | } |
184 | |
185 | /** |
186 | * Execute a SQL query directly |
187 | * |
188 | * @param mixed $sql |
189 | * @return Sqlite |
190 | */ |
191 | public function query(mixed $sql): Sqlite |
192 | { |
193 | if ($sql instanceof \Pop\Db\Sql\AbstractSql) { |
194 | $sql = (string)$sql; |
195 | } |
196 | |
197 | $this->lastSql = (stripos($sql, 'select') !== false) ? $sql : null; |
198 | |
199 | if (!($this->result = $this->connection->query($sql))) { |
200 | if ($this->profiler !== null) { |
201 | $this->profiler->addStep(); |
202 | $this->profiler->current->setQuery($sql); |
203 | $this->profiler->current->addError($this->connection->lastErrorMsg(), $this->connection->lastErrorCode()); |
204 | } |
205 | $this->throwError('Error: ' . $this->connection->lastErrorCode() . ' => ' . $this->connection->lastErrorMsg()); |
206 | } else if ($this->profiler !== null) { |
207 | $this->profiler->addStep(); |
208 | $this->profiler->current->setQuery($sql); |
209 | } |
210 | |
211 | if ($this->profiler !== null) { |
212 | $this->profiler->current->finish(); |
213 | if ($this->profiler->hasDebugger()) { |
214 | $this->profiler->debugger()->save(); |
215 | } |
216 | } |
217 | |
218 | return $this; |
219 | } |
220 | |
221 | /** |
222 | * Prepare a SQL query |
223 | * |
224 | * @param mixed $sql |
225 | * @return Sqlite |
226 | */ |
227 | public function prepare(mixed $sql): Sqlite |
228 | { |
229 | if ($sql instanceof \Pop\Db\Sql\AbstractSql) { |
230 | $sql = (string)$sql; |
231 | } |
232 | |
233 | $this->statement = $this->connection->prepare($sql); |
234 | if (($this->statement === false) && ($this->connection->lastErrorCode() != 0)) { |
235 | if ($this->profiler !== null) { |
236 | $this->profiler->addStep(); |
237 | $this->profiler->current->setQuery($sql); |
238 | $this->profiler->current->addError($this->connection->lastErrorMsg(), $this->connection->lastErrorCode()); |
239 | } |
240 | $this->throwError( |
241 | 'SQLite Statement Error: ' . $this->connection->lastErrorCode() . ' => ' . $this->connection->lastErrorMsg() |
242 | ); |
243 | } else if ($this->profiler !== null) { |
244 | $this->profiler->addStep(); |
245 | $this->profiler->current->setQuery($sql); |
246 | } |
247 | |
248 | return $this; |
249 | } |
250 | |
251 | /** |
252 | * Bind parameters to a prepared SQL query |
253 | * |
254 | * @param array $params |
255 | * @return Sqlite |
256 | */ |
257 | public function bindParams(array $params): Sqlite |
258 | { |
259 | if ($this->profiler !== null) { |
260 | $this->profiler->current->addParams($params); |
261 | } |
262 | |
263 | foreach ($params as $dbColumnName => $dbColumnValue) { |
264 | if (is_array($dbColumnValue)) { |
265 | foreach ($dbColumnValue as $k => $dbColumnVal) { |
266 | ${$dbColumnName . ($k + 1)} = $dbColumnVal; |
267 | if ($this->statement->bindParam(':' . $dbColumnName . ($k + 1), ${$dbColumnName . ($k + 1)}) === false) { |
268 | $this->throwError('Error: There was an error binding the parameters'); |
269 | } |
270 | } |
271 | } else { |
272 | ${$dbColumnName} = $dbColumnValue; |
273 | if ($this->statement->bindParam(':' . $dbColumnName, ${$dbColumnName}) === false) { |
274 | $this->throwError('Error: There was an error binding the parameters'); |
275 | } |
276 | } |
277 | } |
278 | |
279 | return $this; |
280 | } |
281 | |
282 | /** |
283 | * Bind a parameter for a prepared SQL query |
284 | * |
285 | * @param mixed $param |
286 | * @param mixed $value |
287 | * @param int $type |
288 | * @return Sqlite |
289 | */ |
290 | public function bindParam(mixed $param, mixed $value, int $type = SQLITE3_BLOB): Sqlite |
291 | { |
292 | if ($this->profiler !== null) { |
293 | $this->profiler->current->addParam($param, $value); |
294 | } |
295 | |
296 | if ($this->statement->bindParam($param, $value, $type) === false) { |
297 | $this->throwError('Error: There was an error binding the parameter'); |
298 | } |
299 | |
300 | return $this; |
301 | } |
302 | |
303 | /** |
304 | * Bind a value for a prepared SQL query |
305 | * |
306 | * @param mixed $param |
307 | * @param mixed $value |
308 | * @param int $type |
309 | * @return Sqlite |
310 | */ |
311 | public function bindValue(mixed $param, mixed $value, int $type = SQLITE3_BLOB): Sqlite |
312 | { |
313 | if ($this->profiler !== null) { |
314 | $this->profiler->current->addParam($param, $value); |
315 | } |
316 | |
317 | if ($this->statement->bindValue($param, $value, $type) === false) { |
318 | $this->throwError('Error: There was an error binding the value'); |
319 | } |
320 | |
321 | return $this; |
322 | } |
323 | |
324 | /** |
325 | * Execute a prepared SQL query |
326 | * |
327 | * @return Sqlite |
328 | */ |
329 | public function execute(): Sqlite |
330 | { |
331 | if ($this->statement === null) { |
332 | $this->throwError('Error: The database statement resource is not currently set.'); |
333 | } |
334 | |
335 | $this->result = $this->statement->execute(); |
336 | |
337 | if (($this->result === false) && ($this->connection->lastErrorCode() != 0)) { |
338 | if ($this->profiler !== null) { |
339 | $this->profiler->current->addError($this->connection->lastErrorMsg(), $this->connection->lastErrorCode()); |
340 | } |
341 | $this->throwError('Error: ' . $this->connection->lastErrorCode() . ' => ' . $this->connection->lastErrorMsg()); |
342 | } |
343 | |
344 | if ($this->profiler !== null) { |
345 | $this->profiler->current->finish(); |
346 | if ($this->profiler->hasDebugger()) { |
347 | $this->profiler->debugger()->save(); |
348 | } |
349 | } |
350 | |
351 | return $this; |
352 | } |
353 | |
354 | /** |
355 | * Fetch and return a row from the result |
356 | * |
357 | * @return mixed |
358 | */ |
359 | public function fetch(): mixed |
360 | { |
361 | if ($this->result === null) { |
362 | $this->throwError('Error: The database result resource is not currently set.'); |
363 | } |
364 | |
365 | return $this->result->fetchArray(SQLITE3_ASSOC); |
366 | } |
367 | |
368 | /** |
369 | * Fetch and return all rows from the result |
370 | * |
371 | * @return array |
372 | */ |
373 | public function fetchAll(): array |
374 | { |
375 | $rows = []; |
376 | |
377 | while (($row = $this->fetch())) { |
378 | $rows[] = $row; |
379 | } |
380 | |
381 | return $rows; |
382 | } |
383 | |
384 | /** |
385 | * Disconnect from the database |
386 | * |
387 | * @return void |
388 | */ |
389 | public function disconnect(): void |
390 | { |
391 | if ($this->isConnected()) { |
392 | $this->connection->close(); |
393 | } |
394 | |
395 | parent::disconnect(); |
396 | } |
397 | |
398 | /** |
399 | * Escape the value |
400 | * |
401 | * @param ?string $value |
402 | * @return string |
403 | */ |
404 | public function escape(?string $value = null): string |
405 | { |
406 | return $this->connection->escapeString($value); |
407 | } |
408 | |
409 | /** |
410 | * Return the last ID of the last query |
411 | * |
412 | * @return int |
413 | */ |
414 | public function getLastId(): int |
415 | { |
416 | return $this->connection->lastInsertRowID(); |
417 | } |
418 | |
419 | /** |
420 | * Return the number of rows from the last query |
421 | * |
422 | * @throws Exception |
423 | * @return int |
424 | */ |
425 | public function getNumberOfRows(): int |
426 | { |
427 | $count = 0; |
428 | |
429 | if ($this->lastSql === null) { |
430 | $count = $this->connection->changes(); |
431 | } else { |
432 | if ((!($this->lastResult = $this->connection->query($this->lastSql)) && ($this->connection->lastErrorCode() != 0))) { |
433 | $this->throwError( |
434 | 'Error: ' . $this->connection->lastErrorCode() . ' => ' . $this->connection->lastErrorMsg() |
435 | ); |
436 | } else { |
437 | while (($row = $this->lastResult->fetcharray(SQLITE3_ASSOC)) != false) { |
438 | $count++; |
439 | } |
440 | } |
441 | } |
442 | |
443 | return $count; |
444 | } |
445 | |
446 | /** |
447 | * Return the number of affected rows from the last query |
448 | * |
449 | * @return int |
450 | */ |
451 | public function getNumberOfAffectedRows(): int |
452 | { |
453 | return $this->connection->changes(); |
454 | } |
455 | |
456 | /** |
457 | * Return the database version |
458 | * |
459 | * @return string |
460 | */ |
461 | public function getVersion(): string |
462 | { |
463 | $version = $this->connection->version(); |
464 | return 'SQLite ' . $version['versionString']; |
465 | } |
466 | |
467 | /** |
468 | * Return the tables in the database |
469 | * |
470 | * @return array |
471 | */ |
472 | public function getTables(): array |
473 | { |
474 | $tables = []; |
475 | $sql = "SELECT name FROM sqlite_master WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%' " . |
476 | "UNION ALL SELECT name FROM sqlite_temp_master WHERE type IN ('table', 'view') ORDER BY 1"; |
477 | |
478 | $this->query($sql); |
479 | while (($row = $this->fetch())) { |
480 | $tables[] = $row['name']; |
481 | } |
482 | |
483 | return $tables; |
484 | } |
485 | |
486 | } |