From 7ca0f4e877f61973083fcb543f9c07ec9e5d9a3b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 18 Dec 2017 18:29:32 -0500 Subject: [PATCH] Make the SQLite3 driver more generic The changes in this commit should make it more practical to: - Allow the driver to decide for itself whether to try creating a PDO object if its own requirements are not met - Have any driver use a generic schema update procedure - Use the same constructor for native and PDO SQLite --- lib/Database.php | 2 +- lib/Db/AbstractDriver.php | 52 ++++++++++++++++++++++++-- lib/Db/Driver.php | 4 +- lib/Db/SQLite3/Driver.php | 77 ++++++++++++++++----------------------- 4 files changed, 83 insertions(+), 52 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index fdede552..0072cd86 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -27,7 +27,7 @@ class Database { public function __construct($initialize = true) { $driver = Arsse::$conf->dbDriver; - $this->db = new $driver(); + $this->db = $driver::create(); $ver = $this->db->schemaVersion(); if ($initialize && $ver < self::SCHEMA_VERSION) { $this->db->schemaUpdate(self::SCHEMA_VERSION); diff --git a/lib/Db/AbstractDriver.php b/lib/Db/AbstractDriver.php index 74fc25dc..1f106f9a 100644 --- a/lib/Db/AbstractDriver.php +++ b/lib/Db/AbstractDriver.php @@ -6,15 +6,13 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db; +use JKingWeb\Arsse\Arsse; + abstract class AbstractDriver implements Driver { protected $locked = false; protected $transDepth = 0; protected $transStatus = []; - abstract public function prepareArray(string $query, array $paramTypes): Statement; - abstract protected function lock(): bool; - abstract protected function unlock(bool $rollback = false) : bool; - /** @codeCoverageIgnore */ public function schemaVersion(): int { // FIXME: generic schemaVersion() will need to be covered for database engines other than SQLite @@ -25,6 +23,52 @@ abstract class AbstractDriver implements Driver { } } + public function schemaUpdate(int $to, string $basePath = null): bool { + $ver = $this->schemaVersion(); + if (!Arsse::$conf->dbAutoUpdate) { + throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]); + } elseif ($ver >= $to) { + throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); + } + $sep = \DIRECTORY_SEPARATOR; + $path = ($basePath ?? \JKingWeb\Arsse\BASE."sql").$sep.static::schemaID().$sep; + // lock the database + $this->savepointCreate(true); + for ($a = $this->schemaVersion(); $a < $to; $a++) { + $this->savepointCreate(); + try { + $file = $path.$a.".sql"; + if (!file_exists($file)) { + throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + } elseif (!is_readable($file)) { + throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + } + $sql = @file_get_contents($file); + if ($sql===false) { + throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); // @codeCoverageIgnore + } + try { + $this->exec($sql); + } catch (\Throwable $e) { + throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $this->getError()]); + } + if ($this->schemaVersion() != $a+1) { + throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); + } + } catch (\Throwable $e) { + // undo any partial changes from the failed update + $this->savepointUndo(); + // commit any successful updates if updating by more than one version + $this->savepointRelease(); + // throw the error received + throw $e; + } + $this->savepointRelease(); + } + $this->savepointRelease(); + return true; + } + public function begin(bool $lock = false): Transaction { return new Transaction($this, $lock); } diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index 5485458f..bf3bb28d 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -13,11 +13,13 @@ interface Driver { const TR_PEND_COMMIT = -1; const TR_PEND_ROLLBACK = -2; - public function __construct(); + public static function create(): Driver; // returns a human-friendly name for the driver (for display in installer, for example) public static function driverName(): string; // returns the version of the scheme of the opened database; if uninitialized should return 0 public function schemaVersion(): int; + // returns the schema set to be used for database set-up + public static function schemaID(): string; // return a Transaction object public function begin(bool $lock = false): Transaction; // manually begin a real or synthetic transactions, with real or synthetic nesting diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 7c78c787..a2eace41 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -22,7 +22,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function __construct(string $dbFile = null) { // check to make sure required extension is loaded - if (!class_exists("SQLite3")) { + if (!self::requirementsMet()) { throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore } // if no database file is specified in the configuration, use a suitable default @@ -30,9 +30,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $mode = \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE; $timeout = Arsse::$conf->dbSQLite3Timeout * 1000; try { - $this->db = $this->makeConnection($dbFile, $mode, Arsse::$conf->dbSQLite3Key); - // enable exceptions - $this->db->enableExceptions(true); + $this->makeConnection($dbFile, $mode, Arsse::$conf->dbSQLite3Key); // set the timeout; parameters are not allowed for pragmas, but this usage should be safe $this->exec("PRAGMA busy_timeout = $timeout"); // set other initial options @@ -60,8 +58,14 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } } + public static function requirementsMet(): bool { + return class_exists("SQLite3"); + } + protected function makeConnection(string $file, int $opts, string $key) { - return new \SQLite3($file, $opts, $key); + $this->db = new \SQLite3($file, $opts, $key); + // enable exceptions + $this->db->enableExceptions(true); } public function __destruct() { @@ -72,60 +76,41 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { unset($this->db); } + public static function create(): \JKingWeb\Arsse\Db\Driver { + if (self::requirementsMet()) { + return new self; + } elseif (PDODriver::requirementsMet()) { + return new PDODriver; + } else { + throw new Exception("extMissing", self::driverName()); + } + } + public static function driverName(): string { return Arsse::$lang->msg("Driver.Db.SQLite3.Name"); } + public static function schemaID(): string { + return "SQLite3"; + } + public function schemaVersion(): int { return $this->query("PRAGMA user_version")->getValue(); } public function schemaUpdate(int $to, string $basePath = null): bool { - $ver = $this->schemaVersion(); - if (!Arsse::$conf->dbAutoUpdate) { - throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]); - } elseif ($ver >= $to) { - throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]); - } - $sep = \DIRECTORY_SEPARATOR; - $path = ($basePath ?? \JKingWeb\Arsse\BASE."sql").$sep."SQLite3".$sep; // turn off foreign keys $this->exec("PRAGMA foreign_keys = no"); - // lock the database - $this->savepointCreate(true); - for ($a = $this->schemaVersion(); $a < $to; $a++) { - $this->savepointCreate(); - try { - $file = $path.$a.".sql"; - if (!file_exists($file)) { - throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); - } elseif (!is_readable($file)) { - throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); - } - $sql = @file_get_contents($file); - if ($sql===false) { - throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); // @codeCoverageIgnore - } - try { - $this->exec($sql); - } catch (\Throwable $e) { - throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $this->getError()]); - } - if ($this->schemaVersion() != $a+1) { - throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); - } - } catch (\Throwable $e) { - // undo any partial changes from the failed update - $this->savepointUndo(); - // commit any successful updates if updating by more than one version - $this->savepointRelease(); - // throw the error received - throw $e; - } - $this->savepointRelease(); + // run the generic updater + try { + parent::schemaUpdate($to, $basePath); + } catch (\Throwable $e) { + // turn foreign keys back on + $this->exec("PRAGMA foreign_keys = yes"); + // pass the exception up + throw $e; } - $this->savepointRelease(); // turn foreign keys back on $this->exec("PRAGMA foreign_keys = yes"); return true;