Passed
Branch extract-store (f24e42)
by Konrad
04:37
created

PDOSQLiteAdapter::getNumberOfRows()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 14
ccs 9
cts 9
cp 1
crap 1
rs 10
1
<?php
2
3
/*
4
 * This file is part of the sweetrdf/InMemoryStoreSqlite package and licensed under
5
 * the terms of the GPL-3 license.
6
 *
7
 * (c) Konrad Abicht <[email protected]>
8
 * (c) Benjamin Nowack
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace sweetrdf\InMemoryStoreSqlite;
15
16
use Exception;
17
use PDO;
18
19
/**
20
 * PDO SQLite adapter.
21
 */
22
class PDOSQLiteAdapter
23
{
24
    private ?\PDO $db;
25
26
    private int $lastRowCount = 0;
27
28
    /**
29
     * Sent queries.
30
     */
31
    private array $queries = [];
32
33 126
    public function __construct(string $dbName = null)
34
    {
35 126
        $this->checkRequirements();
36
37
        // set path to SQLite file
38 126
        if (!empty($dbName)) {
39
            $dsn = 'sqlite:'.$dbName;
40
        } else {
41
            // use in-memory
42 126
            $dsn = 'sqlite::memory:';
43
        }
44
45 126
        $this->db = new PDO($dsn);
46
47 126
        $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
48
49
        // errors lead to exceptions
50 126
        $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
51
52
        // default fetch mode is associative
53 126
        $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
54
55
        /*
56
         * define CONCAT function (otherwise SQLite will throw an exception)
57
         */
58 126
        $this->db->sqliteCreateFunction('CONCAT', function ($pattern, $string) {
59 2
            $result = '';
60
61 2
            foreach (\func_get_args() as $str) {
62 2
                $result .= $str;
63
            }
64
65 2
            return $result;
66 126
        });
67
68
        /*
69
         * define REGEXP function (otherwise SQLite will throw an exception)
70
         */
71 126
        $this->db->sqliteCreateFunction('REGEXP', function ($pattern, $string) {
72 1
            if (0 < preg_match('/'.$pattern.'/i', $string)) {
73 1
                return true;
74
            }
75
76 1
            return false;
77 126
        }, 2);
78
79 126
        $this->createTables();
80 126
    }
81
82 126
    public function checkRequirements()
83
    {
84 126
        if (false == \extension_loaded('pdo_sqlite')) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
85
            throw new Exception('Extension pdo_sqlite is not loaded.');
86
        }
87 126
    }
88
89
    public function deleteAllTables(): void
90
    {
91
        $this->exec(
92
            'SELECT "drop table " || name || ";"
93
               FROM sqlite_master
94
              WHERE type = "table";'
95
        );
96
    }
97
98
    /**
99
     * Creates all required tables.
100
     */
101 126
    private function createTables(): void
102
    {
103
        // triple
104 126
        $sql = 'CREATE TABLE IF NOT EXISTS triple (
105
            t INTEGER PRIMARY KEY AUTOINCREMENT,
106
            s INTEGER UNSIGNED NOT NULL,
107
            p INTEGER UNSIGNED NOT NULL,
108
            o INTEGER UNSIGNED NOT NULL,
109
            o_lang_dt INTEGER UNSIGNED NOT NULL,
110
            o_comp TEXT NOT NULL,                       -- normalized value for ORDER BY operations
111
            s_type INTEGER UNSIGNED NOT NULL DEFAULT 0, -- uri/bnode => 0/1
112
            o_type INTEGER UNSIGNED NOT NULL DEFAULT 0  -- uri/bnode/literal => 0/1/2
113
        )';
114
115 126
        $this->exec($sql);
116
117
        // g2t
118 126
        $sql = 'CREATE TABLE IF NOT EXISTS g2t (
119
            g INTEGER UNSIGNED NOT NULL,
120
            t INTEGER UNSIGNED NOT NULL,
121
            UNIQUE (g,t)
122
        )';
123
124 126
        $this->exec($sql);
125
126
        // id2val
127 126
        $sql = 'CREATE TABLE IF NOT EXISTS id2val (
128
            id INTEGER PRIMARY KEY AUTOINCREMENT,
129
            val TEXT NOT NULL,
130
            val_type INTEGER NOT NULL DEFAULT 0, -- uri/bnode/literal => 0/1/2
131
            UNIQUE (id,val_type)
132
        )';
133
134 126
        $this->exec($sql);
135
136
        // s2val
137 126
        $sql = 'CREATE TABLE IF NOT EXISTS s2val (
138
            id INTEGER UNSIGNED NOT NULL,
139
            val_hash TEXT NOT NULL,
140
            val TEXT NOT NULL,
141
            UNIQUE (id)
142
        )';
143
144 126
        $this->exec($sql);
145
146
        // o2val
147 126
        $sql = 'CREATE TABLE IF NOT EXISTS o2val (
148
            id INTEGER NOT NULL,
149
            val_hash TEXT NOT NULL,
150
            val TEXT NOT NULL,
151
            UNIQUE (id)
152
        )';
153
154 126
        $this->exec($sql);
155
156
        // setting
157 126
        $sql = 'CREATE TABLE IF NOT EXISTS setting (
158
            k TEXT NOT NULL,
159
            val TEXT NOT NULL,
160
            UNIQUE (k)
161
        )';
162
163 126
        $this->exec($sql);
164 126
    }
165
166
    /**
167
     * It gets all tables from the current database.
168
     */
169 1
    public function getAllTables(): array
170
    {
171 1
        $tables = $this->fetchList('SELECT name FROM sqlite_master WHERE type="table";');
172 1
        $result = [];
173 1
        foreach ($tables as $table) {
174
            // ignore SQLite tables
175 1
            if (false !== strpos($table['name'], 'sqlite_')) {
176 1
                continue;
177
            }
178 1
            $result[] = $table['name'];
179
        }
180
181 1
        return $result;
182
    }
183
184 2
    public function getServerVersion()
185
    {
186 2
        return $this->fetchRow('select sqlite_version()')['sqlite_version()'];
187
    }
188
189
    public function getAffectedRows(): int
190
    {
191
        return $this->lastRowCount;
192
    }
193
194
    /**
195
     * @return void
196
     */
197 1
    public function disconnect()
198
    {
199
        // FYI: https://stackoverflow.com/questions/18277233/pdo-closing-connection
200 1
        $this->db = null;
201 1
    }
202
203 98
    public function escape($value)
204
    {
205 98
        $quoted = $this->db->quote($value);
0 ignored issues
show
Bug introduced by
The method quote() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

205
        /** @scrutinizer ignore-call */ 
206
        $quoted = $this->db->quote($value);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
206
207
        /*
208
         * fixes the case, that we have double quoted strings like:
209
         *      ''x1''
210
         *
211
         * remember, this value will be surrounded by quotes later on!
212
         * so we don't send it back with quotes around.
213
         */
214 98
        if ("'" == substr($quoted, 0, 1)) {
215 98
            $quoted = substr($quoted, 1, \strlen($quoted) - 2);
216
        }
217
218 98
        return $quoted;
219
    }
220
221 107
    public function fetchList(string $sql, array $params = []): array
222
    {
223
        // save query
224 107
        $this->queries[] = [
225 107
            'query' => $sql,
226 107
            'by_function' => 'fetchList',
227
        ];
228
229 107
        $stmt = $this->db->prepare($sql);
230 106
        $stmt->execute($params);
231 106
        $rows = $stmt->fetchAll();
232 106
        $stmt->closeCursor();
233
234 106
        return $rows;
235
    }
236
237
    /**
238
     * @return bool|array
239
     */
240 110
    public function fetchRow(string $sql, array $params = [])
241
    {
242
        // save query
243 110
        $this->queries[] = [
244 110
            'query' => $sql,
245 110
            'by_function' => 'fetchRow',
246
        ];
247
248 110
        $row = false;
249 110
        $stmt = $this->db->prepare($sql);
250 110
        $stmt->execute($params);
251 110
        $rows = $stmt->fetchAll();
252 110
        if (0 < \count($rows)) {
253 102
            $row = array_values($rows)[0];
254
        }
255 110
        $stmt->closeCursor();
256
257 110
        return $row;
258
    }
259
260 1
    public function getPDO()
261
    {
262 1
        return $this->db;
263
    }
264
265
    public function getErrorCode()
266
    {
267
        return $this->db->errorCode();
268
    }
269
270 28
    public function getErrorMessage()
271
    {
272 28
        return $this->db->errorInfo()[2];
273
    }
274
275
    public function getLastInsertId()
276
    {
277
        return $this->db->lastInsertId();
278
    }
279
280 8
    public function getNumberOfRows($sql)
281
    {
282
        // save query
283 8
        $this->queries[] = [
284 8
            'query' => $sql,
285 8
            'by_function' => 'getNumberOfRows',
286
        ];
287
288 8
        $stmt = $this->db->prepare($sql);
289 7
        $stmt->execute();
290 7
        $rowCount = \count($stmt->fetchAll());
291 7
        $stmt->closeCursor();
292
293 7
        return $rowCount;
294
    }
295
296 105
    public function simpleQuery(string $sql): bool
297
    {
298
        // save query
299 105
        $this->queries[] = [
300 105
            'query' => $sql,
301 105
            'by_function' => 'simpleQuery',
302
        ];
303
304 105
        $stmt = $this->db->prepare($sql);
305 104
        $stmt->execute();
306 104
        $this->lastRowCount = $stmt->rowCount();
307 104
        $stmt->closeCursor();
308
309 104
        return true;
310
    }
311
312
    /**
313
     * Encapsulates internal PDO::exec call.
314
     * This allows us to extend it, e.g. with caching functionality.
315
     *
316
     * @param string $sql
317
     *
318
     * @return int number of affected rows
319
     */
320 126
    public function exec($sql)
321
    {
322
        // save query
323 126
        $this->queries[] = [
324 126
            'query' => $sql,
325 126
            'by_function' => 'exec',
326
        ];
327
328 126
        return $this->db->exec($sql);
329
    }
330
331
    /**
332
     * @return int ID of new entry
333
     *
334
     * @throws Exception if invalid table name was given
335
     */
336 79
    public function insert(string $table, array $data): int
337
    {
338 79
        $columns = array_keys($data);
339
340
        // we reject fishy table names
341 79
        if (1 !== preg_match('/^[a-zA-Z0-9_]+$/i', $table)) {
342 1
            throw new Exception('Invalid table name given.');
343
        }
344
345
        /*
346
         * start building SQL
347
         */
348 78
        $sql = 'INSERT INTO '.$table.' ('.implode(', ', $columns);
349 78
        $sql .= ') VALUES (';
350
351
        // add placeholders for each value; collect values
352 78
        $placeholders = [];
353 78
        $params = [];
354 78
        foreach ($data as $v) {
355 78
            $placeholders[] = '?';
356 78
            $params[] = $v;
357
        }
358 78
        $sql .= implode(', ', $placeholders);
359
360 78
        $sql .= ')';
361
362
        /*
363
         * SQL looks like the following now:
364
         *
365
         *      INSERT INTO foo (bar) (?)
366
         */
367
368
        // Setup and run prepared statement
369 78
        $stmt = $this->db->prepare($sql);
370 78
        $stmt->execute($params);
371
372 78
        return $this->db->lastInsertId();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->db->lastInsertId() returns the type string which is incompatible with the type-hinted return integer.
Loading history...
373
    }
374
}
375