Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.12% covered (success)
76.12%
102 / 134
58.33% covered (warning)
58.33%
14 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
Sqlite
76.12% covered (success)
76.12%
102 / 134
58.33% covered (warning)
58.33%
14 / 24
139.65
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 connect
75.00% covered (success)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 setOptions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 hasOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dbFileExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 beginTransaction
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 commit
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 rollback
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 isSuccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
12
 query
64.71% covered (warning)
64.71%
11 / 17
0.00% covered (danger)
0.00%
0 / 1
10.81
 prepare
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
9.66
 bindParams
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
10.54
 bindParam
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 bindValue
80.00% covered (success)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 execute
58.33% covered (warning)
58.33%
7 / 12
0.00% covered (danger)
0.00%
0 / 1
10.54
 fetch
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 fetchAll
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 disconnect
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 escape
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLastId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNumberOfRows
70.00% covered (success)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
5.68
 getNumberOfAffectedRows
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVersion
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTables
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
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 */
14namespace 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 */
26class 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}