Browse Source

Work around various SQLite-related problems

- WAL mode was not getting set properly
- Queries using the PDO driver could fail because PDO sucks
J. King 2 weeks ago
parent
commit
6000d80b7b

+ 1
- 0
lib/AbstractException.php View File

@@ -45,6 +45,7 @@ abstract class AbstractException extends \Exception {
45 45
         "Db/Exception.savepointInvalid"               => 10226,
46 46
         "Db/Exception.savepointStale"                 => 10227,
47 47
         "Db/Exception.resultReused"                   => 10228,
48
+        "Db/ExceptionRetry.schemaChange"              => 10229,
48 49
         "Db/ExceptionInput.missing"                   => 10231,
49 50
         "Db/ExceptionInput.whitespace"                => 10232,
50 51
         "Db/ExceptionInput.tooLong"                   => 10233,

+ 10
- 0
lib/Db/ExceptionRetry.php View File

@@ -0,0 +1,10 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\Db;
8
+
9
+class ExceptionRetry extends \JKingWeb\Arsse\AbstractException {
10
+}

+ 11
- 0
lib/Db/SQLite3/AbstractPDODriver.php View File

@@ -0,0 +1,11 @@
1
+<?php
2
+/** @license MIT
3
+ * Copyright 2017 J. King, Dustin Wilson et al.
4
+ * See LICENSE and AUTHORS files for details */
5
+
6
+declare(strict_types=1);
7
+namespace JKingWeb\Arsse\Db\SQLite3;
8
+
9
+abstract class AbstractPDODriver extends Driver {
10
+    use \JKingWeb\Arsse\Db\PDODriver;
11
+}

+ 5
- 0
lib/Db/SQLite3/Driver.php View File

@@ -17,6 +17,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
17 17
     const TRANSACTIONAL_LOCKS = true;
18 18
 
19 19
     const SQLITE_BUSY = 5;
20
+    const SQLITE_SCHEMA = 17;
20 21
     const SQLITE_CONSTRAINT = 19;
21 22
     const SQLITE_MISMATCH = 20;
22 23
 
@@ -122,6 +123,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
122 123
     }
123 124
 
124 125
     public function schemaUpdate(int $to, string $basePath = null): bool {
126
+        if ($to == 1) {
127
+            // if we're initializing the database for the first time, switch to WAL mode
128
+            $this->exec("PRAGMA journal_mode = wal");
129
+        }
125 130
         // turn off foreign keys
126 131
         $this->exec("PRAGMA foreign_keys = no");
127 132
         // run the generic updater

+ 3
- 0
lib/Db/SQLite3/ExceptionBuilder.php View File

@@ -7,6 +7,7 @@ declare(strict_types=1);
7 7
 namespace JKingWeb\Arsse\Db\SQLite3;
8 8
 
9 9
 use JKingWeb\Arsse\Db\Exception;
10
+use JKingWeb\Arsse\Db\ExceptionRetry;
10 11
 use JKingWeb\Arsse\Db\ExceptionInput;
11 12
 use JKingWeb\Arsse\Db\ExceptionTimeout;
12 13
 
@@ -19,6 +20,8 @@ trait ExceptionBuilder {
19 20
         switch ($code) {
20 21
             case Driver::SQLITE_BUSY:
21 22
                 return [ExceptionTimeout::class, 'general', $msg];
23
+            case Driver::SQLITE_SCHEMA:
24
+                return [ExceptionRetry::class, 'schemaChange', $msg];
22 25
             case Driver::SQLITE_CONSTRAINT:
23 26
                 return [ExceptionInput::class, 'engineConstraintViolation', $msg];
24 27
             case Driver::SQLITE_MISMATCH:

+ 37
- 3
lib/Db/SQLite3/PDODriver.php View File

@@ -11,9 +11,7 @@ use JKingWeb\Arsse\Db\Exception;
11 11
 use JKingWeb\Arsse\Db\ExceptionInput;
12 12
 use JKingWeb\Arsse\Db\ExceptionTimeout;
13 13
 
14
-class PDODriver extends Driver {
15
-    use \JKingWeb\Arsse\Db\PDODriver;
16
-
14
+class PDODriver extends AbstractPDODriver {
17 15
     protected $db;
18 16
 
19 17
     public static function requirementsMet(): bool {
@@ -49,4 +47,40 @@ class PDODriver extends Driver {
49 47
     public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
50 48
         return new PDOStatement($this->db, $query, $paramTypes);
51 49
     }
50
+
51
+    /** @codeCoverageIgnore */
52
+    public function exec(string $query): bool {
53
+        // because PDO uses sqlite3_prepare() internally instead of sqlite3_prepare_v2(), 
54
+        // we have to retry ourselves in cases of schema changes
55
+        // the SQLite3 class is not similarly affected
56
+        $attempts = 0;
57
+        retry:
58
+        try {
59
+            return parent::exec($query);
60
+        } catch (\JKingWeb\Arsse\Db\ExceptionRetry $e) {
61
+            if (++$attempts > 50) {
62
+                throw $e;
63
+            } else {
64
+                goto retry;
65
+            }
66
+        }
67
+    }
68
+
69
+    /** @codeCoverageIgnore */
70
+    public function query(string $query): \JKingWeb\Arsse\Db\Result {
71
+        // because PDO uses sqlite3_prepare() internally instead of sqlite3_prepare_v2(), 
72
+        // we have to retry ourselves in cases of schema changes
73
+        // the SQLite3 class is not similarly affected
74
+        $attempts = 0;
75
+        retry:
76
+        try {
77
+            return parent::query($query);
78
+        } catch (\JKingWeb\Arsse\Db\ExceptionRetry $e) {
79
+            if (++$attempts > 50) {
80
+                throw $e;
81
+            } else {
82
+                goto retry;
83
+            }
84
+        }
85
+    }
52 86
 }

+ 19
- 0
lib/Db/SQLite3/PDOStatement.php View File

@@ -9,4 +9,23 @@ namespace JKingWeb\Arsse\Db\SQLite3;
9 9
 class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {
10 10
     use ExceptionBuilder;
11 11
     use \JKingWeb\Arsse\Db\PDOError;
12
+
13
+    /** @codeCoverageIgnore */
14
+    public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
15
+        // because PDO uses sqlite3_prepare() internally instead of sqlite3_prepare_v2(), 
16
+        // we have to retry ourselves in cases of schema changes
17
+        // the SQLite3 class is not similarly affected
18
+        $attempts = 0;
19
+        retry:
20
+        try {
21
+            return parent::runArray($values);
22
+        } catch (\JKingWeb\Arsse\Db\ExceptionRetry $e) {
23
+            if (++$attempts > 50) {
24
+                throw $e;
25
+            } else {
26
+                $this->st = $this->db->prepare($this->st->queryString);
27
+                goto retry;
28
+            }
29
+        }
30
+    }
12 31
 }

+ 1
- 0
locale/en.php View File

@@ -120,6 +120,7 @@ return [
120 120
     'Exception.JKingWeb/Arsse/Db/Exception.savepointStale'                 => 'Tried to {action} stale savepoint {index}',
121 121
     // indicates programming error
122 122
     'Exception.JKingWeb/Arsse/Db/Exception.resultReused'                   => 'Result set already iterated',
123
+    'Exception.JKingWeb/Arsse/Db/ExceptionRetry.schemaChange'              => '{0}',
123 124
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing'                   => 'Required field "{field}" missing while performing action "{action}"',
124 125
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace'                => 'Field "{field}" of action "{action}" may not contain only whitespace',
125 126
     'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong'                   => 'Field "{field}" of action "{action}" has a maximum length of {max}',

+ 0
- 2
sql/SQLite3/0.sql View File

@@ -2,9 +2,6 @@
2 2
 -- Copyright 2017 J. King, Dustin Wilson et al.
3 3
 -- See LICENSE and AUTHORS files for details
4 4
 
5
-PRAGMA journal_mode = wal;
6
-
7 5
 create table arsse_meta(
8 6
 -- application metadata
9 7
     key text primary key not null,                                                                          -- metadata key

+ 1
- 1
tests/cases/DatabaseDrivers/SQLite3.php View File

@@ -28,7 +28,7 @@ trait SQLite3 {
28 28
     }
29 29
     
30 30
     public static function dbTableList($db): array {
31
-        $listTables = "SELECT name from sqlite_master where type = 'table' and name like 'arsse_%'";
31
+        $listTables = "SELECT name from sqlite_master where type = 'table' and name like 'arsse^_%' escape '^'";
32 32
         if ($db instanceof Driver) {
33 33
             $tables = $db->query($listTables)->getAll();
34 34
             $tables = sizeof($tables) ? array_column($tables, "name") : [];

Loading…
Cancel
Save