diff --git a/lib/AbstractException.php b/lib/AbstractException.php
index 4eee0ffe..01c4e1be 100644
--- a/lib/AbstractException.php
+++ b/lib/AbstractException.php
@@ -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,
diff --git a/lib/Conf.php b/lib/Conf.php
index 4572cd39..d252a871 100644
--- a/lib/Conf.php
+++ b/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;
diff --git a/lib/Db/AbstractDriver.php b/lib/Db/AbstractDriver.php
index 6dbfb0a5..7ce30e02 100644
--- a/lib/Db/AbstractDriver.php
+++ b/lib/Db/AbstractDriver.php
@@ -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;
diff --git a/lib/Db/MySQL/Driver.php b/lib/Db/MySQL/Driver.php
new file mode 100644
index 00000000..80feff70
--- /dev/null
+++ b/lib/Db/MySQL/Driver.php
@@ -0,0 +1,163 @@
+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 {
+ }
+}
diff --git a/lib/Db/MySQL/PDODriver.php b/lib/Db/MySQL/PDODriver.php
new file mode 100644
index 00000000..d568468b
--- /dev/null
+++ b/lib/Db/MySQL/PDODriver.php
@@ -0,0 +1,59 @@
+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");
+ }
+}
diff --git a/lib/Db/PDOError.php b/lib/Db/PDOError.php
index 7e8252d2..6cb615c9 100644
--- a/lib/Db/PDOError.php
+++ b/lib/Db/PDOError.php
@@ -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
}
diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php
index 89e7a7cb..986eeed3 100644
--- a/lib/Db/PostgreSQL/Driver.php
+++ b/lib/Db/PostgreSQL/Driver.php
@@ -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;
diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php
index cee1b1ca..95fd7d9d 100644
--- a/lib/Db/SQLite3/Driver.php
+++ b/lib/Db/SQLite3/Driver.php
@@ -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;
diff --git a/locale/en.php b/locale/en.php
index dc5381f1..51cb71da 100644
--- a/locale/en.php
+++ b/locale/en.php
@@ -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',
diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php
index 5f309916..91992e45 100644
--- a/tests/cases/Db/BaseDriver.php
+++ b/tests/cases/Db/BaseDriver.php
@@ -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();
diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php
index bb1725bf..d849060c 100644
--- a/tests/cases/Db/BaseResult.php
+++ b/tests/cases/Db/BaseResult.php
@@ -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());
}
diff --git a/tests/cases/Db/BaseStatement.php b/tests/cases/Db/BaseStatement.php
index c3c3704e..15abab3c 100644
--- a/tests/cases/Db/BaseStatement.php
+++ b/tests/cases/Db/BaseStatement.php
@@ -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]);
}
diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php
index 28cde468..54855196 100644
--- a/tests/cases/Db/BaseUpdate.php
+++ b/tests/cases/Db/BaseUpdate.php
@@ -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);
diff --git a/tests/cases/Db/MySQLPDO/TestDriver.php b/tests/cases/Db/MySQLPDO/TestDriver.php
new file mode 100644
index 00000000..3873338c
--- /dev/null
+++ b/tests/cases/Db/MySQLPDO/TestDriver.php
@@ -0,0 +1,20 @@
+
+ * @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)";
+}
diff --git a/tests/cases/Db/MySQLPDO/TestResult.php b/tests/cases/Db/MySQLPDO/TestResult.php
new file mode 100644
index 00000000..7777f994
--- /dev/null
+++ b/tests/cases/Db/MySQLPDO/TestResult.php
@@ -0,0 +1,25 @@
+
+ */
+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];
+ }
+}
diff --git a/tests/cases/Db/MySQLPDO/TestStatement.php b/tests/cases/Db/MySQLPDO/TestStatement.php
new file mode 100644
index 00000000..6be598a5
--- /dev/null
+++ b/tests/cases/Db/MySQLPDO/TestStatement.php
@@ -0,0 +1,33 @@
+
+ * @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;
+ }
+ }
+}
diff --git a/tests/cases/Db/MySQLPDO/TestUpdate.php b/tests/cases/Db/MySQLPDO/TestUpdate.php
new file mode 100644
index 00000000..d168f5ce
--- /dev/null
+++ b/tests/cases/Db/MySQLPDO/TestUpdate.php
@@ -0,0 +1,17 @@
+
+ * @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';";
+}
diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php
index 9ec5ae57..80bebe11 100644
--- a/tests/lib/AbstractTest.php
+++ b/tests/lib/AbstractTest.php
@@ -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);
}
diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php
index ea70afc0..0350362f 100644
--- a/tests/lib/DatabaseInformation.php
+++ b/tests/lib/DatabaseInformation.php
@@ -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,
+ ],
];
}
}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index ceca94ac..3410ac82 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -68,6 +68,12 @@
cases/Db/PostgreSQLPDO/TestCreation.php
cases/Db/PostgreSQLPDO/TestDriver.php
cases/Db/PostgreSQLPDO/TestUpdate.php
+
+ cases/Db/MySQLPDO/TestResult.php
+ cases/Db/MySQLPDO/TestStatement.php
+ cases/Db/MySQLPDO/TestCreation.php
+ cases/Db/MySQLPDO/TestDriver.php
+ cases/Db/MySQLPDO/TestUpdate.php
cases/Db/SQLite3/TestDatabase.php