Issues (55)

src/PDOSQLiteAdapter.php (3 issues)

1
<?php
2
3
/**
4
 * This file is part of the sweetrdf/InMemoryStoreSqlite package and licensed under
5
 * the terms of the GPL-2 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 113
    public function __construct()
34
    {
35 113
        $this->checkRequirements();
36
37
        // use in-memory
38 113
        $dsn = 'sqlite::memory:';
39
40 113
        $this->db = new PDO($dsn);
41
42 113
        $this->db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
43
44
        // errors lead to exceptions
45 113
        $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
46
47
        // default fetch mode is associative
48 113
        $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
49
50
        /*
51
         * These PRAGMAs may speed up insert operations a bit.
52
         * Because database runs exclusively in memory for a process
53
         * journal mode etc. is not relevant.
54
         */
55 113
        $this->db->query('PRAGMA synchronous = OFF;');
56 113
        $this->db->query('PRAGMA journal_mode = OFF;');
57 113
        $this->db->query('PRAGMA locking_mode = EXCLUSIVE;');
58 113
        $this->db->query('PRAGMA page_size = 4096;');
59
60
        /*
61
         * define CONCAT function (otherwise SQLite will throw an exception)
62
         */
63 113
        $this->db->sqliteCreateFunction('CONCAT', function ($pattern, $string) {
64 2
            $result = '';
65
66 2
            foreach (\func_get_args() as $str) {
67 2
                $result .= $str;
68
            }
69
70 2
            return $result;
71 113
        });
72
73
        /*
74
         * define REGEXP function (otherwise SQLite will throw an exception)
75
         */
76 113
        $this->db->sqliteCreateFunction('REGEXP', function ($pattern, $string) {
77 1
            if (0 < preg_match('/'.$pattern.'/i', $string)) {
78 1
                return true;
79
            }
80
81 1
            return false;
82 113
        }, 2);
83
84 113
        $this->createTables();
85
    }
86
87 113
    public function checkRequirements()
88
    {
89 113
        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...
90
            throw new Exception('Extension pdo_sqlite is not loaded.');
91
        }
92
    }
93
94
    public function deleteAllTables(): void
95
    {
96
        $this->exec(
97
            'SELECT "drop table " || name || ";"
98
               FROM sqlite_master
99
              WHERE type = "table";'
100
        );
101
    }
102
103
    /**
104
     * Creates all required tables.
105
     */
106 113
    private function createTables(): void
107
    {
108
        // triple
109 113
        $sql = 'CREATE TABLE IF NOT EXISTS triple (
110
            t INTEGER PRIMARY KEY AUTOINCREMENT,
111
            s INTEGER UNSIGNED NOT NULL,
112
            p INTEGER UNSIGNED NOT NULL,
113
            o INTEGER UNSIGNED NOT NULL,
114
            o_lang_dt INTEGER UNSIGNED NOT NULL,
115
            o_comp TEXT NOT NULL,                       -- normalized value for ORDER BY operations
116
            s_type INTEGER UNSIGNED NOT NULL DEFAULT 0, -- uri/bnode => 0/1
117
            o_type INTEGER UNSIGNED NOT NULL DEFAULT 0  -- uri/bnode/literal => 0/1/2
118 113
        )';
119
120 113
        $this->exec($sql);
121
122
        // g2t
123 113
        $sql = 'CREATE TABLE IF NOT EXISTS g2t (
124
            g INTEGER UNSIGNED NOT NULL,
125
            t INTEGER UNSIGNED NOT NULL,
126
            UNIQUE (g,t)
127 113
        )';
128
129 113
        $this->exec($sql);
130
131
        // id2val
132 113
        $sql = 'CREATE TABLE IF NOT EXISTS id2val (
133
            id INTEGER PRIMARY KEY AUTOINCREMENT,
134
            val TEXT NOT NULL,
135
            val_type INTEGER NOT NULL DEFAULT 0, -- uri/bnode/literal => 0/1/2
136
            UNIQUE (id,val_type)
137 113
        )';
138
139 113
        $this->exec($sql);
140
141
        // s2val
142 113
        $sql = 'CREATE TABLE IF NOT EXISTS s2val (
143
            id INTEGER UNSIGNED NOT NULL,
144
            val_hash TEXT NOT NULL,
145
            val TEXT NOT NULL,
146
            UNIQUE (id)
147 113
        )';
148
149 113
        $this->exec($sql);
150
151
        // o2val
152 113
        $sql = 'CREATE TABLE IF NOT EXISTS o2val (
153
            id INTEGER NOT NULL,
154
            val_hash TEXT NOT NULL,
155
            val TEXT NOT NULL,
156
            UNIQUE (id)
157 113
        )';
158
159 113
        $this->exec($sql);
160
    }
161
162
    /**
163
     * It gets all tables from the current database.
164
     */
165 1
    public function getAllTables(): array
166
    {
167 1
        $tables = $this->fetchList('SELECT name FROM sqlite_master WHERE type="table";');
168 1
        $result = [];
169 1
        foreach ($tables as $table) {
170
            // ignore SQLite tables
171 1
            if (false !== strpos($table['name'], 'sqlite_')) {
172 1
                continue;
173
            }
174 1
            $result[] = $table['name'];
175
        }
176
177 1
        return $result;
178
    }
179
180 2
    public function getServerVersion()
181
    {
182 2
        return $this->fetchRow('select sqlite_version()')['sqlite_version()'];
183
    }
184
185
    public function getAffectedRows(): int
186
    {
187
        return $this->lastRowCount;
188
    }
189
190
    /**
191
     * @return void
192
     */
193 1
    public function disconnect()
194
    {
195
        // FYI: https://stackoverflow.com/questions/18277233/pdo-closing-connection
196 1
        $this->db = null;
197
    }
198
199 9
    public function escape($value)
200
    {
201 9
        $quoted = $this->db->quote($value);
0 ignored issues
show
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

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