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