Browse Source

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
microsub
J. King 6 years ago
parent
commit
7ca0f4e877
  1. 2
      lib/Database.php
  2. 52
      lib/Db/AbstractDriver.php
  3. 4
      lib/Db/Driver.php
  4. 77
      lib/Db/SQLite3/Driver.php

2
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);

52
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);
}

4
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

77
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;

Loading…
Cancel
Save