diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 7df2263..a524da6 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -45,6 +45,7 @@ abstract class AbstractException extends \Exception { "Db/Exception.savepointInvalid" => 10226, "Db/Exception.savepointStale" => 10227, "Db/Exception.resultReused" => 10228, + "Db/ExceptionRetry.schemaChange" => 10229, "Db/ExceptionInput.missing" => 10231, "Db/ExceptionInput.whitespace" => 10232, "Db/ExceptionInput.tooLong" => 10233, diff --git a/lib/Db/ExceptionRetry.php b/lib/Db/ExceptionRetry.php new file mode 100644 index 0000000..be4769a --- /dev/null +++ b/lib/Db/ExceptionRetry.php @@ -0,0 +1,10 @@ +exec("PRAGMA journal_mode = wal"); + } // turn off foreign keys $this->exec("PRAGMA foreign_keys = no"); // run the generic updater diff --git a/lib/Db/SQLite3/ExceptionBuilder.php b/lib/Db/SQLite3/ExceptionBuilder.php index 9e3bfff..c87e62f 100644 --- a/lib/Db/SQLite3/ExceptionBuilder.php +++ b/lib/Db/SQLite3/ExceptionBuilder.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db\SQLite3; use JKingWeb\Arsse\Db\Exception; +use JKingWeb\Arsse\Db\ExceptionRetry; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionTimeout; @@ -19,6 +20,8 @@ trait ExceptionBuilder { switch ($code) { case Driver::SQLITE_BUSY: return [ExceptionTimeout::class, 'general', $msg]; + case Driver::SQLITE_SCHEMA: + return [ExceptionRetry::class, 'schemaChange', $msg]; case Driver::SQLITE_CONSTRAINT: return [ExceptionInput::class, 'engineConstraintViolation', $msg]; case Driver::SQLITE_MISMATCH: diff --git a/lib/Db/SQLite3/PDODriver.php b/lib/Db/SQLite3/PDODriver.php index c36a3c1..b1cff19 100644 --- a/lib/Db/SQLite3/PDODriver.php +++ b/lib/Db/SQLite3/PDODriver.php @@ -11,9 +11,7 @@ use JKingWeb\Arsse\Db\Exception; use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionTimeout; -class PDODriver extends Driver { - use \JKingWeb\Arsse\Db\PDODriver; - +class PDODriver extends AbstractPDODriver { protected $db; public static function requirementsMet(): bool { @@ -49,4 +47,40 @@ class PDODriver extends Driver { public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { return new PDOStatement($this->db, $query, $paramTypes); } + + /** @codeCoverageIgnore */ + public function exec(string $query): bool { + // because PDO uses sqlite3_prepare() internally instead of sqlite3_prepare_v2(), + // we have to retry ourselves in cases of schema changes + // the SQLite3 class is not similarly affected + $attempts = 0; + retry: + try { + return parent::exec($query); + } catch (\JKingWeb\Arsse\Db\ExceptionRetry $e) { + if (++$attempts > 50) { + throw $e; + } else { + goto retry; + } + } + } + + /** @codeCoverageIgnore */ + public function query(string $query): \JKingWeb\Arsse\Db\Result { + // because PDO uses sqlite3_prepare() internally instead of sqlite3_prepare_v2(), + // we have to retry ourselves in cases of schema changes + // the SQLite3 class is not similarly affected + $attempts = 0; + retry: + try { + return parent::query($query); + } catch (\JKingWeb\Arsse\Db\ExceptionRetry $e) { + if (++$attempts > 50) { + throw $e; + } else { + goto retry; + } + } + } } diff --git a/lib/Db/SQLite3/PDOStatement.php b/lib/Db/SQLite3/PDOStatement.php index 7e7642d..166fe31 100644 --- a/lib/Db/SQLite3/PDOStatement.php +++ b/lib/Db/SQLite3/PDOStatement.php @@ -9,4 +9,23 @@ namespace JKingWeb\Arsse\Db\SQLite3; class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement { use ExceptionBuilder; use \JKingWeb\Arsse\Db\PDOError; + + /** @codeCoverageIgnore */ + public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { + // because PDO uses sqlite3_prepare() internally instead of sqlite3_prepare_v2(), + // we have to retry ourselves in cases of schema changes + // the SQLite3 class is not similarly affected + $attempts = 0; + retry: + try { + return parent::runArray($values); + } catch (\JKingWeb\Arsse\Db\ExceptionRetry $e) { + if (++$attempts > 50) { + throw $e; + } else { + $this->st = $this->db->prepare($this->st->queryString); + goto retry; + } + } + } } diff --git a/locale/en.php b/locale/en.php index 5e8ad0f..ddbf118 100644 --- a/locale/en.php +++ b/locale/en.php @@ -120,6 +120,7 @@ return [ 'Exception.JKingWeb/Arsse/Db/Exception.savepointStale' => 'Tried to {action} stale savepoint {index}', // indicates programming error 'Exception.JKingWeb/Arsse/Db/Exception.resultReused' => 'Result set already iterated', + 'Exception.JKingWeb/Arsse/Db/ExceptionRetry.schemaChange' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.missing' => 'Required field "{field}" missing while performing action "{action}"', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.whitespace' => 'Field "{field}" of action "{action}" may not contain only whitespace', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.tooLong' => 'Field "{field}" of action "{action}" has a maximum length of {max}', diff --git a/sql/SQLite3/0.sql b/sql/SQLite3/0.sql index 7a9dea6..5466629 100644 --- a/sql/SQLite3/0.sql +++ b/sql/SQLite3/0.sql @@ -2,9 +2,6 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details --- Make the database WAL-journalled; this is persitent -PRAGMA journal_mode = wal; - create table arsse_meta( -- application metadata key text primary key not null, -- metadata key diff --git a/tests/cases/DatabaseDrivers/SQLite3.php b/tests/cases/DatabaseDrivers/SQLite3.php index e927d41..880539a 100644 --- a/tests/cases/DatabaseDrivers/SQLite3.php +++ b/tests/cases/DatabaseDrivers/SQLite3.php @@ -28,7 +28,7 @@ trait SQLite3 { } public static function dbTableList($db): array { - $listTables = "SELECT name from sqlite_master where type = 'table' and name like 'arsse_%'"; + $listTables = "SELECT name from sqlite_master where type = 'table' and name like 'arsse^_%' escape '^'"; if ($db instanceof Driver) { $tables = $db->query($listTables)->getAll(); $tables = sizeof($tables) ? array_column($tables, "name") : [];