1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 21:22:40 +00:00

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
This commit is contained in:
J. King 2019-01-10 19:01:32 -05:00
parent 3da773eef6
commit c4a41255b0
23 changed files with 354 additions and 123 deletions

View file

@ -9,13 +9,15 @@ namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Arsse;
abstract class AbstractDriver implements Driver { abstract class AbstractDriver implements Driver {
use SQLState;
protected $locked = false; protected $locked = false;
protected $transDepth = 0; protected $transDepth = 0;
protected $transStatus = []; protected $transStatus = [];
abstract protected function lock(): bool; abstract protected function lock(): bool;
abstract protected function unlock(bool $rollback = false): 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 { public function schemaUpdate(int $to, string $basePath = null): bool {
$ver = $this->schemaVersion(); $ver = $this->schemaVersion();
@ -46,7 +48,7 @@ abstract class AbstractDriver implements Driver {
try { try {
$this->exec($sql); $this->exec($sql);
} catch (\Throwable $e) { } 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) { if ($this->schemaVersion() != $a+1) {
throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]); throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);

View file

@ -10,12 +10,15 @@ use JKingWeb\Arsse\Misc\Date;
use JKingWeb\Arsse\Misc\ValueInfo; use JKingWeb\Arsse\Misc\ValueInfo;
abstract class AbstractStatement implements Statement { abstract class AbstractStatement implements Statement {
use SQLState;
protected $types = []; protected $types = [];
protected $isNullable = []; protected $isNullable = [];
abstract public function runArray(array $values = []): Result; abstract public function runArray(array $values = []): Result;
abstract protected function bindValue($value, string $type, int $position): bool; abstract protected function bindValue($value, string $type, int $position): bool;
abstract protected function prepare(string $query): bool; abstract protected function prepare(string $query): bool;
abstract protected static function buildEngineException($code, string $msg): array;
public function run(...$values): Result { public function run(...$values): Result {
return $this->runArray($values); return $this->runArray($values);

View file

@ -13,11 +13,15 @@ use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout; use JKingWeb\Arsse\Db\ExceptionTimeout;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { 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 SQL_MODE = "ANSI_QUOTES,HIGH_NOT_PRECEDENCE,NO_BACKSLASH_ESCAPES,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,STRICT_ALL_TABLES";
const TRANSACTIONAL_LOCKS = false; const TRANSACTIONAL_LOCKS = false;
/** @var \mysql */
protected $db; protected $db;
protected $transStart = 0; protected $transStart = 0;
protected $packetSize = 4194304;
public function __construct() { public function __construct() {
// check to make sure required extension is loaded // check to make sure required extension is loaded
@ -26,7 +30,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
} }
$host = Arsse::$conf->dbMySQLHost; $host = Arsse::$conf->dbMySQLHost;
if ($host[0] == "/") { if ($host[0] == "/") {
// host is a socket // host is a Unix socket
$socket = $host; $socket = $host;
$host = ""; $host = "";
} elseif(substr($host, 0, 9) == "\\\\.\\pipe\\") { } elseif(substr($host, 0, 9) == "\\\\.\\pipe\\") {
@ -38,9 +42,22 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
$pass = Arsse::$conf->dbMySQLPass ?? ""; $pass = Arsse::$conf->dbMySQLPass ?? "";
$port = Arsse::$conf->dbMySQLPost ?? 3306; $port = Arsse::$conf->dbMySQLPost ?? 3306;
$db = Arsse::$conf->dbMySQLDb ?? "arsse"; $db = Arsse::$conf->dbMySQLDb ?? "arsse";
// make the connection
$this->makeConnection($user, $pass, $db, $host, $port, $socket ?? ""); $this->makeConnection($user, $pass, $db, $host, $port, $socket ?? "");
$this->exec("SET lock_wait_timeout = 1"); // set session variables
$this->exec("SET time_zone = '+00:00'"); 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 */ /** @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) { 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 { public function exec(string $query): bool {
$this->dispatch($query);
return true;
}
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 { 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 { public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
return new Statement($this->db, $query, $paramTypes, $this->packetSize);
} }
} }

View file

@ -0,0 +1,30 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
trait ExceptionBuilder {
protected function buildException(): array {
return self::buildEngineException($this->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];
}
}
}

View file

@ -33,7 +33,6 @@ class PDODriver extends Driver {
$dsn = "mysql:".implode(";", $dsn); $dsn = "mysql:".implode(";", $dsn);
$this->db = new \PDO($dsn, $user, $password, [ $this->db = new \PDO($dsn, $user, $password, [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::MYSQL_ATTR_INIT_COMMAND => "SET sql_mode = '".self::SQL_MODE."'",
]); ]);
} }

View file

@ -7,15 +7,10 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL; 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 { public static function mungeQuery(string $query, array $types, ...$extraData): string {
$query = explode("?", $query); return Statement::mungeQuery($query, $types);
$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;
} }
} }

51
lib/Db/MySQL/Result.php Normal file
View file

@ -0,0 +1,51 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
use JKingWeb\Arsse\Db\Exception;
class Result extends \JKingWeb\Arsse\Db\AbstractResult {
protected $st;
protected $set;
protected $cur = null;
protected $rows = 0;
protected $id = 0;
// actual public methods
public function changes(): int {
return $this->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);
}
}

107
lib/Db/MySQL/Statement.php Normal file
View file

@ -0,0 +1,107 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\MySQL;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
use ExceptionBuilder;
const BINDINGS = [
"integer" => "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;
}
}

View file

@ -14,7 +14,7 @@ trait PDODriver {
$this->db->exec($query); $this->db->exec($query);
return true; return true;
} catch (\PDOException $e) { } catch (\PDOException $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild(); list($excClass, $excMsg, $excData) = $this->buildPDOException();
throw new $excClass($excMsg, $excData); throw new $excClass($excMsg, $excData);
} }
} }
@ -23,13 +23,9 @@ trait PDODriver {
try { try {
$r = $this->db->query($query); $r = $this->db->query($query);
} catch (\PDOException $e) { } catch (\PDOException $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild(); list($excClass, $excMsg, $excData) = $this->buildPDOException();
throw new $excClass($excMsg, $excData); throw new $excClass($excMsg, $excData);
} }
return new PDOResult($this->db, $r); return new PDOResult($this->db, $r);
} }
public function prepareArray(string $query, array $paramTypes): Statement {
return new PDOStatement($this->db, $query, $paramTypes);
}
} }

View file

@ -6,56 +6,19 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Db; namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Db\SQLite3\Driver as SQLite3;
trait PDOError { trait PDOError {
public function exceptionBuild(bool $statementError = false): array { use SQLState;
protected function buildPDOException(bool $statementError = false): array {
if ($statementError) { if ($statementError) {
$err = $this->st->errorInfo(); $err = $this->st->errorInfo();
} else { } else {
$err = $this->db->errorInfo(); $err = $this->db->errorInfo();
} }
switch ($err[0]) { if ($err[0]=="HY000") {
case "22007": return static::buildEngineException($err[1], $err[2]);
case "22P02": } else {
case "42804": return static::buildStandardException($err[0], $err[2]);
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
}
}
public function getError(): string {
return (string) $this->db->errorInfo()[2];
} }
} }

View file

@ -6,7 +6,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Db; namespace JKingWeb\Arsse\Db;
class PDOStatement extends AbstractStatement { abstract class PDOStatement extends AbstractStatement {
use PDOError; use PDOError;
const BINDINGS = [ const BINDINGS = [
@ -34,7 +34,7 @@ class PDOStatement extends AbstractStatement {
$this->st = $this->db->prepare($query); $this->st = $this->db->prepare($query);
return true; return true;
} catch (\PDOException $e) { // @codeCoverageIgnore } catch (\PDOException $e) { // @codeCoverageIgnore
list($excClass, $excMsg, $excData) = $this->exceptionBuild(); // @codeCoverageIgnore list($excClass, $excMsg, $excData) = $this->buildPDOException(); // @codeCoverageIgnore
throw new $excClass($excMsg, $excData); // @codeCoverageIgnore throw new $excClass($excMsg, $excData); // @codeCoverageIgnore
} }
} }
@ -49,7 +49,7 @@ class PDOStatement extends AbstractStatement {
try { try {
$this->st->execute(); $this->st->execute();
} catch (\PDOException $e) { } catch (\PDOException $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild(true); list($excClass, $excMsg, $excData) = $this->buildPDOException(true);
throw new $excClass($excMsg, $excData); throw new $excClass($excMsg, $excData);
} }
return new PDOResult($this->db, $this->st); return new PDOResult($this->db, $this->st);

View file

@ -6,37 +6,19 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL; 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 { trait Dispatch {
protected function dispatchQuery(string $query, array $params = []) { protected function dispatchQuery(string $query, array $params = []) {
pg_send_query_params($this->db, $query, $params); pg_send_query_params($this->db, $query, $params);
$result = pg_get_result($this->db); $result = pg_get_result($this->db);
if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) { 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 { } else {
return $result; return $result;
} }
} }
protected function buildException(string $code, string $msg): array { public static function buildEngineException($code, string $msg): array {
switch ($code) { // PostgreSQL uses SQLSTATE exclusively, so this is not used
case "22P02": return [];
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
}
} }
} }

View file

@ -20,7 +20,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
protected $db; protected $db;
protected $transStart = 0; 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 // check to make sure required extension is loaded
if (!static::requirementsMet()) { if (!static::requirementsMet()) {
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore 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 { public function exec(string $query): bool {
pg_send_query($this->db, $query); pg_send_query($this->db, $query);
while ($result = pg_get_result($this->db)) { while ($result = pg_get_result($this->db)) {
if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) { 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); throw new $excClass($excMsg, $excData);
} }
} }

View file

@ -10,4 +10,9 @@ class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {
public static function mungeQuery(string $query, array $types, ...$extraData): string { public static function mungeQuery(string $query, array $types, ...$extraData): string {
return Statement::mungeQuery($query, $types, false); return Statement::mungeQuery($query, $types, false);
} }
public static function buildEngineException($code, string $msg): array {
// PostgreSQL uses SQLSTATE exclusively, so this is not used
return [];
}
} }

View file

@ -38,6 +38,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
$this->in = []; $this->in = [];
$this->bindValues($values); $this->bindValues($values);
$r = $this->dispatchQuery($this->qMunged, $this->in); $r = $this->dispatchQuery($this->qMunged, $this->in);
$this->in = [];
if (is_resource($r)) { if (is_resource($r)) {
return new Result($this->db, $r); return new Result($this->db, $r);
} else { } else {

View file

@ -9,12 +9,20 @@ namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Db\Exception; use JKingWeb\Arsse\Db\Exception;
class ResultEmpty extends AbstractResult { 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 { public function changes(): int {
return 0; return $this->changes;
} }
public function lastId(): int { public function lastId(): int {
return 0; return $this->id;
} }
// PHP iterator methods // PHP iterator methods

31
lib/Db/SQLState.php Normal file
View file

@ -0,0 +1,31 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
trait SQLState {
protected static function buildStandardException(string $code, string $msg): array {
switch ($code) {
case "22007":
case "22P02":
case "42804":
return [ExceptionInput::class, 'engineTypeViolation', $msg];
case "23000":
case "23502":
case "23505":
return [ExceptionInput::class, "constraintViolation", $msg];
case "55P03":
case "57014":
return [ExceptionTimeout::class, 'general', $msg];
default:
return [Exception::class, "engineErrorGeneral", "SQLSTATE $code: $msg"]; // @codeCoverageIgnore
}
}
}

View file

@ -22,7 +22,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
protected $db; protected $db;
public function __construct(string $dbFile = null, string $dbKey = null) { public function __construct() {
// check to make sure required extension is loaded // check to make sure required extension is loaded
if (!static::requirementsMet()) { if (!static::requirementsMet()) {
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
@ -140,15 +140,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
return true; return true;
} }
protected function getError(): string {
return $this->db->lastErrorMsg();
}
public function exec(string $query): bool { public function exec(string $query): bool {
try { try {
return (bool) $this->db->exec($query); return (bool) $this->db->exec($query);
} catch (\Exception $e) { } catch (\Exception $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild(); list($excClass, $excMsg, $excData) = $this->buildException();
throw new $excClass($excMsg, $excData); throw new $excClass($excMsg, $excData);
} }
} }
@ -157,7 +153,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
try { try {
$r = $this->db->query($query); $r = $this->db->query($query);
} catch (\Exception $e) { } catch (\Exception $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild(); list($excClass, $excMsg, $excData) = $this->buildException();
throw new $excClass($excMsg, $excData); throw new $excClass($excMsg, $excData);
} }
$changes = $this->db->changes(); $changes = $this->db->changes();

View file

@ -11,16 +11,20 @@ use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout; use JKingWeb\Arsse\Db\ExceptionTimeout;
trait ExceptionBuilder { trait ExceptionBuilder {
public function exceptionBuild(): array { protected function buildException(): array {
switch ($this->db->lastErrorCode()) { return self::buildEngineException($this->db->lastErrorCode(), $this->db->lastErrorMsg());
case self::SQLITE_BUSY: }
return [ExceptionTimeout::class, 'general', $this->db->lastErrorMsg()];
case self::SQLITE_CONSTRAINT: public static function buildEngineException($code, string $msg): array {
return [ExceptionInput::class, 'engineConstraintViolation', $this->db->lastErrorMsg()]; switch ($code) {
case self::SQLITE_MISMATCH: case Driver::SQLITE_BUSY:
return [ExceptionInput::class, 'engineTypeViolation', $this->db->lastErrorMsg()]; return [ExceptionTimeout::class, 'general', $msg];
case Driver::SQLITE_CONSTRAINT:
return [ExceptionInput::class, 'engineConstraintViolation', $msg];
case Driver::SQLITE_MISMATCH:
return [ExceptionInput::class, 'engineTypeViolation', $msg];
default: default:
return [Exception::class, 'engineErrorGeneral', $this->db->lastErrorMsg()]; return [Exception::class, 'engineErrorGeneral', $msg];
} }
} }
} }

View file

@ -45,4 +45,8 @@ class PDODriver extends Driver {
public static function driverName(): string { public static function driverName(): string {
return Arsse::$lang->msg("Driver.Db.SQLite3PDO.Name"); 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);
}
} }

View file

@ -0,0 +1,12 @@
<?php
/** @license MIT
* Copyright 2017 J. King, Dustin Wilson et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace JKingWeb\Arsse\Db\SQLite3;
class PDOStatement extends \JKingWeb\Arsse\Db\PDOStatement {
use ExceptionBuilder;
use \JKingWeb\Arsse\Db\PDOError;
}

View file

@ -41,7 +41,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
$this->st = $this->db->prepare($query); $this->st = $this->db->prepare($query);
return true; return true;
} catch (\Exception $e) { // @codeCoverageIgnore } catch (\Exception $e) { // @codeCoverageIgnore
list($excClass, $excMsg, $excData) = $this->exceptionBuild(); // @codeCoverageIgnore list($excClass, $excMsg, $excData) = $this->buildException(); // @codeCoverageIgnore
throw new $excClass($excMsg, $excData); // @codeCoverageIgnore throw new $excClass($excMsg, $excData); // @codeCoverageIgnore
} }
} }
@ -60,7 +60,7 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
try { try {
$r = $this->st->execute(); $r = $this->st->execute();
} catch (\Exception $e) { } catch (\Exception $e) {
list($excClass, $excMsg, $excData) = $this->exceptionBuild(); list($excClass, $excMsg, $excData) = $this->buildException();
throw new $excClass($excMsg, $excData); throw new $excClass($excMsg, $excData);
} }
$changes = $this->db->changes(); $changes = $this->db->changes();

View file

@ -228,7 +228,7 @@ class DatabaseInformation {
'PDO SQLite 3' => [ 'PDO SQLite 3' => [
'pdo' => true, 'pdo' => true,
'backend' => "SQLite 3", 'backend' => "SQLite 3",
'statementClass' => \JKingWeb\Arsse\Db\PDOStatement::class, 'statementClass' => \JKingWeb\Arsse\Db\SQLite3\PDOStatement::class,
'resultClass' => \JKingWeb\Arsse\Db\PDOResult::class, 'resultClass' => \JKingWeb\Arsse\Db\PDOResult::class,
'driverClass' => \JKingWeb\Arsse\Db\SQLite3\PDODriver::class, 'driverClass' => \JKingWeb\Arsse\Db\SQLite3\PDODriver::class,
'stringOutput' => true, 'stringOutput' => true,