Proof-of-concept PDO MySQL driver
- Configuration options were added - Non-transactional locking was added to the savepoint handlers - Tests were adjusted for MySQL's reserved words
This commit is contained in:
parent
316ba941a2
commit
4ef36643a4
20 changed files with 483 additions and 62 deletions
|
@ -55,6 +55,7 @@ abstract class AbstractException extends \Exception {
|
|||
"Db/ExceptionInput.circularDependence" => 10238,
|
||||
"Db/ExceptionInput.subjectMissing" => 10239,
|
||||
"Db/ExceptionTimeout.general" => 10241,
|
||||
"Db/ExceptionTimeout.logicalLock" => 10241,
|
||||
"Conf/Exception.fileMissing" => 10301,
|
||||
"Conf/Exception.fileUnusable" => 10302,
|
||||
"Conf/Exception.fileUnreadable" => 10303,
|
||||
|
|
10
lib/Conf.php
10
lib/Conf.php
|
@ -43,6 +43,16 @@ class Conf {
|
|||
public $dbPostgreSQLSchema = "";
|
||||
/** @var string Service file entry to use (if using PostgreSQL); if using a service entry all above parameters except schema are ignored */
|
||||
public $dbPostgreSQLService = "";
|
||||
/** @var string Host name, address, or socket path of MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
public $dbMySQLHost = "localhost";
|
||||
/** @var string Log-in user name for MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
public $dbMySQLUser = "arsse";
|
||||
/** @var string Log-in password for MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
public $dbMySQLPass = "";
|
||||
/** @var integer Listening port for MySQL/MariaDB database server (if using MySQL/MariaDB over TCP) */
|
||||
public $dbMySQLPort = 3306;
|
||||
/** @var string Database name on MySQL/MariaDB database server (if using MySQL/MariaDB) */
|
||||
public $dbMySQLDb = "arsse";
|
||||
|
||||
/** @var string Class of the user management driver in use (Internal by default) */
|
||||
public $userDriver = User\Internal\Driver::class;
|
||||
|
|
|
@ -75,8 +75,13 @@ abstract class AbstractDriver implements Driver {
|
|||
$this->lock();
|
||||
$this->locked = true;
|
||||
}
|
||||
// create a savepoint, incrementing the transaction depth
|
||||
$this->exec("SAVEPOINT arsse_".(++$this->transDepth));
|
||||
if ($this->locked && static::TRANSACTIONAL_LOCKS == false) {
|
||||
// if locks are not compatible with transactions (and savepoints), don't actually create a savepoint)
|
||||
$this->transDepth++;
|
||||
} else {
|
||||
// create a savepoint, incrementing the transaction depth
|
||||
$this->exec("SAVEPOINT arsse_".(++$this->transDepth));
|
||||
}
|
||||
// set the state of the newly created savepoint to pending
|
||||
$this->transStatus[$this->transDepth] = self::TR_PEND;
|
||||
// return the depth number
|
||||
|
@ -89,8 +94,13 @@ abstract class AbstractDriver implements Driver {
|
|||
if (array_key_exists($index, $this->transStatus)) {
|
||||
switch ($this->transStatus[$index]) {
|
||||
case self::TR_PEND:
|
||||
// release the requested savepoint and set its state to committed
|
||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||
if ($this->locked && static::TRANSACTIONAL_LOCKS == false) {
|
||||
// if locks are not compatible with transactions, do nothing
|
||||
} else {
|
||||
// release the requested savepoint
|
||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||
}
|
||||
// set its state to committed
|
||||
$this->transStatus[$index] = self::TR_COMMIT;
|
||||
// for any later pending savepoints, set their state to implicitly committed
|
||||
$a = $index;
|
||||
|
@ -142,8 +152,14 @@ abstract class AbstractDriver implements Driver {
|
|||
if (array_key_exists($index, $this->transStatus)) {
|
||||
switch ($this->transStatus[$index]) {
|
||||
case self::TR_PEND:
|
||||
$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT arsse_".$index);
|
||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||
if ($this->locked && static::TRANSACTIONAL_LOCKS == false) {
|
||||
// if locks are not compatible with transactions, do nothing and report failure as a rollback cannot occur
|
||||
$out = false;
|
||||
} else {
|
||||
// roll back and then erase the savepoint
|
||||
$this->exec("ROLLBACK TO SAVEPOINT arsse_".$index);
|
||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||
}
|
||||
$this->transStatus[$index] = self::TR_ROLLBACK;
|
||||
$a = $index;
|
||||
while (++$a && $a <= $this->transDepth) {
|
||||
|
@ -151,7 +167,7 @@ abstract class AbstractDriver implements Driver {
|
|||
$this->transStatus[$a] = self::TR_PEND_ROLLBACK;
|
||||
}
|
||||
}
|
||||
$out = true;
|
||||
$out = $out ?? true;
|
||||
break;
|
||||
case self::TR_PEND_COMMIT:
|
||||
$this->transStatus[$index] = self::TR_ROLLBACK;
|
||||
|
|
163
lib/Db/MySQL/Driver.php
Normal file
163
lib/Db/MySQL/Driver.php
Normal file
|
@ -0,0 +1,163 @@
|
|||
<?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\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||
const SQL_MODE = "ANSI_QUOTES,HIGH_NOT_PRECEDENCE,NO_BACKSLASH_ESCAPES,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY,PIPES_AS_CONCAT,STRICT_ALL_TABLES";
|
||||
const TRANSACTIONAL_LOCKS = false;
|
||||
|
||||
protected $db;
|
||||
protected $transStart = 0;
|
||||
|
||||
public function __construct() {
|
||||
// check to make sure required extension is loaded
|
||||
if (!static::requirementsMet()) {
|
||||
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
|
||||
}
|
||||
$host = Arsse::$conf->dbMySQLHost;
|
||||
if ($host[0] == "/") {
|
||||
// host is a socket
|
||||
$socket = $host;
|
||||
$host = "";
|
||||
} elseif(substr($host, 0, 9) == "\\\\.\\pipe\\") {
|
||||
// host is a Windows named piple
|
||||
$socket = substr($host, 10);
|
||||
$host = "";
|
||||
}
|
||||
$user = Arsse::$conf->dbMySQLUser ?? "";
|
||||
$pass = Arsse::$conf->dbMySQLPass ?? "";
|
||||
$port = Arsse::$conf->dbMySQLPost ?? 3306;
|
||||
$db = Arsse::$conf->dbMySQLDb ?? "arsse";
|
||||
$this->makeConnection($user, $pass, $db, $host, $port, $socket ?? "");
|
||||
$this->exec("SET lock_wait_timeout = 1");
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
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 schemaID(): string {
|
||||
return "MySQL";
|
||||
}
|
||||
|
||||
public function charsetAcceptable(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function schemaVersion(): int {
|
||||
if ($this->query("SELECT count(*) from information_schema.tables where table_name = 'arsse_meta'")->getValue()) {
|
||||
return (int) $this->query("SELECT value from arsse_meta where `key` = 'schema_version'")->getValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function sqlToken(string $token): string {
|
||||
switch (strtolower($token)) {
|
||||
case "nocase":
|
||||
return '"utf8mb4_unicode_nopad_ci"';
|
||||
default:
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
public function savepointCreate(bool $lock = false): int {
|
||||
if (!$this->transStart && !$lock) {
|
||||
$this->exec("BEGIN");
|
||||
$this->transStart = parent::savepointCreate($lock);
|
||||
return $this->transStart;
|
||||
} else {
|
||||
return parent::savepointCreate($lock);
|
||||
}
|
||||
}
|
||||
|
||||
public function savepointRelease(int $index = null): bool {
|
||||
$index = $index ?? $this->transDepth;
|
||||
$out = parent::savepointRelease($index);
|
||||
if ($index == $this->transStart) {
|
||||
$this->exec("COMMIT");
|
||||
$this->transStart = 0;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function savepointUndo(int $index = null): bool {
|
||||
$index = $index ?? $this->transDepth;
|
||||
$out = parent::savepointUndo($index);
|
||||
if ($index == $this->transStart) {
|
||||
$this->exec("ROLLBACK");
|
||||
$this->transStart = 0;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected function lock(): bool {
|
||||
$tables = $this->query("SELECT table_name as name from information_schema.tables where table_schema = database() and table_name like 'arsse_%'")->getAll();
|
||||
if ($tables) {
|
||||
$tables = array_column($tables, "name");
|
||||
$tables = array_map(function($table) {
|
||||
$table = str_replace('"', '""', $table);
|
||||
return "\"$table\" write";
|
||||
}, $tables);
|
||||
$tables = implode(", ", $tables);
|
||||
try {
|
||||
$this->exec("SET lock_wait_timeout = 1; LOCK TABLES $tables");
|
||||
} finally {
|
||||
$this->exec("SET lock_wait_timeout = 0");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function unlock(bool $rollback = false): bool {
|
||||
$this->exec("UNLOCK TABLES");
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if (isset($this->db)) {
|
||||
$this->db->close();
|
||||
unset($this->db);
|
||||
}
|
||||
}
|
||||
|
||||
public static function driverName(): string {
|
||||
return Arsse::$lang->msg("Driver.Db.MySQL.Name");
|
||||
}
|
||||
|
||||
public static function requirementsMet(): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket) {
|
||||
}
|
||||
|
||||
protected function getError(): string {
|
||||
}
|
||||
|
||||
public function exec(string $query): bool {
|
||||
}
|
||||
|
||||
public function query(string $query): \JKingWeb\Arsse\Db\Result {
|
||||
}
|
||||
|
||||
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
|
||||
}
|
||||
}
|
59
lib/Db/MySQL/PDODriver.php
Normal file
59
lib/Db/MySQL/PDODriver.php
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?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\Arsse;
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
class PDODriver extends Driver {
|
||||
use \JKingWeb\Arsse\Db\PDODriver;
|
||||
|
||||
protected $db;
|
||||
|
||||
public static function requirementsMet(): bool {
|
||||
return class_exists("PDO") && in_array("mysql", \PDO::getAvailableDrivers());
|
||||
}
|
||||
|
||||
protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket) {
|
||||
$dsn = [];
|
||||
$dsn[] = "charset=utf8mb4";
|
||||
$dsn[] = "dbname=$db";
|
||||
if (strlen($host)) {
|
||||
$dsn[] = "host=$host";
|
||||
$dsn[] = "port=$port";
|
||||
} elseif (strlen($socket)) {
|
||||
$dsn[] = "socket=$socket";
|
||||
}
|
||||
$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."'",
|
||||
]);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
unset($this->db);
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public static function create(): \JKingWeb\Arsse\Db\Driver {
|
||||
if (self::requirementsMet()) {
|
||||
return new self;
|
||||
} elseif (Driver::requirementsMet()) {
|
||||
return new Driver;
|
||||
} else {
|
||||
throw new Exception("extMissing", self::driverName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function driverName(): string {
|
||||
return Arsse::$lang->msg("Driver.Db.MySQLPDO.Name");
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@
|
|||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Db;
|
||||
|
||||
use JKingWeb\Arsse\Db\SQLite3\Driver as SQLite3;
|
||||
|
||||
trait PDOError {
|
||||
public function exceptionBuild(bool $statementError = null): array {
|
||||
if ($statementError ?? ($this instanceof Statement)) {
|
||||
|
@ -14,6 +16,7 @@ trait PDOError {
|
|||
$err = $this->db->errorInfo();
|
||||
}
|
||||
switch ($err[0]) {
|
||||
case "22007":
|
||||
case "22P02":
|
||||
case "42804":
|
||||
return [ExceptionInput::class, 'engineTypeViolation', $err[2]];
|
||||
|
@ -29,18 +32,22 @@ trait PDOError {
|
|||
switch ($this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
|
||||
case "sqlite":
|
||||
switch ($err[1]) {
|
||||
case \JKingWeb\Arsse\Db\SQLite3\Driver::SQLITE_BUSY:
|
||||
case SQLite3::SQLITE_BUSY:
|
||||
return [ExceptionTimeout::class, 'general', $err[2]];
|
||||
case \JKingWeb\Arsse\Db\SQLite3\Driver::SQLITE_MISMATCH:
|
||||
case SQLite3::SQLITE_MISMATCH:
|
||||
return [ExceptionInput::class, 'engineTypeViolation', $err[2]];
|
||||
default:
|
||||
return [Exception::class, "engineErrorGeneral", $err[1]." - ".$err[2]];
|
||||
}
|
||||
// no break
|
||||
default:
|
||||
return [Exception::class, "engineErrorGeneral", $err[2]]; // @codeCoverageIgnore
|
||||
break;
|
||||
case "mysql":
|
||||
switch ($err[1]) {
|
||||
case 1205:
|
||||
return [ExceptionTimeout::class, 'general', $err[2]];
|
||||
case 1364:
|
||||
return [ExceptionInput::class, "constraintViolation", $err[2]];
|
||||
}
|
||||
break;
|
||||
}
|
||||
// no break
|
||||
return [Exception::class, "engineErrorGeneral", $err[0]."/".$err[1].": ".$err[2]]; // @codeCoverageIgnore
|
||||
default:
|
||||
return [Exception::class, "engineErrorGeneral", $err[0].": ".$err[2]]; // @codeCoverageIgnore
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ use JKingWeb\Arsse\Db\ExceptionTimeout;
|
|||
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||
use Dispatch;
|
||||
|
||||
const TRANSACTIONAL_LOCKS = true;
|
||||
|
||||
protected $db;
|
||||
protected $transStart = 0;
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ use JKingWeb\Arsse\Db\ExceptionTimeout;
|
|||
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||
use ExceptionBuilder;
|
||||
|
||||
const TRANSACTIONAL_LOCKS = true;
|
||||
|
||||
const SQLITE_BUSY = 5;
|
||||
const SQLITE_CONSTRAINT = 19;
|
||||
const SQLITE_MISMATCH = 20;
|
||||
|
|
|
@ -22,6 +22,8 @@ return [
|
|||
'Driver.Db.SQLite3PDO.Name' => 'SQLite 3 (PDO)',
|
||||
'Driver.Db.PostgreSQL.Name' => 'PostgreSQL',
|
||||
'Driver.Db.PostgreSQLPDO.Name' => 'PostgreSQL (PDO)',
|
||||
'Driver.Db.MySQL.Name' => 'MySQL/MariaDB',
|
||||
'Driver.Db.MySQLPDO.Name' => 'MySQL/MariaDB (PDO)',
|
||||
'Driver.Service.Curl.Name' => 'HTTP (curl)',
|
||||
'Driver.Service.Internal.Name' => 'Internal',
|
||||
'Driver.User.Internal.Name' => 'Internal',
|
||||
|
@ -163,6 +165,7 @@ return [
|
|||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineConstraintViolation' => '{0}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.logicalLock' => 'Database is locked',
|
||||
'Exception.JKingWeb/Arsse/User/Exception.alreadyExists' => 'Could not perform action "{action}" because the user {user} already exists',
|
||||
'Exception.JKingWeb/Arsse/User/Exception.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist',
|
||||
'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed',
|
||||
|
|
|
@ -11,6 +11,7 @@ use JKingWeb\Arsse\Db\Result;
|
|||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected static $insertDefaultValues = "INSERT INTO arsse_test default values";
|
||||
protected static $dbInfo;
|
||||
protected static $interface;
|
||||
protected $drv;
|
||||
|
@ -39,8 +40,8 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
// completely clear the database and ensure the schema version can easily be altered
|
||||
(static::$dbInfo->razeFunction)(static::$interface, [
|
||||
"CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)",
|
||||
"INSERT INTO arsse_meta(key,value) values('schema_version','0')",
|
||||
"CREATE TABLE arsse_meta(\"key\" varchar(255) primary key not null, value text)",
|
||||
"INSERT INTO arsse_meta(\"key\",value) values('schema_version','0')",
|
||||
]);
|
||||
// construct a fresh driver for each test
|
||||
$this->drv = new static::$dbInfo->driverClass;
|
||||
|
@ -115,14 +116,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$this->exec($this->create);
|
||||
$this->exec($this->lock);
|
||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||
$lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock;
|
||||
$this->drv->exec($lock);
|
||||
$this->drv->exec("INSERT INTO arsse_meta(\"key\", value) values('lock', '1')");
|
||||
}
|
||||
|
||||
public function testExecConstraintViolation() {
|
||||
$this->drv->exec("CREATE TABLE arsse_test(id varchar(255) not null)");
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$this->drv->exec("INSERT INTO arsse_test default values");
|
||||
$this->drv->exec(static::$insertDefaultValues);
|
||||
}
|
||||
|
||||
public function testExecTypeViolation() {
|
||||
|
@ -140,18 +140,10 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
$this->drv->query("Apollo was astonished; Dionysus thought me mad");
|
||||
}
|
||||
|
||||
public function testQueryTimeout() {
|
||||
$this->exec($this->create);
|
||||
$this->exec($this->lock);
|
||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||
$lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock;
|
||||
$this->drv->exec($lock);
|
||||
}
|
||||
|
||||
public function testQueryConstraintViolation() {
|
||||
$this->drv->exec("CREATE TABLE arsse_test(id integer not null)");
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$this->drv->query("INSERT INTO arsse_test default values");
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
}
|
||||
|
||||
public function testQueryTypeViolation() {
|
||||
|
@ -220,23 +212,21 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testBeginATransaction() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
}
|
||||
|
||||
public function testCommitATransaction() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr->commit();
|
||||
|
@ -246,10 +236,9 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testRollbackATransaction() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr->rollback();
|
||||
|
@ -259,28 +248,26 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testBeginChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
}
|
||||
|
||||
public function testCommitChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2->commit();
|
||||
|
@ -291,14 +278,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testCommitChainedTransactionsOutOfOrder() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr1->commit();
|
||||
|
@ -308,14 +294,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testRollbackChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2->rollback();
|
||||
|
@ -328,14 +313,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testRollbackChainedTransactionsOutOfOrder() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr1->rollback();
|
||||
|
@ -348,14 +332,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testPartiallyRollbackChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->drv->query(static::$insertDefaultValues);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2->rollback();
|
||||
|
|
|
@ -10,6 +10,7 @@ use JKingWeb\Arsse\Db\Result;
|
|||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected static $insertDefault = "INSERT INTO arsse_test default values";
|
||||
protected static $dbInfo;
|
||||
protected static $interface;
|
||||
protected $resultClass;
|
||||
|
@ -57,17 +58,17 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testGetChangeCountAndLastInsertId() {
|
||||
$this->makeResult(static::$createMeta);
|
||||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_meta(key,value) values('test', 1)"));
|
||||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_meta(\"key\",value) values('test', 1)"));
|
||||
$this->assertSame(1, $r->changes());
|
||||
$this->assertSame(0, $r->lastId());
|
||||
}
|
||||
|
||||
public function testGetChangeCountAndLastInsertIdBis() {
|
||||
$this->makeResult(static::$createTest);
|
||||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_test default values"));
|
||||
$r = new $this->resultClass(...$this->makeResult(static::$insertDefault));
|
||||
$this->assertSame(1, $r->changes());
|
||||
$this->assertSame(1, $r->lastId());
|
||||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_test default values"));
|
||||
$r = new $this->resultClass(...$this->makeResult(static::$insertDefault));
|
||||
$this->assertSame(1, $r->changes());
|
||||
$this->assertSame(2, $r->lastId());
|
||||
}
|
||||
|
|
|
@ -124,8 +124,8 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
|
||||
public function testViolateConstraint() {
|
||||
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run();
|
||||
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(key) values(?)", ["str"]));
|
||||
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(\"key\" varchar(255) primary key not null, value text)")))->run();
|
||||
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(\"key\") values(?)", ["str"]));
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$s->runArray([null]);
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
|
||||
public function testLoadIncompleteFile() {
|
||||
file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);");
|
||||
file_put_contents($this->path."0.sql", "create table arsse_meta(\"key\" varchar(255) primary key not null, value text);");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testPerformPartialUpdate() {
|
||||
file_put_contents($this->path."0.sql", static::$minimal1);
|
||||
file_put_contents($this->path."1.sql", "UPDATE arsse_meta set value = '1' where key = 'schema_version'");
|
||||
file_put_contents($this->path."1.sql", "UPDATE arsse_meta set value = '1' where \"key\" = 'schema_version'");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
try {
|
||||
$this->drv->schemaUpdate(2, $this->base);
|
||||
|
|
20
tests/cases/Db/MySQLPDO/TestDriver.php
Normal file
20
tests/cases/Db/MySQLPDO/TestDriver.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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\TestCase\Db\MySQLPDO;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
protected static $implementation = "PDO MySQL";
|
||||
protected $create = "CREATE TABLE arsse_test(id bigint auto_increment primary key)";
|
||||
protected $lock = ["SET lock_wait_timeout = 1", "LOCK TABLES arsse_meta WRITE"];
|
||||
protected $setVersion = "UPDATE arsse_meta set value = '#' where `key` = 'schema_version'";
|
||||
protected static $insertDefaultValues = "INSERT INTO arsse_test(id) values(default)";
|
||||
}
|
25
tests/cases/Db/MySQLPDO/TestResult.php
Normal file
25
tests/cases/Db/MySQLPDO/TestResult.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?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\TestCase\Db\MySQLPDO;
|
||||
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PDOResult<extended>
|
||||
*/
|
||||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
|
||||
protected static $implementation = "PDO MySQL";
|
||||
protected static $createMeta = "CREATE TABLE arsse_meta(`key` varchar(255) primary key not null, value text)";
|
||||
protected static $createTest = "CREATE TABLE arsse_test(id bigint auto_increment primary key)";
|
||||
protected static $insertDefault = "INSERT INTO arsse_test(id) values(default)";
|
||||
|
||||
protected function makeResult(string $q): array {
|
||||
$set = static::$interface->query($q);
|
||||
return [static::$interface, $set];
|
||||
}
|
||||
}
|
33
tests/cases/Db/MySQLPDO/TestStatement.php
Normal file
33
tests/cases/Db/MySQLPDO/TestStatement.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?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\TestCase\Db\MySQLPDO;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PDOStatement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
protected static $implementation = "PDO MySQL";
|
||||
|
||||
protected function makeStatement(string $q, array $types = []): array {
|
||||
return [static::$interface, static::$interface->prepare($q), $types];
|
||||
}
|
||||
|
||||
protected function decorateTypeSyntax(string $value, string $type): string {
|
||||
switch ($type) {
|
||||
case "float":
|
||||
return (substr($value, -2)==".0") ? "'".substr($value, 0, strlen($value) - 2)."'" : "'$value'";
|
||||
case "string":
|
||||
if (preg_match("<^char\((\d+)\)$>", $value, $match)) {
|
||||
return "'".\IntlChar::chr((int) $match[1])."'";
|
||||
}
|
||||
return $value;
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
17
tests/cases/Db/MySQLPDO/TestUpdate.php
Normal file
17
tests/cases/Db/MySQLPDO/TestUpdate.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?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\TestCase\Db\MySQLPDO;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\MySQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
protected static $implementation = "PDO MySQL";
|
||||
protected static $minimal1 = "CREATE TABLE arsse_meta(`key` varchar(255) primary key, value text); INSERT INTO arsse_meta(`key`,value) values('schema_version','1');";
|
||||
protected static $minimal2 = "UPDATE arsse_meta set value = '2' where `key` = 'schema_version';";
|
||||
}
|
|
@ -48,6 +48,9 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
|
|||
'dbPostgreSQLPass' => "arsse_test",
|
||||
'dbPostgreSQLDb' => "arsse_test",
|
||||
'dbPostgreSQLSchema' => "arsse_test",
|
||||
'dbMySQLUser' => "arsse_test",
|
||||
'dbMySQLPass' => "arsse_test",
|
||||
'dbMySQLDb' => "arsse_test",
|
||||
];
|
||||
Arsse::$conf = ($force ? null : Arsse::$conf) ?? (new Conf)->import($defaults)->import($conf);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ class DatabaseInformation {
|
|||
if (!isset(self::$data)) {
|
||||
self::$data = self::getData();
|
||||
}
|
||||
if (!isset(self::$data[$name])) {
|
||||
if (!array_key_exists($name, self::$data)) {
|
||||
throw new \Exception("Invalid database driver name");
|
||||
}
|
||||
$this->name = $name;
|
||||
|
@ -162,6 +162,48 @@ class DatabaseInformation {
|
|||
$pgExecFunction($db, $st);
|
||||
}
|
||||
};
|
||||
$mysqlTableList = function($db): array {
|
||||
$listTables = "SELECT table_name as name from information_schema.tables where table_schema = database() and table_name like 'arsse_%'";
|
||||
if ($db instanceof Driver) {
|
||||
$tables = $db->query($listTables)->getAll();
|
||||
} elseif ($db instanceof \PDO) {
|
||||
$tables = $db->query($listTables)->fetchAll(\PDO::FETCH_ASSOC);
|
||||
} else {
|
||||
$tables = $db->query($listTables)->fetch_all(\MYSQLI_ASSOC);
|
||||
}
|
||||
$tables = sizeof($tables) ? array_column($tables, "name") : [];
|
||||
return $tables;
|
||||
};
|
||||
$mysqlTruncateFunction = function($db, array $afterStatements = []) use ($mysqlTableList) {
|
||||
// rollback any pending transaction
|
||||
try {
|
||||
$db->query("UNLOCK TABLES; ROLLBACK");
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
foreach ($mysqlTableList($db) as $table) {
|
||||
if ($table == "arsse_meta") {
|
||||
$db->query("DELETE FROM $table where `key` <> 'schema_version'");
|
||||
} else {
|
||||
$db->query("DELETE FROM $table");
|
||||
}
|
||||
}
|
||||
foreach ($afterStatements as $st) {
|
||||
$db->query($st);
|
||||
}
|
||||
};
|
||||
$mysqlRazeFunction = function($db, array $afterStatements = []) use ($mysqlTableList) {
|
||||
// rollback any pending transaction
|
||||
try {
|
||||
$db->query("UNLOCK TABLES; ROLLBACK");
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
foreach ($mysqlTableList($db) as $table) {
|
||||
$db->query("DROP TABLE IF EXISTS $table");
|
||||
}
|
||||
foreach ($afterStatements as $st) {
|
||||
$db->query($st);
|
||||
}
|
||||
};
|
||||
return [
|
||||
'SQLite 3' => [
|
||||
'pdo' => false,
|
||||
|
@ -244,6 +286,34 @@ class DatabaseInformation {
|
|||
'truncateFunction' => $pgTruncateFunction,
|
||||
'razeFunction' => $pgRazeFunction,
|
||||
],
|
||||
'PDO MySQL' => [
|
||||
'pdo' => true,
|
||||
'backend' => "MySQL",
|
||||
'statementClass' => \JKingWeb\Arsse\Db\PDOStatement::class,
|
||||
'resultClass' => \JKingWeb\Arsse\Db\PDOResult::class,
|
||||
'driverClass' => \JKingWeb\Arsse\Db\MySQL\PDODriver::class,
|
||||
'stringOutput' => true,
|
||||
'interfaceConstructor' => function() {
|
||||
try {
|
||||
$dsn = [];
|
||||
$params = [
|
||||
'charset' => "utf8mb4",
|
||||
'host' => Arsse::$conf->dbMySQLHost,
|
||||
'port' => Arsse::$conf->dbMySQLPort,
|
||||
'dbname' => Arsse::$conf->dbMySQLDb,
|
||||
];
|
||||
foreach ($params as $k => $v) {
|
||||
$dsn[] = "$k=$v";
|
||||
}
|
||||
$dsn = "mysql:".implode(";", $dsn);
|
||||
return new \PDO($dsn, Arsse::$conf->dbMySQLUser, Arsse::$conf->dbMySQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::MYSQL_ATTR_MULTI_STATEMENTS => false, \PDO::MYSQL_ATTR_INIT_COMMAND => "SET sql_mode = '".\JKingWeb\Arsse\Db\MySQL\PDODriver::SQL_MODE."'",]);
|
||||
} catch (\Throwable $e) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
'truncateFunction' => $mysqlTruncateFunction,
|
||||
'razeFunction' => $mysqlRazeFunction,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,12 @@
|
|||
<file>cases/Db/PostgreSQLPDO/TestCreation.php</file>
|
||||
<file>cases/Db/PostgreSQLPDO/TestDriver.php</file>
|
||||
<file>cases/Db/PostgreSQLPDO/TestUpdate.php</file>
|
||||
|
||||
<file>cases/Db/MySQLPDO/TestResult.php</file>
|
||||
<file>cases/Db/MySQLPDO/TestStatement.php</file>
|
||||
<file>cases/Db/MySQLPDO/TestCreation.php</file>
|
||||
<file>cases/Db/MySQLPDO/TestDriver.php</file>
|
||||
<file>cases/Db/MySQLPDO/TestUpdate.php</file>
|
||||
</testsuite>
|
||||
<testsuite name="Database functions">
|
||||
<file>cases/Db/SQLite3/TestDatabase.php</file>
|
||||
|
|
Loading…
Reference in a new issue