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

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:
J. King 2018-12-20 18:06:28 -05:00
parent 316ba941a2
commit 4ef36643a4
20 changed files with 483 additions and 62 deletions

View file

@ -55,6 +55,7 @@ abstract class AbstractException extends \Exception {
"Db/ExceptionInput.circularDependence" => 10238, "Db/ExceptionInput.circularDependence" => 10238,
"Db/ExceptionInput.subjectMissing" => 10239, "Db/ExceptionInput.subjectMissing" => 10239,
"Db/ExceptionTimeout.general" => 10241, "Db/ExceptionTimeout.general" => 10241,
"Db/ExceptionTimeout.logicalLock" => 10241,
"Conf/Exception.fileMissing" => 10301, "Conf/Exception.fileMissing" => 10301,
"Conf/Exception.fileUnusable" => 10302, "Conf/Exception.fileUnusable" => 10302,
"Conf/Exception.fileUnreadable" => 10303, "Conf/Exception.fileUnreadable" => 10303,

View file

@ -43,6 +43,16 @@ class Conf {
public $dbPostgreSQLSchema = ""; public $dbPostgreSQLSchema = "";
/** @var string Service file entry to use (if using PostgreSQL); if using a service entry all above parameters except schema are ignored */ /** @var string Service file entry to use (if using PostgreSQL); if using a service entry all above parameters except schema are ignored */
public $dbPostgreSQLService = ""; 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) */ /** @var string Class of the user management driver in use (Internal by default) */
public $userDriver = User\Internal\Driver::class; public $userDriver = User\Internal\Driver::class;

View file

@ -75,8 +75,13 @@ abstract class AbstractDriver implements Driver {
$this->lock(); $this->lock();
$this->locked = true; $this->locked = true;
} }
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 // create a savepoint, incrementing the transaction depth
$this->exec("SAVEPOINT arsse_".(++$this->transDepth)); $this->exec("SAVEPOINT arsse_".(++$this->transDepth));
}
// set the state of the newly created savepoint to pending // set the state of the newly created savepoint to pending
$this->transStatus[$this->transDepth] = self::TR_PEND; $this->transStatus[$this->transDepth] = self::TR_PEND;
// return the depth number // return the depth number
@ -89,8 +94,13 @@ abstract class AbstractDriver implements Driver {
if (array_key_exists($index, $this->transStatus)) { if (array_key_exists($index, $this->transStatus)) {
switch ($this->transStatus[$index]) { switch ($this->transStatus[$index]) {
case self::TR_PEND: case self::TR_PEND:
// release the requested savepoint and set its state to committed 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); $this->exec("RELEASE SAVEPOINT arsse_".$index);
}
// set its state to committed
$this->transStatus[$index] = self::TR_COMMIT; $this->transStatus[$index] = self::TR_COMMIT;
// for any later pending savepoints, set their state to implicitly committed // for any later pending savepoints, set their state to implicitly committed
$a = $index; $a = $index;
@ -142,8 +152,14 @@ abstract class AbstractDriver implements Driver {
if (array_key_exists($index, $this->transStatus)) { if (array_key_exists($index, $this->transStatus)) {
switch ($this->transStatus[$index]) { switch ($this->transStatus[$index]) {
case self::TR_PEND: case self::TR_PEND:
$this->exec("ROLLBACK TRANSACTION TO 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->exec("RELEASE SAVEPOINT arsse_".$index);
}
$this->transStatus[$index] = self::TR_ROLLBACK; $this->transStatus[$index] = self::TR_ROLLBACK;
$a = $index; $a = $index;
while (++$a && $a <= $this->transDepth) { while (++$a && $a <= $this->transDepth) {
@ -151,7 +167,7 @@ abstract class AbstractDriver implements Driver {
$this->transStatus[$a] = self::TR_PEND_ROLLBACK; $this->transStatus[$a] = self::TR_PEND_ROLLBACK;
} }
} }
$out = true; $out = $out ?? true;
break; break;
case self::TR_PEND_COMMIT: case self::TR_PEND_COMMIT:
$this->transStatus[$index] = self::TR_ROLLBACK; $this->transStatus[$index] = self::TR_ROLLBACK;

163
lib/Db/MySQL/Driver.php Normal file
View 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 {
}
}

View 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");
}
}

View file

@ -6,6 +6,8 @@
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 = null): array { public function exceptionBuild(bool $statementError = null): array {
if ($statementError ?? ($this instanceof Statement)) { if ($statementError ?? ($this instanceof Statement)) {
@ -14,6 +16,7 @@ trait PDOError {
$err = $this->db->errorInfo(); $err = $this->db->errorInfo();
} }
switch ($err[0]) { switch ($err[0]) {
case "22007":
case "22P02": case "22P02":
case "42804": case "42804":
return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; return [ExceptionInput::class, 'engineTypeViolation', $err[2]];
@ -29,18 +32,22 @@ trait PDOError {
switch ($this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) { switch ($this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
case "sqlite": case "sqlite":
switch ($err[1]) { switch ($err[1]) {
case \JKingWeb\Arsse\Db\SQLite3\Driver::SQLITE_BUSY: case SQLite3::SQLITE_BUSY:
return [ExceptionTimeout::class, 'general', $err[2]]; return [ExceptionTimeout::class, 'general', $err[2]];
case \JKingWeb\Arsse\Db\SQLite3\Driver::SQLITE_MISMATCH: case SQLite3::SQLITE_MISMATCH:
return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; return [ExceptionInput::class, 'engineTypeViolation', $err[2]];
default:
return [Exception::class, "engineErrorGeneral", $err[1]." - ".$err[2]];
} }
// no break break;
default: case "mysql":
return [Exception::class, "engineErrorGeneral", $err[2]]; // @codeCoverageIgnore switch ($err[1]) {
case 1205:
return [ExceptionTimeout::class, 'general', $err[2]];
case 1364:
return [ExceptionInput::class, "constraintViolation", $err[2]];
} }
// no break break;
}
return [Exception::class, "engineErrorGeneral", $err[0]."/".$err[1].": ".$err[2]]; // @codeCoverageIgnore
default: default:
return [Exception::class, "engineErrorGeneral", $err[0].": ".$err[2]]; // @codeCoverageIgnore return [Exception::class, "engineErrorGeneral", $err[0].": ".$err[2]]; // @codeCoverageIgnore
} }

View file

@ -15,6 +15,8 @@ use JKingWeb\Arsse\Db\ExceptionTimeout;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
use Dispatch; use Dispatch;
const TRANSACTIONAL_LOCKS = true;
protected $db; protected $db;
protected $transStart = 0; protected $transStart = 0;

View file

@ -14,6 +14,8 @@ use JKingWeb\Arsse\Db\ExceptionTimeout;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
use ExceptionBuilder; use ExceptionBuilder;
const TRANSACTIONAL_LOCKS = true;
const SQLITE_BUSY = 5; const SQLITE_BUSY = 5;
const SQLITE_CONSTRAINT = 19; const SQLITE_CONSTRAINT = 19;
const SQLITE_MISMATCH = 20; const SQLITE_MISMATCH = 20;

View file

@ -22,6 +22,8 @@ return [
'Driver.Db.SQLite3PDO.Name' => 'SQLite 3 (PDO)', 'Driver.Db.SQLite3PDO.Name' => 'SQLite 3 (PDO)',
'Driver.Db.PostgreSQL.Name' => 'PostgreSQL', 'Driver.Db.PostgreSQL.Name' => 'PostgreSQL',
'Driver.Db.PostgreSQLPDO.Name' => 'PostgreSQL (PDO)', '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.Curl.Name' => 'HTTP (curl)',
'Driver.Service.Internal.Name' => 'Internal', 'Driver.Service.Internal.Name' => 'Internal',
'Driver.User.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.engineConstraintViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{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.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.doesNotExist' => 'Could not perform action "{action}" because the user {user} does not exist',
'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed', 'Exception.JKingWeb/Arsse/User/Exception.authMissing' => 'Please log in to proceed',

View file

@ -11,6 +11,7 @@ use JKingWeb\Arsse\Db\Result;
use JKingWeb\Arsse\Test\DatabaseInformation; use JKingWeb\Arsse\Test\DatabaseInformation;
abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
protected static $insertDefaultValues = "INSERT INTO arsse_test default values";
protected static $dbInfo; protected static $dbInfo;
protected static $interface; protected static $interface;
protected $drv; 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 // completely clear the database and ensure the schema version can easily be altered
(static::$dbInfo->razeFunction)(static::$interface, [ (static::$dbInfo->razeFunction)(static::$interface, [
"CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)", "CREATE TABLE arsse_meta(\"key\" varchar(255) primary key not null, value text)",
"INSERT INTO arsse_meta(key,value) values('schema_version','0')", "INSERT INTO arsse_meta(\"key\",value) values('schema_version','0')",
]); ]);
// construct a fresh driver for each test // construct a fresh driver for each test
$this->drv = new static::$dbInfo->driverClass; $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->create);
$this->exec($this->lock); $this->exec($this->lock);
$this->assertException("general", "Db", "ExceptionTimeout"); $this->assertException("general", "Db", "ExceptionTimeout");
$lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock; $this->drv->exec("INSERT INTO arsse_meta(\"key\", value) values('lock', '1')");
$this->drv->exec($lock);
} }
public function testExecConstraintViolation() { public function testExecConstraintViolation() {
$this->drv->exec("CREATE TABLE arsse_test(id varchar(255) not null)"); $this->drv->exec("CREATE TABLE arsse_test(id varchar(255) not null)");
$this->assertException("constraintViolation", "Db", "ExceptionInput"); $this->assertException("constraintViolation", "Db", "ExceptionInput");
$this->drv->exec("INSERT INTO arsse_test default values"); $this->drv->exec(static::$insertDefaultValues);
} }
public function testExecTypeViolation() { 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"); $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() { public function testQueryConstraintViolation() {
$this->drv->exec("CREATE TABLE arsse_test(id integer not null)"); $this->drv->exec("CREATE TABLE arsse_test(id integer not null)");
$this->assertException("constraintViolation", "Db", "ExceptionInput"); $this->assertException("constraintViolation", "Db", "ExceptionInput");
$this->drv->query("INSERT INTO arsse_test default values"); $this->drv->query(static::$insertDefaultValues);
} }
public function testQueryTypeViolation() { public function testQueryTypeViolation() {
@ -220,23 +212,21 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
public function testBeginATransaction() { public function testBeginATransaction() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr = $this->drv->begin(); $tr = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $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(2, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
} }
public function testCommitATransaction() { public function testCommitATransaction() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr = $this->drv->begin(); $tr = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr->commit(); $tr->commit();
@ -246,10 +236,9 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
public function testRollbackATransaction() { public function testRollbackATransaction() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr = $this->drv->begin(); $tr = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr->rollback(); $tr->rollback();
@ -259,28 +248,26 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
public function testBeginChainedTransactions() { public function testBeginChainedTransactions() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr1 = $this->drv->begin(); $tr1 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2 = $this->drv->begin(); $tr2 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(2, $this->drv->query($select)->getValue()); $this->assertEquals(2, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
} }
public function testCommitChainedTransactions() { public function testCommitChainedTransactions() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr1 = $this->drv->begin(); $tr1 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2 = $this->drv->begin(); $tr2 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(2, $this->drv->query($select)->getValue()); $this->assertEquals(2, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2->commit(); $tr2->commit();
@ -291,14 +278,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
public function testCommitChainedTransactionsOutOfOrder() { public function testCommitChainedTransactionsOutOfOrder() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr1 = $this->drv->begin(); $tr1 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2 = $this->drv->begin(); $tr2 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(2, $this->drv->query($select)->getValue()); $this->assertEquals(2, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr1->commit(); $tr1->commit();
@ -308,14 +294,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
public function testRollbackChainedTransactions() { public function testRollbackChainedTransactions() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr1 = $this->drv->begin(); $tr1 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2 = $this->drv->begin(); $tr2 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(2, $this->drv->query($select)->getValue()); $this->assertEquals(2, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2->rollback(); $tr2->rollback();
@ -328,14 +313,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
public function testRollbackChainedTransactionsOutOfOrder() { public function testRollbackChainedTransactionsOutOfOrder() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr1 = $this->drv->begin(); $tr1 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2 = $this->drv->begin(); $tr2 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(2, $this->drv->query($select)->getValue()); $this->assertEquals(2, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr1->rollback(); $tr1->rollback();
@ -348,14 +332,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
public function testPartiallyRollbackChainedTransactions() { public function testPartiallyRollbackChainedTransactions() {
$select = "SELECT count(*) FROM arsse_test"; $select = "SELECT count(*) FROM arsse_test";
$insert = "INSERT INTO arsse_test default values";
$this->drv->exec($this->create); $this->drv->exec($this->create);
$tr1 = $this->drv->begin(); $tr1 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(1, $this->drv->query($select)->getValue()); $this->assertEquals(1, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2 = $this->drv->begin(); $tr2 = $this->drv->begin();
$this->drv->query($insert); $this->drv->query(static::$insertDefaultValues);
$this->assertEquals(2, $this->drv->query($select)->getValue()); $this->assertEquals(2, $this->drv->query($select)->getValue());
$this->assertEquals(0, $this->query($select)); $this->assertEquals(0, $this->query($select));
$tr2->rollback(); $tr2->rollback();

View file

@ -10,6 +10,7 @@ use JKingWeb\Arsse\Db\Result;
use JKingWeb\Arsse\Test\DatabaseInformation; use JKingWeb\Arsse\Test\DatabaseInformation;
abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
protected static $insertDefault = "INSERT INTO arsse_test default values";
protected static $dbInfo; protected static $dbInfo;
protected static $interface; protected static $interface;
protected $resultClass; protected $resultClass;
@ -57,17 +58,17 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
public function testGetChangeCountAndLastInsertId() { public function testGetChangeCountAndLastInsertId() {
$this->makeResult(static::$createMeta); $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(1, $r->changes());
$this->assertSame(0, $r->lastId()); $this->assertSame(0, $r->lastId());
} }
public function testGetChangeCountAndLastInsertIdBis() { public function testGetChangeCountAndLastInsertIdBis() {
$this->makeResult(static::$createTest); $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->changes());
$this->assertSame(1, $r->lastId()); $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(1, $r->changes());
$this->assertSame(2, $r->lastId()); $this->assertSame(2, $r->lastId());
} }

View file

@ -124,8 +124,8 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
} }
public function testViolateConstraint() { 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(); (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"])); $s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(\"key\") values(?)", ["str"]));
$this->assertException("constraintViolation", "Db", "ExceptionInput"); $this->assertException("constraintViolation", "Db", "ExceptionInput");
$s->runArray([null]); $s->runArray([null]);
} }

View file

@ -81,7 +81,7 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
} }
public function testLoadIncompleteFile() { 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->assertException("updateFileIncomplete", "Db");
$this->drv->schemaUpdate(1, $this->base); $this->drv->schemaUpdate(1, $this->base);
} }
@ -100,7 +100,7 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
public function testPerformPartialUpdate() { public function testPerformPartialUpdate() {
file_put_contents($this->path."0.sql", static::$minimal1); 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"); $this->assertException("updateFileIncomplete", "Db");
try { try {
$this->drv->schemaUpdate(2, $this->base); $this->drv->schemaUpdate(2, $this->base);

View 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)";
}

View 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];
}
}

View 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;
}
}
}

View 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';";
}

View file

@ -48,6 +48,9 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase {
'dbPostgreSQLPass' => "arsse_test", 'dbPostgreSQLPass' => "arsse_test",
'dbPostgreSQLDb' => "arsse_test", 'dbPostgreSQLDb' => "arsse_test",
'dbPostgreSQLSchema' => "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); Arsse::$conf = ($force ? null : Arsse::$conf) ?? (new Conf)->import($defaults)->import($conf);
} }

View file

@ -27,7 +27,7 @@ class DatabaseInformation {
if (!isset(self::$data)) { if (!isset(self::$data)) {
self::$data = self::getData(); self::$data = self::getData();
} }
if (!isset(self::$data[$name])) { if (!array_key_exists($name, self::$data)) {
throw new \Exception("Invalid database driver name"); throw new \Exception("Invalid database driver name");
} }
$this->name = $name; $this->name = $name;
@ -162,6 +162,48 @@ class DatabaseInformation {
$pgExecFunction($db, $st); $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 [ return [
'SQLite 3' => [ 'SQLite 3' => [
'pdo' => false, 'pdo' => false,
@ -244,6 +286,34 @@ class DatabaseInformation {
'truncateFunction' => $pgTruncateFunction, 'truncateFunction' => $pgTruncateFunction,
'razeFunction' => $pgRazeFunction, '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,
],
]; ];
} }
} }

View file

@ -68,6 +68,12 @@
<file>cases/Db/PostgreSQLPDO/TestCreation.php</file> <file>cases/Db/PostgreSQLPDO/TestCreation.php</file>
<file>cases/Db/PostgreSQLPDO/TestDriver.php</file> <file>cases/Db/PostgreSQLPDO/TestDriver.php</file>
<file>cases/Db/PostgreSQLPDO/TestUpdate.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>
<testsuite name="Database functions"> <testsuite name="Database functions">
<file>cases/Db/SQLite3/TestDatabase.php</file> <file>cases/Db/SQLite3/TestDatabase.php</file>