From c4a41255b09dd08d1e6257ecd9a6b07c87cec787 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 10 Jan 2019 19:01:32 -0500 Subject: [PATCH] Experimental native MySQL driver No testing has been performed yet, but changes are extensive enough to warrant a commit. Of particular note: - SQL states are enumerated in a separate trait to reduce duplication - PDOStatement is now an abstract class to avoid duplication of engine-specific error handling - Error handling has been cleaned up somewhat --- lib/Db/AbstractDriver.php | 6 +- lib/Db/AbstractStatement.php | 3 + lib/Db/MySQL/Driver.php | 57 +++++++++++++-- lib/Db/MySQL/ExceptionBuilder.php | 30 ++++++++ lib/Db/MySQL/PDODriver.php | 1 - lib/Db/MySQL/PDOStatement.php | 15 ++-- lib/Db/MySQL/Result.php | 51 +++++++++++++ lib/Db/MySQL/Statement.php | 107 ++++++++++++++++++++++++++++ lib/Db/PDODriver.php | 8 +-- lib/Db/PDOError.php | 51 ++----------- lib/Db/PDOStatement.php | 6 +- lib/Db/PostgreSQL/Dispatch.php | 26 ++----- lib/Db/PostgreSQL/Driver.php | 9 +-- lib/Db/PostgreSQL/PDOStatement.php | 7 +- lib/Db/PostgreSQL/Statement.php | 1 + lib/Db/ResultEmpty.php | 12 +++- lib/Db/SQLState.php | 31 ++++++++ lib/Db/SQLite3/Driver.php | 10 +-- lib/Db/SQLite3/ExceptionBuilder.php | 22 +++--- lib/Db/SQLite3/PDODriver.php | 4 ++ lib/Db/SQLite3/PDOStatement.php | 12 ++++ lib/Db/SQLite3/Statement.php | 4 +- tests/lib/DatabaseInformation.php | 2 +- 23 files changed, 353 insertions(+), 122 deletions(-) create mode 100644 lib/Db/MySQL/ExceptionBuilder.php create mode 100644 lib/Db/MySQL/Result.php create mode 100644 lib/Db/MySQL/Statement.php create mode 100644 lib/Db/SQLState.php create mode 100644 lib/Db/SQLite3/PDOStatement.php diff --git a/lib/Db/AbstractDriver.php b/lib/Db/AbstractDriver.php index 7ce30e0..682d741 100644 --- a/lib/Db/AbstractDriver.php +++ b/lib/Db/AbstractDriver.php @@ -9,13 +9,15 @@ namespace JKingWeb\Arsse\Db; use JKingWeb\Arsse\Arsse; abstract class AbstractDriver implements Driver { + use SQLState; + protected $locked = false; protected $transDepth = 0; protected $transStatus = []; abstract protected function lock(): bool; abstract protected function unlock(bool $rollback = false): bool; - abstract protected function getError(): string; + abstract protected static function buildEngineException($code, string $msg): array; public function schemaUpdate(int $to, string $basePath = null): bool { $ver = $this->schemaVersion(); @@ -46,7 +48,7 @@ abstract class AbstractDriver implements Driver { try { $this->exec($sql); } catch (\Throwable $e) { - throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $this->getError()]); + throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $e->getMessage()]); } if ($this->schemaVersion() != $a+1) { throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index 55c6cf6..1dc990f 100644 --- a/lib/Db/AbstractStatement.php +++ b/lib/Db/AbstractStatement.php @@ -10,12 +10,15 @@ use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo; abstract class AbstractStatement implements Statement { + use SQLState; + protected $types = []; protected $isNullable = []; abstract public function runArray(array $values = []): Result; abstract protected function bindValue($value, string $type, int $position): bool; abstract protected function prepare(string $query): bool; + abstract protected static function buildEngineException($code, string $msg): array; public function run(...$values): Result { return $this->runArray($values); diff --git a/lib/Db/MySQL/Driver.php b/lib/Db/MySQL/Driver.php index dba104f..984542d 100644 --- a/lib/Db/MySQL/Driver.php +++ b/lib/Db/MySQL/Driver.php @@ -13,11 +13,15 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionTimeout; class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { + use ExceptionBuilder; + const SQL_MODE = "ANSI_QUOTES,HIGH_NOT_PRECEDENCE,NO_BACKSLASH_ESCAPES,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,STRICT_ALL_TABLES"; const TRANSACTIONAL_LOCKS = false; + /** @var \mysql */ protected $db; protected $transStart = 0; + protected $packetSize = 4194304; public function __construct() { // check to make sure required extension is loaded @@ -26,7 +30,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } $host = Arsse::$conf->dbMySQLHost; if ($host[0] == "/") { - // host is a socket + // host is a Unix socket $socket = $host; $host = ""; } elseif(substr($host, 0, 9) == "\\\\.\\pipe\\") { @@ -38,9 +42,22 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $pass = Arsse::$conf->dbMySQLPass ?? ""; $port = Arsse::$conf->dbMySQLPost ?? 3306; $db = Arsse::$conf->dbMySQLDb ?? "arsse"; + // make the connection $this->makeConnection($user, $pass, $db, $host, $port, $socket ?? ""); - $this->exec("SET lock_wait_timeout = 1"); - $this->exec("SET time_zone = '+00:00'"); + // set session variables + foreach (static::makeSetupQueries() as $q) { + $this->exec($q); + } + // get the maximum packet size; parameter strings larger than this size need to be chunked + $this->packetSize = $this->query("select variable_value from performance_schema.session_variables where variable_name = 'max_allowed_packet'")->getValue(); + } + + public static function makeSetupQueries(): array { + return [ + "SET sql_mode = '".self::SQL_MODE."'", + "SET time_zone = '+00:00'", + "SET lock_wait_timeout = 1", + ]; } /** @codeCoverageIgnore */ @@ -148,17 +165,47 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket) { + try { + $this->db = new \mysqli($host, $user, $password, $db, $port, $socket); + if ($this->db->connect_errno) { + echo $this->db->connect_errno.": ".$this->db->connect_error; + } + $this->db->set_character_set("utf8mb4"); + } catch (\Exception $e) { + throw $e; + } } - protected function getError(): string { + public function exec(string $query): bool { + $this->dispatch($query); + return true; } - public function exec(string $query): bool { + protected function dispatch(string $query) { + $r = $this->db->query($query); + if ($this->db->sqlstate != "00000") { + if ($this->db->sqlstate == "HY000") { + list($excClass, $excMsg, $excData) = $this->buildEngineException($this->db->errno, $this->db->error); + } else { + list($excClass, $excMsg, $excData) = $this->buildStandardException($this->db->sqlstate, $this->db->error); + } + throw new $excClass($excMsg, $excData); + } + return $r; } public function query(string $query): \JKingWeb\Arsse\Db\Result { + $r = $this->dispatch($query); + $rows = (int) $this->db->affected_rows; + $id = (int) $this->db->insert_id; + if ($r === true) { + return new \JKingWeb\Arsse\Db\ResultEmpty($rows, $id); + } else { + return new ResultE($r, [$rows, $id]); + } } public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { + return new Statement($this->db, $query, $paramTypes, $this->packetSize); } } diff --git a/lib/Db/MySQL/ExceptionBuilder.php b/lib/Db/MySQL/ExceptionBuilder.php new file mode 100644 index 0000000..b801d7d --- /dev/null +++ b/lib/Db/MySQL/ExceptionBuilder.php @@ -0,0 +1,30 @@ +db->errno, $this->db->error); + } + + public static function buildEngineException($code, string $msg): array { + switch ($code) { + case 1205: + return [ExceptionTimeout::class, 'general', $msg]; + case 1364: + return [ExceptionInput::class, "constraintViolation", $msg]; + case 1366: + return [ExceptionInput::class, 'engineTypeViolation', $msg]; + default: + return [Exception::class, 'engineErrorGeneral', $msg]; + } + } +} diff --git a/lib/Db/MySQL/PDODriver.php b/lib/Db/MySQL/PDODriver.php index b3939dc..abc97f4 100644 --- a/lib/Db/MySQL/PDODriver.php +++ b/lib/Db/MySQL/PDODriver.php @@ -33,7 +33,6 @@ class PDODriver extends Driver { $dsn = "mysql:".implode(";", $dsn); $this->db = new \PDO($dsn, $user, $password, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - \PDO::MYSQL_ATTR_INIT_COMMAND => "SET sql_mode = '".self::SQL_MODE."'", ]); } diff --git a/lib/Db/MySQL/PDOStatement.php b/lib/Db/MySQL/PDOStatement.php index f388de4..dd405a2 100644 --- a/lib/Db/MySQL/PDOStatement.php +++ b/lib/Db/MySQL/PDOStatement.php @@ -6,16 +6,11 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db\MySQL; -class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement { +class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement { + use ExceptionBuilder; + use \JKingWeb\Arsse\Db\PDOError; + public static function mungeQuery(string $query, array $types, ...$extraData): string { - $query = explode("?", $query); - $out = ""; - for ($b = 1; $b < sizeof($query); $b++) { - $a = $b - 1; - $mark = (($types[$a] ?? "") == "datetime") ? "cast(? as datetime(0))" : "?"; - $out .= $query[$a].$mark; - } - $out .= array_pop($query); - return $out; + return Statement::mungeQuery($query, $types); } } diff --git a/lib/Db/MySQL/Result.php b/lib/Db/MySQL/Result.php new file mode 100644 index 0000000..d96c7eb --- /dev/null +++ b/lib/Db/MySQL/Result.php @@ -0,0 +1,51 @@ +rows; + } + + public function lastId(): int { + return $this->id; + } + + // constructor/destructor + + public function __construct(\mysqli_result $result, array $changes = [0,0], Statement $statement = null) { + $this->st = $statement; //keeps the statement from being destroyed, invalidating the result set + $this->set = $result; + $this->rows = $changes[0]; + $this->id = $changes[1]; + } + + public function __destruct() { + try { + $this->set->free(); + } catch (\Throwable $e) { // @codeCoverageIgnore + } + unset($this->set); + } + + // PHP iterator methods + + public function valid() { + $this->cur = $this->set->fetch_assoc(); + return ($this->cur !== null); + } +} diff --git a/lib/Db/MySQL/Statement.php b/lib/Db/MySQL/Statement.php new file mode 100644 index 0000000..342c6f4 --- /dev/null +++ b/lib/Db/MySQL/Statement.php @@ -0,0 +1,107 @@ + "i", + "float" => "d", + "datetime" => "s", + "binary" => "b", + "string" => "s", + "boolean" => "i", + ]; + + protected $db; + protected $st; + protected $query; + protected $packetSize; + + protected $values; + protected $types; + + public function __construct(\mysqli $db, string $query, array $bindings = [], int $packetSize) { + $this->db = $db; + $this->query = $query; + $this->packetSize = $packetSize; + $this->retypeArray($bindings); + } + + protected function prepare(string $query): bool { + $this->st = $this->db->prepare($query); + if (!$this->st) { + list($excClass, $excMsg, $excData) = $this->buildEngineException($this->db->errno, $this->db->error); + throw new $excClass($excMsg, $excData); + } + return true; + } + + public function __destruct() { + try { + $this->st->close(); + } catch (\Throwable $e) { // @codeCoverageIgnore + } + unset($this->st); + } + + public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { + $this->st->reset(); + // prepare values and them all at once + $this->bindValues($values); + $this->st->bind_params($this->types, ...$this->values); + // execute the statement + $this->st->execute(); + // clear normalized values + $this->types = ""; + $this->values = []; + // check for errors + if ($this->st->sqlstate != "00000") { + if ($this->st->sqlstate == "HY000") { + list($excClass, $excMsg, $excData) = $this->buildEngineException($this->st->errno, $this->st->error); + } else { + list($excClass, $excMsg, $excData) = $this->buildStandardException($this->st->sqlstate, $this->st->error); + } + throw new $excClass($excMsg, $excData); + } + // create a result-set instance + $r = $this->st->get_result(); + $changes = $this->st->affected_rows; + $lastId = $this->st->insert_id; + return new Result($r, [$changes, $lastId], $this); + } + + protected function bindValue($value, string $type, int $position): bool { + // this is a bit of a hack: we collect values (and MySQL bind types) here so that we can take + // advantage of the work done by bindValues() even though MySQL requires everything to be bound + // all at once; we also packetize large values here if necessary + if (is_string($value) && strlen($value) > $this->packetSize) { + $this->values[] = null; + $this->st->send_long_data($position - 1, $value); + } else { + $this->values[] = $value; + $this->types .= self::BINDINGS[$type]; + } + return true; + } + public static function mungeQuery(string $query, array $types, ...$extraData): string { + $query = explode("?", $query); + $out = ""; + for ($b = 1; $b < sizeof($query); $b++) { + $a = $b - 1; + $mark = (($types[$a] ?? "") == "datetime") ? "cast(? as datetime(0))" : "?"; + $out .= $query[$a].$mark; + } + $out .= array_pop($query); + return $out; + } +} diff --git a/lib/Db/PDODriver.php b/lib/Db/PDODriver.php index 7b4ca9a..c5b4f4d 100644 --- a/lib/Db/PDODriver.php +++ b/lib/Db/PDODriver.php @@ -14,7 +14,7 @@ trait PDODriver { $this->db->exec($query); return true; } catch (\PDOException $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + list($excClass, $excMsg, $excData) = $this->buildPDOException(); throw new $excClass($excMsg, $excData); } } @@ -23,13 +23,9 @@ trait PDODriver { try { $r = $this->db->query($query); } catch (\PDOException $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + list($excClass, $excMsg, $excData) = $this->buildPDOException(); throw new $excClass($excMsg, $excData); } return new PDOResult($this->db, $r); } - - public function prepareArray(string $query, array $paramTypes): Statement { - return new PDOStatement($this->db, $query, $paramTypes); - } } diff --git a/lib/Db/PDOError.php b/lib/Db/PDOError.php index dcb8aa4..6869e4a 100644 --- a/lib/Db/PDOError.php +++ b/lib/Db/PDOError.php @@ -6,56 +6,19 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db; -use JKingWeb\Arsse\Db\SQLite3\Driver as SQLite3; - trait PDOError { - public function exceptionBuild(bool $statementError = false): array { + use SQLState; + + protected function buildPDOException(bool $statementError = false): array { if ($statementError) { $err = $this->st->errorInfo(); } else { $err = $this->db->errorInfo(); } - switch ($err[0]) { - case "22007": - case "22P02": - case "42804": - return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; - case "23000": - case "23502": - case "23505": - return [ExceptionInput::class, "constraintViolation", $err[2]]; - case "55P03": - case "57014": - return [ExceptionTimeout::class, 'general', $err[2]]; - case "HY000": - // engine-specific errors - switch ($this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) { - case "sqlite": - switch ($err[1]) { - case SQLite3::SQLITE_BUSY: - return [ExceptionTimeout::class, 'general', $err[2]]; - case SQLite3::SQLITE_MISMATCH: - return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; - } - break; // @codeCoverageIgnore - case "mysql": - switch ($err[1]) { - case 1205: - return [ExceptionTimeout::class, 'general', $err[2]]; - case 1364: - return [ExceptionInput::class, "constraintViolation", $err[2]]; - case 1366: - return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; - } - break; // @codeCoverageIgnore - } - return [Exception::class, "engineErrorGeneral", $err[0]."/".$err[1].": ".$err[2]]; // @codeCoverageIgnore - default: - return [Exception::class, "engineErrorGeneral", $err[0].": ".$err[2]]; // @codeCoverageIgnore + if ($err[0]=="HY000") { + return static::buildEngineException($err[1], $err[2]); + } else { + return static::buildStandardException($err[0], $err[2]); } } - - public function getError(): string { - return (string) $this->db->errorInfo()[2]; - } } diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php index d5a36c5..594ecf8 100644 --- a/lib/Db/PDOStatement.php +++ b/lib/Db/PDOStatement.php @@ -6,7 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db; -class PDOStatement extends AbstractStatement { +abstract class PDOStatement extends AbstractStatement { use PDOError; const BINDINGS = [ @@ -34,7 +34,7 @@ class PDOStatement extends AbstractStatement { $this->st = $this->db->prepare($query); return true; } catch (\PDOException $e) { // @codeCoverageIgnore - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); // @codeCoverageIgnore + list($excClass, $excMsg, $excData) = $this->buildPDOException(); // @codeCoverageIgnore throw new $excClass($excMsg, $excData); // @codeCoverageIgnore } } @@ -49,7 +49,7 @@ class PDOStatement extends AbstractStatement { try { $this->st->execute(); } catch (\PDOException $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(true); + list($excClass, $excMsg, $excData) = $this->buildPDOException(true); throw new $excClass($excMsg, $excData); } return new PDOResult($this->db, $this->st); diff --git a/lib/Db/PostgreSQL/Dispatch.php b/lib/Db/PostgreSQL/Dispatch.php index c6cb198..6f4c89d 100644 --- a/lib/Db/PostgreSQL/Dispatch.php +++ b/lib/Db/PostgreSQL/Dispatch.php @@ -6,37 +6,19 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db\PostgreSQL; -use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Conf; -use JKingWeb\Arsse\Db\Exception; -use JKingWeb\Arsse\Db\ExceptionInput; -use JKingWeb\Arsse\Db\ExceptionTimeout; - trait Dispatch { protected function dispatchQuery(string $query, array $params = []) { pg_send_query_params($this->db, $query, $params); $result = pg_get_result($this->db); if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) { - return $this->buildException($code, pg_result_error($result)); + return $this->buildStandardException($code, pg_result_error($result)); } else { return $result; } } - protected function buildException(string $code, string $msg): array { - switch ($code) { - case "22P02": - case "42804": - return [ExceptionInput::class, 'engineTypeViolation', $msg]; - case "23000": - case "23502": - case "23505": - return [ExceptionInput::class, "engineConstraintViolation", $msg]; - case "55P03": - case "57014": - return [ExceptionTimeout::class, 'general', $msg]; - default: - return [Exception::class, "engineErrorGeneral", $code.": ".$msg]; // @codeCoverageIgnore - } + public static function buildEngineException($code, string $msg): array { + // PostgreSQL uses SQLSTATE exclusively, so this is not used + return []; } } diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 986eeed..0fa3bd1 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -20,7 +20,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { protected $db; protected $transStart = 0; - public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) { + public function __construct() { // check to make sure required extension is loaded if (!static::requirementsMet()) { throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore @@ -193,16 +193,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } } - protected function getError(): string { - // stub - return ""; - } - public function exec(string $query): bool { pg_send_query($this->db, $query); while ($result = pg_get_result($this->db)) { if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) { - list($excClass, $excMsg, $excData) = $this->buildException($code, pg_result_error($result)); + list($excClass, $excMsg, $excData) = $this->buildStandardException($code, pg_result_error($result)); throw new $excClass($excMsg, $excData); } } diff --git a/lib/Db/PostgreSQL/PDOStatement.php b/lib/Db/PostgreSQL/PDOStatement.php index c255eae..4bb32cb 100644 --- a/lib/Db/PostgreSQL/PDOStatement.php +++ b/lib/Db/PostgreSQL/PDOStatement.php @@ -6,8 +6,13 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db\PostgreSQL; -class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement { +class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement { public static function mungeQuery(string $query, array $types, ...$extraData): string { return Statement::mungeQuery($query, $types, false); } + + public static function buildEngineException($code, string $msg): array { + // PostgreSQL uses SQLSTATE exclusively, so this is not used + return []; + } } diff --git a/lib/Db/PostgreSQL/Statement.php b/lib/Db/PostgreSQL/Statement.php index 64d4ddc..8d6668f 100644 --- a/lib/Db/PostgreSQL/Statement.php +++ b/lib/Db/PostgreSQL/Statement.php @@ -38,6 +38,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { $this->in = []; $this->bindValues($values); $r = $this->dispatchQuery($this->qMunged, $this->in); + $this->in = []; if (is_resource($r)) { return new Result($this->db, $r); } else { diff --git a/lib/Db/ResultEmpty.php b/lib/Db/ResultEmpty.php index 60478b2..d5b6e0d 100644 --- a/lib/Db/ResultEmpty.php +++ b/lib/Db/ResultEmpty.php @@ -9,12 +9,20 @@ namespace JKingWeb\Arsse\Db; use JKingWeb\Arsse\Db\Exception; class ResultEmpty extends AbstractResult { + protected $changes = 0; + protected $id = 0; + + public function __construct(int $changes = 0, int $id = 0) { + $this->changes = $changes; + $this->id = $id; + } + public function changes(): int { - return 0; + return $this->changes; } public function lastId(): int { - return 0; + return $this->id; } // PHP iterator methods diff --git a/lib/Db/SQLState.php b/lib/Db/SQLState.php new file mode 100644 index 0000000..7448798 --- /dev/null +++ b/lib/Db/SQLState.php @@ -0,0 +1,31 @@ +db->lastErrorMsg(); - } - public function exec(string $query): bool { try { return (bool) $this->db->exec($query); } catch (\Exception $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + list($excClass, $excMsg, $excData) = $this->buildException(); throw new $excClass($excMsg, $excData); } } @@ -157,7 +153,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { try { $r = $this->db->query($query); } catch (\Exception $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + list($excClass, $excMsg, $excData) = $this->buildException(); throw new $excClass($excMsg, $excData); } $changes = $this->db->changes(); diff --git a/lib/Db/SQLite3/ExceptionBuilder.php b/lib/Db/SQLite3/ExceptionBuilder.php index 4e63163..9e3bfff 100644 --- a/lib/Db/SQLite3/ExceptionBuilder.php +++ b/lib/Db/SQLite3/ExceptionBuilder.php @@ -11,16 +11,20 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionTimeout; trait ExceptionBuilder { - public function exceptionBuild(): array { - switch ($this->db->lastErrorCode()) { - case self::SQLITE_BUSY: - return [ExceptionTimeout::class, 'general', $this->db->lastErrorMsg()]; - case self::SQLITE_CONSTRAINT: - return [ExceptionInput::class, 'engineConstraintViolation', $this->db->lastErrorMsg()]; - case self::SQLITE_MISMATCH: - return [ExceptionInput::class, 'engineTypeViolation', $this->db->lastErrorMsg()]; + protected function buildException(): array { + return self::buildEngineException($this->db->lastErrorCode(), $this->db->lastErrorMsg()); + } + + public static function buildEngineException($code, string $msg): array { + switch ($code) { + case Driver::SQLITE_BUSY: + return [ExceptionTimeout::class, 'general', $msg]; + case Driver::SQLITE_CONSTRAINT: + return [ExceptionInput::class, 'engineConstraintViolation', $msg]; + case Driver::SQLITE_MISMATCH: + return [ExceptionInput::class, 'engineTypeViolation', $msg]; default: - return [Exception::class, 'engineErrorGeneral', $this->db->lastErrorMsg()]; + return [Exception::class, 'engineErrorGeneral', $msg]; } } } diff --git a/lib/Db/SQLite3/PDODriver.php b/lib/Db/SQLite3/PDODriver.php index cbad93c..c36a3c1 100644 --- a/lib/Db/SQLite3/PDODriver.php +++ b/lib/Db/SQLite3/PDODriver.php @@ -45,4 +45,8 @@ class PDODriver extends Driver { public static function driverName(): string { return Arsse::$lang->msg("Driver.Db.SQLite3PDO.Name"); } + + public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { + return new PDOStatement($this->db, $query, $paramTypes); + } } diff --git a/lib/Db/SQLite3/PDOStatement.php b/lib/Db/SQLite3/PDOStatement.php new file mode 100644 index 0000000..7e7642d --- /dev/null +++ b/lib/Db/SQLite3/PDOStatement.php @@ -0,0 +1,12 @@ +st = $this->db->prepare($query); return true; } catch (\Exception $e) { // @codeCoverageIgnore - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); // @codeCoverageIgnore + list($excClass, $excMsg, $excData) = $this->buildException(); // @codeCoverageIgnore throw new $excClass($excMsg, $excData); // @codeCoverageIgnore } } @@ -60,7 +60,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { try { $r = $this->st->execute(); } catch (\Exception $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + list($excClass, $excMsg, $excData) = $this->buildException(); throw new $excClass($excMsg, $excData); } $changes = $this->db->changes(); diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php index 9f5788c..6a5322f 100644 --- a/tests/lib/DatabaseInformation.php +++ b/tests/lib/DatabaseInformation.php @@ -228,7 +228,7 @@ class DatabaseInformation { 'PDO SQLite 3' => [ 'pdo' => true, 'backend' => "SQLite 3", - 'statementClass' => \JKingWeb\Arsse\Db\PDOStatement::class, + 'statementClass' => \JKingWeb\Arsse\Db\SQLite3\PDOStatement::class, 'resultClass' => \JKingWeb\Arsse\Db\PDOResult::class, 'driverClass' => \JKingWeb\Arsse\Db\SQLite3\PDODriver::class, 'stringOutput' => true,