From 796315c00c249422d427f0267bd514e03be1f840 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 10 Nov 2018 00:02:38 -0500 Subject: [PATCH 01/58] Basic stub of PDO-base PostgreSQL driver --- lib/Conf.php | 12 ++++ lib/Db/PostgreSQL/Driver.php | 124 ++++++++++++++++++++++++++++++++ lib/Db/PostgreSQL/PDODriver.php | 47 ++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 lib/Db/PostgreSQL/Driver.php create mode 100644 lib/Db/PostgreSQL/PDODriver.php diff --git a/lib/Conf.php b/lib/Conf.php index 3d4eeb23..9173d3cb 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -25,6 +25,18 @@ class Conf { public $dbSQLite3Key = ""; /** @var integer Number of seconds for SQLite to wait before returning a timeout error when writing to the database */ public $dbSQLite3Timeout = 60; + /** @var string Host name, address, or socket path of PostgreSQL database server (if using PostgreSQL) */ + public $dbPostgreSQLHost = ""; + /** @var string Log-in user name for PostgreSQL database server (if using PostgreSQL) */ + public $dbPostgreSQLUser = "arsse"; + /** @var string Log-in password for PostgreSQL database server (if using PostgreSQL) */ + public $dbPostgreSQLPass = ""; + /** @var integer Listening port for PostgreSQL database server (if using PostgreSQL over TCP) */ + public $dbPostgreSQLPort = 5432; + /** @var string Database name on PostgreSQL database server (if using PostgreSQL) */ + public $dbPostgreSQLDb = "arsse"; + /** @var string Schema name on PostgreSQL database server (if using PostgreSQL) */ + public $dbPostgreSQLSchema = ""; /** @var string Class of the user management driver in use (Internal by default) */ public $userDriver = User\Internal\Driver::class; diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php new file mode 100644 index 00000000..2ba5baa0 --- /dev/null +++ b/lib/Db/PostgreSQL/Driver.php @@ -0,0 +1,124 @@ +dbPostgreSQLUser; + $pass = $pass ?? Arsse::$conf->dbPostgreSQLPass; + $db = $db ?? Arsse::$conf->dbPostgreSQLDb; + $host = $host ?? Arsse::$conf->dbPostgreSQLHost; + $port = $port ?? Arsse::$conf->dbPostgreSQLPort; + $schema = $schema ?? Arsse::$conf->dbPostgreSQLSchema; + $this->makeConnection($user, $pass, $db, $host, $port); + } + + public static function requirementsMet(): bool { + // stub: native interface is not yet supported + return false; + } + + protected function makeConnection(string $user, string $pass, string $db, string $host, int $port) { + // stub: native interface is not yet supported + throw new \Exception; + } + + protected function makeConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port): string { + $out = ['dbname' => $db]; + if ($host != "") { + $out['host'] = $host; + $out['port'] = (string) $port; + } + if (!$pdo) { + $out['user'] = $user; + $out['password'] = $pass; + } + $out = array_map(function($v, $k) { + return "$k='".str_replace("'", "\\'", str_replace("\\", "\\\\", $v))."'"; + }, $out, array_keys($out)); + return implode(($pdo ? ";" : " "), $out); + } + + public function __destruct() { + } + + /** @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 driverName(): string { + return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name"); + } + + public static function schemaID(): string { + return "PostgreSQL"; + } + + public function schemaVersion(): int { + // stub + return 0; + } + + public function schemaUpdate(int $to, string $basePath = null): bool { + // stub + return false; + } + + public function charsetAcceptable(): bool { + // stub + return true; + } + + protected function getError(): string { + // stub + return ""; + } + + public function exec(string $query): bool { + // stub + return true; + } + + public function query(string $query): \JKingWeb\Arsse\Db\Result { + // stub + return new ResultEmpty; + } + + public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { + // stub + return new Statement($this->db, $s, $paramTypes); + } + + protected function lock(): bool { + // stub + return true; + } + + protected function unlock(bool $rollback = false): bool { + // stub + return true; + } +} diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php new file mode 100644 index 00000000..fd43780e --- /dev/null +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -0,0 +1,47 @@ +makeconnectionString(true, $user, $pass, $db, $host, $port); + $this->db = new \PDO("pgsql:$dsn", $user, $pass); + } + + 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.PostgreSQLPDO.Name"); + } +} From edfae438faa71677142c8145d3c8f03d5fd3b7f2 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 16 Nov 2018 21:20:54 -0500 Subject: [PATCH 02/58] Refine pg connection strings --- lib/Conf.php | 4 +- lib/Db/PostgreSQL/Driver.php | 43 +++++++++++++------ lib/Db/PostgreSQL/PDODriver.php | 4 +- locale/en.php | 2 + tests/cases/Db/PostgreSQL/TestDriver.php | 54 ++++++++++++++++++++++++ tests/phpunit.xml | 2 + 6 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 tests/cases/Db/PostgreSQL/TestDriver.php diff --git a/lib/Conf.php b/lib/Conf.php index 9173d3cb..f15926c3 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -35,8 +35,10 @@ class Conf { public $dbPostgreSQLPort = 5432; /** @var string Database name on PostgreSQL database server (if using PostgreSQL) */ public $dbPostgreSQLDb = "arsse"; - /** @var string Schema name on PostgreSQL database server (if using PostgreSQL) */ + /** @var string Schema name in PostgreSQL database (if using PostgreSQL) */ 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 Class of the user management driver in use (Internal by default) */ public $userDriver = User\Internal\Driver::class; diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 2ba5baa0..7e763e84 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -14,7 +14,7 @@ use JKingWeb\Arsse\Db\ExceptionTimeout; class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { - public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null) { + public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) { // check to make sure required extension is loaded if (!static::requirementsMet()) { throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore @@ -25,7 +25,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $host = $host ?? Arsse::$conf->dbPostgreSQLHost; $port = $port ?? Arsse::$conf->dbPostgreSQLPort; $schema = $schema ?? Arsse::$conf->dbPostgreSQLSchema; - $this->makeConnection($user, $pass, $db, $host, $port); + $service = $service ?? Arsse::$conf->dbPostgreSQLService; + $this->makeConnection($user, $pass, $db, $host, $port, $service); } public static function requirementsMet(): bool { @@ -38,20 +39,38 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { throw new \Exception; } - protected function makeConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port): string { - $out = ['dbname' => $db]; - if ($host != "") { - $out['host'] = $host; - $out['port'] = (string) $port; - } - if (!$pdo) { - $out['user'] = $user; - $out['password'] = $pass; + public static function makeConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service): string { + $base = [ + 'client_encoding' => "UTF8", + 'application_name' => "arsse", + ]; + $out = []; + if ($service != "") { + $out['service'] = $service; + } else { + if ($host != "") { + $out['host'] = $host; + } + if ($port != 5432 && !($host != "" && $host[0] == "/")) { + $out['port'] = (string) $port; + } + if ($db != "") { + $out['dbname'] = $db; + } + if (!$pdo) { + $out['user'] = $user; + if ($pass != "") { + $out['password'] = $pass; + } + } } + ksort($out); + ksort($base); + $out = array_merge($out, $base); $out = array_map(function($v, $k) { return "$k='".str_replace("'", "\\'", str_replace("\\", "\\\\", $v))."'"; }, $out, array_keys($out)); - return implode(($pdo ? ";" : " "), $out); + return implode(" ", $out); } public function __destruct() { diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php index fd43780e..9bb630ec 100644 --- a/lib/Db/PostgreSQL/PDODriver.php +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -20,8 +20,8 @@ class PDODriver extends Driver { return class_exists("PDO") && in_array("pgsql", \PDO::getAvailableDrivers()); } - protected function makeConnection(string $user, string $pass, string $db, string $host, int $port) { - $dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port); + protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { + $dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port, $service); $this->db = new \PDO("pgsql:$dsn", $user, $pass); } diff --git a/locale/en.php b/locale/en.php index 55a0bd32..50b8b5fc 100644 --- a/locale/en.php +++ b/locale/en.php @@ -20,6 +20,8 @@ return [ 'Driver.Db.SQLite3.Name' => 'SQLite 3', 'Driver.Db.SQLite3PDO.Name' => 'SQLite 3 (PDO)', + 'Driver.Db.PostgreSQL.Name' => 'PostgreSQL', + 'Driver.Db.PostgreSQLPDO.Name' => 'PostgreSQL (PDO)', 'Driver.Service.Curl.Name' => 'HTTP (curl)', 'Driver.Service.Internal.Name' => 'Internal', 'Driver.User.Internal.Name' => 'Internal', diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php new file mode 100644 index 00000000..59e113bb --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -0,0 +1,54 @@ + */ +class TestDriver extends \JKingWeb\Arsse\Test\AbstractTest { + /** @dataProvider provideConnectionStrings */ + public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { + $postfix = "application_name='arsse' client_encoding='UTF8'"; + $act = Driver::makeConnectionString($pdo, $user, $pass, $db, $host, $port, $service); + if ($act==$postfix) { + $this->assertSame($exp, ""); + } else { + $test = substr($act, 0, strlen($act) - (strlen($postfix) + 1) ); + $check = substr($act, strlen($test) + 1); + $this->assertSame($postfix, $check); + $this->assertSame($exp, $test); + } + } + + public function provideConnectionStrings() { + return [ + [false, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse' password='secret' user='arsse'"], + [false, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse' password='p word' user='arsse'"], + [false, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse' password='p\\'word' user='arsse'"], + [false, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db' password='secret' user='arsse user'"], + [false, "arsse", "secret", "", "", 5432, "", "password='secret' user='arsse'"], + [false, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost' password='secret' user='arsse'"], + [false, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' password='secret' port='9999' user='arsse'"], + [false, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' password='secret' port='9999' user='arsse'"], + [false, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket' password='secret' user='arsse'"], + [false, "T'Pau of Vulcan", "", "", "", 5432, "", "user='T\\'Pau of Vulcan'"], + [false, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"], + [true, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse'"], + [true, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse'"], + [true, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse'"], + [true, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db'"], + [true, "arsse", "secret", "", "", 5432, "", ""], + [true, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost'"], + [true, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' port='9999'"], + [true, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' port='9999'"], + [true, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket'"], + [true, "T'Pau of Vulcan", "", "", "", 5432, "", ""], + [true, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"], + ]; + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index f2a49675..564ced7b 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -55,6 +55,8 @@ cases/Db/SQLite3PDO/TestCreation.php cases/Db/SQLite3PDO/TestDriver.php cases/Db/SQLite3PDO/TestUpdate.php + + cases/Db/PostgreSQL/TestDriver.php cases/Db/SQLite3/Database/TestMiscellany.php From 976672de5bac96bf3956d0b3838530a47cf1c9fa Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 16 Nov 2018 21:32:27 -0500 Subject: [PATCH 03/58] Test cleanup --- tests/cases/Db/SQLite3/TestCreation.php | 2 +- tests/cases/Db/SQLite3/TestUpdate.php | 11 +++-------- tests/cases/Db/SQLite3PDO/TestCreation.php | 2 +- tests/cases/Db/SQLite3PDO/TestUpdate.php | 14 ++++---------- tests/cases/Db/TestResult.php | 6 ++++-- tests/cases/Db/TestStatement.php | 6 ++++-- tests/lib/AbstractTest.php | 8 +++++++- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/tests/cases/Db/SQLite3/TestCreation.php b/tests/cases/Db/SQLite3/TestCreation.php index f9ac55b0..86ba40be 100644 --- a/tests/cases/Db/SQLite3/TestCreation.php +++ b/tests/cases/Db/SQLite3/TestCreation.php @@ -107,7 +107,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { chmod($path."Awal/arsse.db-wal", 0111); chmod($path."Ashm/arsse.db-shm", 0111); // set up configuration - $this->setConf(['dbSQLite3File' => ":memory:"]); + $this->setConf(); } public function tearDown() { diff --git a/tests/cases/Db/SQLite3/TestUpdate.php b/tests/cases/Db/SQLite3/TestUpdate.php index 2ff02e80..7347eca3 100644 --- a/tests/cases/Db/SQLite3/TestUpdate.php +++ b/tests/cases/Db/SQLite3/TestUpdate.php @@ -25,16 +25,13 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { const MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; const MINIMAL2 = "pragma user_version=2"; - public function setUp(Conf $conf = null) { + public function setUp(array $conf = []) { if (!Driver::requirementsMet()) { $this->markTestSkipped("SQLite extension not loaded"); } $this->clearData(); $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); - $conf = $conf ?? new Conf; - $conf->dbDriver = Driver::class; - $conf->dbSQLite3File = ":memory:"; - Arsse::$conf = $conf; + $this->setConf($conf); $this->base = $this->vfs->url(); $this->path = $this->base."/SQLite3/"; $this->drv = new Driver(); @@ -109,9 +106,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { public function testDeclineManualUpdate() { // turn auto-updating off - $conf = new Conf; - $conf->dbAutoUpdate = false; - $this->setUp($conf); + $this->setUp(['dbAutoUpdate' => false]); $this->assertException("updateManual", "Db"); $this->drv->schemaUpdate(Database::SCHEMA_VERSION); } diff --git a/tests/cases/Db/SQLite3PDO/TestCreation.php b/tests/cases/Db/SQLite3PDO/TestCreation.php index 4f2e1a2b..bec51619 100644 --- a/tests/cases/Db/SQLite3PDO/TestCreation.php +++ b/tests/cases/Db/SQLite3PDO/TestCreation.php @@ -108,7 +108,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { chmod($path."Awal/arsse.db-wal", 0111); chmod($path."Ashm/arsse.db-shm", 0111); // set up configuration - $this->setConf(['dbSQLite3File' => ":memory:"]); + $this->setConf(); } public function tearDown() { diff --git a/tests/cases/Db/SQLite3PDO/TestUpdate.php b/tests/cases/Db/SQLite3PDO/TestUpdate.php index 9c8df845..d58f971c 100644 --- a/tests/cases/Db/SQLite3PDO/TestUpdate.php +++ b/tests/cases/Db/SQLite3PDO/TestUpdate.php @@ -25,18 +25,14 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { const MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; const MINIMAL2 = "pragma user_version=2"; - public function setUp(Conf $conf = null) { + public function setUp(array $conf = []) { if (!PDODriver::requirementsMet()) { $this->markTestSkipped("PDO-SQLite extension not loaded"); } $this->clearData(); $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); - if (!$conf) { - $conf = new Conf(); - } - $conf->dbDriver = PDODriver::class; - $conf->dbSQLite3File = ":memory:"; - Arsse::$conf = $conf; + $conf['dbDriver'] = PDODriver::class; + $this->setConf($conf); $this->base = $this->vfs->url(); $this->path = $this->base."/SQLite3/"; $this->drv = new PDODriver(); @@ -111,9 +107,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { public function testDeclineManualUpdate() { // turn auto-updating off - $conf = new Conf(); - $conf->dbAutoUpdate = false; - $this->setUp($conf); + $this->setUp(['dbAutoUpdate' => false]); $this->assertException("updateManual", "Db"); $this->drv->schemaUpdate(Database::SCHEMA_VERSION); } diff --git a/tests/cases/Db/TestResult.php b/tests/cases/Db/TestResult.php index 9a252b80..eb2e695f 100644 --- a/tests/cases/Db/TestResult.php +++ b/tests/cases/Db/TestResult.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db; +use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Db\Result; use JKingWeb\Arsse\Db\PDOResult; use JKingWeb\Arsse\Db\SQLite3\PDODriver; @@ -16,16 +17,17 @@ use JKingWeb\Arsse\Db\SQLite3\PDODriver; */ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { public function provideDrivers() { + $this->setConf(); $drvSqlite3 = (function() { if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { - $d = new \SQLite3(":memory:"); + $d = new \SQLite3(Arsse::$conf->dbSQLite3File); $d->enableExceptions(true); return $d; } })(); $drvPdo = (function() { if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { - return new \PDO("sqlite::memory:", "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + return new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); } })(); return [ diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/TestStatement.php index 94a48858..e83106a6 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/TestStatement.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db; +use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Db\Statement; use JKingWeb\Arsse\Db\PDOStatement; @@ -16,16 +17,17 @@ use JKingWeb\Arsse\Db\PDOStatement; * @covers \JKingWeb\Arsse\Db\PDOError */ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { public function provideDrivers() { + $this->setConf(); $drvSqlite3 = (function() { if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { - $d = new \SQLite3(":memory:"); + $d = new \SQLite3(Arsse::$conf->dbSQLite3File); $d->enableExceptions(true); return $d; } })(); $drvPdo = (function() { if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { - return new \PDO("sqlite::memory:", "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + return new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); } })(); return [ diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index a75c339a..7ad4c7c2 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -41,7 +41,13 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } public function setConf(array $conf = []) { - Arsse::$conf = (new Conf)->import($conf); + $defaults = [ + 'dbSQLite3File' => ":memory:", + 'dbPostgreSQLUser' => "arsse_test", + 'dbPostgreSQLPass' => "arsse_test", + 'dbPostgreSQLDb' => "arsse_test", + ]; + Arsse::$conf = (new Conf)->import($defaults)->import($conf); } public function assertException(string $msg = "", string $prefix = "", string $type = "Exception") { From e30d82fbaa2ed15eff7b1f496ec673b72528ade4 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 16 Nov 2018 21:35:05 -0500 Subject: [PATCH 04/58] Correct signature --- lib/Db/PostgreSQL/Driver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 7e763e84..99154ede 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -34,7 +34,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return false; } - protected function makeConnection(string $user, string $pass, string $db, string $host, int $port) { + protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { // stub: native interface is not yet supported throw new \Exception; } From b5733b070c373bf509f9297efcc84c9525c079ee Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 20 Nov 2018 15:45:20 -0500 Subject: [PATCH 05/58] Clean up statement tests PostgreSQL tests are suppressed for now, but most pass. --- lib/Db/AbstractStatement.php | 14 +- tests/cases/Db/TestStatement.php | 389 +++++++++++++++++-------------- 2 files changed, 222 insertions(+), 181 deletions(-) diff --git a/lib/Db/AbstractStatement.php b/lib/Db/AbstractStatement.php index 57185ee8..acc9650f 100644 --- a/lib/Db/AbstractStatement.php +++ b/lib/Db/AbstractStatement.php @@ -74,20 +74,26 @@ abstract class AbstractStatement implements Statement { } } - protected function bindValues(array $values, int $offset = 0): int { - $a = $offset; + protected function bindValues(array $values, int $offset = null): int { + $a = (int) $offset; foreach ($values as $value) { if (is_array($value)) { // recursively flatten any arrays, which may be provided for SET or IN() clauses $a += $this->bindValues($value, $a); } elseif (array_key_exists($a, $this->types)) { $value = $this->cast($value, $this->types[$a], $this->isNullable[$a]); - $this->bindValue($value, $this->types[$a], $a+1); - $a++; + $this->bindValue($value, $this->types[$a], ++$a); } else { throw new Exception("paramTypeMissing", $a+1); } } + // once the last value is bound, check that all parameters have been supplied values and bind null for any missing ones + // SQLite will happily substitute null for a missing value, but other engines (viz. PostgreSQL) produce an error + if (is_null($offset)) { + while ($a < sizeof($this->types)) { + $this->bindValue(null, $this->types[$a], ++$a); + } + } return $a - $offset; } } diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/TestStatement.php index e83106a6..59759395 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/TestStatement.php @@ -25,6 +25,12 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { return $d; } })(); + $drvPgsql = (function() { + if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) { + $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); + return new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + } + })(); $drvPdo = (function() { if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { return new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); @@ -35,10 +41,14 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $s = $drvSqlite3->prepare($query); return [$drvSqlite3, $s, $types]; }], - 'PDO' => [isset($drvPdo), true, \JKingWeb\Arsse\Db\PDOStatement::class, function(string $query, array $types = []) use($drvPdo) { + 'PDO SQLite 3' => [isset($drvPdo), true, \JKingWeb\Arsse\Db\PDOStatement::class, function(string $query, array $types = []) use($drvPdo) { $s = $drvPdo->prepare($query); return [$drvPdo, $s, $types]; }], + /*'PDO PostgreSQL' => [isset($drvPgsql), true, \JKingWeb\Arsse\Db\PDOStatement::class, function(string $query, array $types = []) use($drvPgsql) { + $s = $drvPgsql->prepare($query); + return [$drvPgsql, $s, $types]; + }],*/ ]; } @@ -51,23 +61,20 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideBindings */ - public function testBindATypedValue(bool $driverTestable, string $class, \Closure $func, $value, string $type, string $exp, string $expPDO = null) { + public function testBindATypedValue(bool $driverTestable, string $class, \Closure $func, $value, string $type, string $exp) { if (!$driverTestable) { $this->markTestSkipped(); } - $exp = ($class == PDOStatement::class) ? ($expPDO ?? $exp) : $exp; + if ($exp=="null") { + $query = "SELECT (cast(? as text) is null) as pass"; + } else { + $query = "SELECT ($exp = ?) as pass"; + } $typeStr = "'".str_replace("'", "''", $type)."'"; - $s = new $class(...$func( - "SELECT ( - (CASE WHEN substr($typeStr, 0, 7) <> 'strict ' then null else 1 end) is null - and ? is null - ) or ( - $exp = ? - ) as pass" - )); - $s->retype(...[$type, $type]); - $act = (bool) $s->run(...[$value, $value])->getRow()['pass']; - $this->assertTrue($act); + $s = new $class(...$func($query)); + $s->retype(...[$type]); + $act = $s->run(...[$value])->getValue(); + $this->assertTrue((bool) $act); } /** @dataProvider provideDrivers */ @@ -75,7 +82,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { if (!$driverTestable) { $this->markTestSkipped(); } - $s = new $class(...$func("SELECT ? as value")); + $s = new $class(...$func("SELECT ? as value", ["int"])); $val = $s->runArray()->getRow()['value']; $this->assertSame(null, $val); } @@ -150,184 +157,212 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC")); $tests = [ /* input, type, expected binding as SQL fragment */ - 'Null as integer' => [null, "integer", "null"], - 'Null as float' => [null, "float", "null"], - 'Null as string' => [null, "string", "null"], - 'Null as binary' => [null, "binary", "null"], - 'Null as datetime' => [null, "datetime", "null"], - 'Null as boolean' => [null, "boolean", "null"], - 'Null as strict integer' => [null, "strict integer", "0"], - 'Null as strict float' => [null, "strict float", "0.0", "'0'"], - 'Null as strict string' => [null, "strict string", "''"], - 'Null as strict binary' => [null, "strict binary", "x''"], + 'Null as integer' => [null, "integer", "null"], + 'Null as float' => [null, "float", "null"], + 'Null as string' => [null, "string", "null"], + 'Null as binary' => [null, "binary", "null"], + 'Null as datetime' => [null, "datetime", "null"], + 'Null as boolean' => [null, "boolean", "null"], + 'Null as strict integer' => [null, "strict integer", "0"], + 'Null as strict float' => [null, "strict float", "0.0"], + 'Null as strict string' => [null, "strict string", "''"], + 'Null as strict binary' => [null, "strict binary", "x''"], 'Null as strict datetime' => [null, "strict datetime", "'1970-01-01 00:00:00'"], - 'Null as strict boolean' => [null, "strict boolean", "0"], - 'True as integer' => [true, "integer", "1"], - 'True as float' => [true, "float", "1.0", "'1'"], - 'True as string' => [true, "string", "'1'"], - 'True as binary' => [true, "binary", "x'31'"], - 'True as datetime' => [true, "datetime", "null"], - 'True as boolean' => [true, "boolean", "1"], - 'True as strict integer' => [true, "strict integer", "1"], - 'True as strict float' => [true, "strict float", "1.0", "'1'"], - 'True as strict string' => [true, "strict string", "'1'"], - 'True as strict binary' => [true, "strict binary", "x'31'"], + 'Null as strict boolean' => [null, "strict boolean", "0"], + 'True as integer' => [true, "integer", "1"], + 'True as float' => [true, "float", "1.0"], + 'True as string' => [true, "string", "'1'"], + 'True as binary' => [true, "binary", "x'31'"], + 'True as datetime' => [true, "datetime", "null"], + 'True as boolean' => [true, "boolean", "1"], + 'True as strict integer' => [true, "strict integer", "1"], + 'True as strict float' => [true, "strict float", "1.0"], + 'True as strict string' => [true, "strict string", "'1'"], + 'True as strict binary' => [true, "strict binary", "x'31'"], 'True as strict datetime' => [true, "strict datetime", "'1970-01-01 00:00:00'"], - 'True as strict boolean' => [true, "strict boolean", "1"], - 'False as integer' => [false, "integer", "0"], - 'False as float' => [false, "float", "0.0", "'0'"], - 'False as string' => [false, "string", "''"], - 'False as binary' => [false, "binary", "x''"], - 'False as datetime' => [false, "datetime", "null"], - 'False as boolean' => [false, "boolean", "0"], - 'False as strict integer' => [false, "strict integer", "0"], - 'False as strict float' => [false, "strict float", "0.0", "'0'"], - 'False as strict string' => [false, "strict string", "''"], - 'False as strict binary' => [false, "strict binary", "x''"], + 'True as strict boolean' => [true, "strict boolean", "1"], + 'False as integer' => [false, "integer", "0"], + 'False as float' => [false, "float", "0.0"], + 'False as string' => [false, "string", "''"], + 'False as binary' => [false, "binary", "x''"], + 'False as datetime' => [false, "datetime", "null"], + 'False as boolean' => [false, "boolean", "0"], + 'False as strict integer' => [false, "strict integer", "0"], + 'False as strict float' => [false, "strict float", "0.0"], + 'False as strict string' => [false, "strict string", "''"], + 'False as strict binary' => [false, "strict binary", "x''"], 'False as strict datetime' => [false, "strict datetime", "'1970-01-01 00:00:00'"], - 'False as strict boolean' => [false, "strict boolean", "0"], - 'Integer as integer' => [2112, "integer", "2112"], - 'Integer as float' => [2112, "float", "2112.0", "'2112'"], - 'Integer as string' => [2112, "string", "'2112'"], - 'Integer as binary' => [2112, "binary", "x'32313132'"], - 'Integer as datetime' => [2112, "datetime", "'1970-01-01 00:35:12'"], - 'Integer as boolean' => [2112, "boolean", "1"], - 'Integer as strict integer' => [2112, "strict integer", "2112"], - 'Integer as strict float' => [2112, "strict float", "2112.0", "'2112'"], - 'Integer as strict string' => [2112, "strict string", "'2112'"], - 'Integer as strict binary' => [2112, "strict binary", "x'32313132'"], + 'False as strict boolean' => [false, "strict boolean", "0"], + 'Integer as integer' => [2112, "integer", "2112"], + 'Integer as float' => [2112, "float", "2112.0"], + 'Integer as string' => [2112, "string", "'2112'"], + 'Integer as binary' => [2112, "binary", "x'32313132'"], + 'Integer as datetime' => [2112, "datetime", "'1970-01-01 00:35:12'"], + 'Integer as boolean' => [2112, "boolean", "1"], + 'Integer as strict integer' => [2112, "strict integer", "2112"], + 'Integer as strict float' => [2112, "strict float", "2112.0"], + 'Integer as strict string' => [2112, "strict string", "'2112'"], + 'Integer as strict binary' => [2112, "strict binary", "x'32313132'"], 'Integer as strict datetime' => [2112, "strict datetime", "'1970-01-01 00:35:12'"], - 'Integer as strict boolean' => [2112, "strict boolean", "1"], - 'Integer zero as integer' => [0, "integer", "0"], - 'Integer zero as float' => [0, "float", "0.0", "'0'"], - 'Integer zero as string' => [0, "string", "'0'"], - 'Integer zero as binary' => [0, "binary", "x'30'"], - 'Integer zero as datetime' => [0, "datetime", "'1970-01-01 00:00:00'"], - 'Integer zero as boolean' => [0, "boolean", "0"], - 'Integer zero as strict integer' => [0, "strict integer", "0"], - 'Integer zero as strict float' => [0, "strict float", "0.0", "'0'"], - 'Integer zero as strict string' => [0, "strict string", "'0'"], - 'Integer zero as strict binary' => [0, "strict binary", "x'30'"], + 'Integer as strict boolean' => [2112, "strict boolean", "1"], + 'Integer zero as integer' => [0, "integer", "0"], + 'Integer zero as float' => [0, "float", "0.0"], + 'Integer zero as string' => [0, "string", "'0'"], + 'Integer zero as binary' => [0, "binary", "x'30'"], + 'Integer zero as datetime' => [0, "datetime", "'1970-01-01 00:00:00'"], + 'Integer zero as boolean' => [0, "boolean", "0"], + 'Integer zero as strict integer' => [0, "strict integer", "0"], + 'Integer zero as strict float' => [0, "strict float", "0.0"], + 'Integer zero as strict string' => [0, "strict string", "'0'"], + 'Integer zero as strict binary' => [0, "strict binary", "x'30'"], 'Integer zero as strict datetime' => [0, "strict datetime", "'1970-01-01 00:00:00'"], - 'Integer zero as strict boolean' => [0, "strict boolean", "0"], - 'Float as integer' => [2112.5, "integer", "2112"], - 'Float as float' => [2112.5, "float", "2112.5", "'2112.5'"], - 'Float as string' => [2112.5, "string", "'2112.5'"], - 'Float as binary' => [2112.5, "binary", "x'323131322e35'"], - 'Float as datetime' => [2112.5, "datetime", "'1970-01-01 00:35:12'"], - 'Float as boolean' => [2112.5, "boolean", "1"], - 'Float as strict integer' => [2112.5, "strict integer", "2112"], - 'Float as strict float' => [2112.5, "strict float", "2112.5", "'2112.5'"], - 'Float as strict string' => [2112.5, "strict string", "'2112.5'"], - 'Float as strict binary' => [2112.5, "strict binary", "x'323131322e35'"], + 'Integer zero as strict boolean' => [0, "strict boolean", "0"], + 'Float as integer' => [2112.5, "integer", "2112"], + 'Float as float' => [2112.5, "float", "2112.5"], + 'Float as string' => [2112.5, "string", "'2112.5'"], + 'Float as binary' => [2112.5, "binary", "x'323131322e35'"], + 'Float as datetime' => [2112.5, "datetime", "'1970-01-01 00:35:12'"], + 'Float as boolean' => [2112.5, "boolean", "1"], + 'Float as strict integer' => [2112.5, "strict integer", "2112"], + 'Float as strict float' => [2112.5, "strict float", "2112.5"], + 'Float as strict string' => [2112.5, "strict string", "'2112.5'"], + 'Float as strict binary' => [2112.5, "strict binary", "x'323131322e35'"], 'Float as strict datetime' => [2112.5, "strict datetime", "'1970-01-01 00:35:12'"], - 'Float as strict boolean' => [2112.5, "strict boolean", "1"], - 'Float zero as integer' => [0.0, "integer", "0"], - 'Float zero as float' => [0.0, "float", "0.0", "'0'"], - 'Float zero as string' => [0.0, "string", "'0'"], - 'Float zero as binary' => [0.0, "binary", "x'30'"], - 'Float zero as datetime' => [0.0, "datetime", "'1970-01-01 00:00:00'"], - 'Float zero as boolean' => [0.0, "boolean", "0"], - 'Float zero as strict integer' => [0.0, "strict integer", "0"], - 'Float zero as strict float' => [0.0, "strict float", "0.", "'0'"], - 'Float zero as strict string' => [0.0, "strict string", "'0'"], - 'Float zero as strict binary' => [0.0, "strict binary", "x'30'"], + 'Float as strict boolean' => [2112.5, "strict boolean", "1"], + 'Float zero as integer' => [0.0, "integer", "0"], + 'Float zero as float' => [0.0, "float", "0.0"], + 'Float zero as string' => [0.0, "string", "'0'"], + 'Float zero as binary' => [0.0, "binary", "x'30'"], + 'Float zero as datetime' => [0.0, "datetime", "'1970-01-01 00:00:00'"], + 'Float zero as boolean' => [0.0, "boolean", "0"], + 'Float zero as strict integer' => [0.0, "strict integer", "0"], + 'Float zero as strict float' => [0.0, "strict float", "0.0"], + 'Float zero as strict string' => [0.0, "strict string", "'0'"], + 'Float zero as strict binary' => [0.0, "strict binary", "x'30'"], 'Float zero as strict datetime' => [0.0, "strict datetime", "'1970-01-01 00:00:00'"], - 'Float zero as strict boolean' => [0.0, "strict boolean", "0"], - 'ASCII string as integer' => ["Random string", "integer", "0"], - 'ASCII string as float' => ["Random string", "float", "0.0", "'0'"], - 'ASCII string as string' => ["Random string", "string", "'Random string'"], - 'ASCII string as binary' => ["Random string", "binary", "x'52616e646f6d20737472696e67'"], - 'ASCII string as datetime' => ["Random string", "datetime", "null"], - 'ASCII string as boolean' => ["Random string", "boolean", "1"], - 'ASCII string as strict integer' => ["Random string", "strict integer", "0"], - 'ASCII string as strict float' => ["Random string", "strict float", "0.0", "'0'"], - 'ASCII string as strict string' => ["Random string", "strict string", "'Random string'"], - 'ASCII string as strict binary' => ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"], + 'Float zero as strict boolean' => [0.0, "strict boolean", "0"], + 'ASCII string as integer' => ["Random string", "integer", "0"], + 'ASCII string as float' => ["Random string", "float", "0.0"], + 'ASCII string as string' => ["Random string", "string", "'Random string'"], + 'ASCII string as binary' => ["Random string", "binary", "x'52616e646f6d20737472696e67'"], + 'ASCII string as datetime' => ["Random string", "datetime", "null"], + 'ASCII string as boolean' => ["Random string", "boolean", "1"], + 'ASCII string as strict integer' => ["Random string", "strict integer", "0"], + 'ASCII string as strict float' => ["Random string", "strict float", "0.0"], + 'ASCII string as strict string' => ["Random string", "strict string", "'Random string'"], + 'ASCII string as strict binary' => ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"], 'ASCII string as strict datetime' => ["Random string", "strict datetime", "'1970-01-01 00:00:00'"], - 'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"], - 'UTF-8 string as integer' => ["\u{e9}", "integer", "0"], - 'UTF-8 string as float' => ["\u{e9}", "float", "0.0", "'0'"], - 'UTF-8 string as string' => ["\u{e9}", "string", "char(233)"], - 'UTF-8 string as binary' => ["\u{e9}", "binary", "x'c3a9'"], - 'UTF-8 string as datetime' => ["\u{e9}", "datetime", "null"], - 'UTF-8 string as boolean' => ["\u{e9}", "boolean", "1"], - 'UTF-8 string as strict integer' => ["\u{e9}", "strict integer", "0"], - 'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.0", "'0'"], - 'UTF-8 string as strict string' => ["\u{e9}", "strict string", "char(233)"], - 'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"], + 'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"], + 'UTF-8 string as integer' => ["\u{e9}", "integer", "0"], + 'UTF-8 string as float' => ["\u{e9}", "float", "0.0"], + 'UTF-8 string as string' => ["\u{e9}", "string", "char(233)"], + 'UTF-8 string as binary' => ["\u{e9}", "binary", "x'c3a9'"], + 'UTF-8 string as datetime' => ["\u{e9}", "datetime", "null"], + 'UTF-8 string as boolean' => ["\u{e9}", "boolean", "1"], + 'UTF-8 string as strict integer' => ["\u{e9}", "strict integer", "0"], + 'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.0"], + 'UTF-8 string as strict string' => ["\u{e9}", "strict string", "char(233)"], + 'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"], 'UTF-8 string as strict datetime' => ["\u{e9}", "strict datetime", "'1970-01-01 00:00:00'"], - 'UTF-8 string as strict boolean' => ["\u{e9}", "strict boolean", "1"], - 'Binary string as integer' => [chr(233).chr(233), "integer", "0"], - 'Binary string as float' => [chr(233).chr(233), "float", "0.0", "'0'"], - 'Binary string as string' => [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"], - 'Binary string as binary' => [chr(233).chr(233), "binary", "x'e9e9'"], - 'Binary string as datetime' => [chr(233).chr(233), "datetime", "null"], - 'Binary string as boolean' => [chr(233).chr(233), "boolean", "1"], - 'Binary string as strict integer' => [chr(233).chr(233), "strict integer", "0"], - 'Binary string as strict float' => [chr(233).chr(233), "strict float", "0.0", "'0'"], - 'Binary string as strict string' => [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"], - 'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"], + 'UTF-8 string as strict boolean' => ["\u{e9}", "strict boolean", "1"], + 'Binary string as integer' => [chr(233).chr(233), "integer", "0"], + 'Binary string as float' => [chr(233).chr(233), "float", "0.0"], + 'Binary string as string' => [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"], + 'Binary string as binary' => [chr(233).chr(233), "binary", "x'e9e9'"], + 'Binary string as datetime' => [chr(233).chr(233), "datetime", "null"], + 'Binary string as boolean' => [chr(233).chr(233), "boolean", "1"], + 'Binary string as strict integer' => [chr(233).chr(233), "strict integer", "0"], + 'Binary string as strict float' => [chr(233).chr(233), "strict float", "0.0"], + 'Binary string as strict string' => [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"], + 'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"], 'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"], - 'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"], - 'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"], - 'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0", "'0'"], - 'ISO 8601 string as string' => ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"], - 'ISO 8601 string as binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"], - 'ISO 8601 string as datetime' => ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"], - 'ISO 8601 string as boolean' => ["2017-01-09T13:11:17", "boolean", "1"], - 'ISO 8601 string as strict integer' => ["2017-01-09T13:11:17", "strict integer", "0"], - 'ISO 8601 string as strict float' => ["2017-01-09T13:11:17", "strict float", "0.0", "'0'"], - 'ISO 8601 string as strict string' => ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"], - 'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"], + 'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"], + 'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"], + 'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0"], + 'ISO 8601 string as string' => ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"], + 'ISO 8601 string as binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"], + 'ISO 8601 string as datetime' => ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"], + 'ISO 8601 string as boolean' => ["2017-01-09T13:11:17", "boolean", "1"], + 'ISO 8601 string as strict integer' => ["2017-01-09T13:11:17", "strict integer", "0"], + 'ISO 8601 string as strict float' => ["2017-01-09T13:11:17", "strict float", "0.0"], + 'ISO 8601 string as strict string' => ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"], + 'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"], 'ISO 8601 string as strict datetime' => ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"], - 'ISO 8601 string as strict boolean' => ["2017-01-09T13:11:17", "strict boolean", "1"], - 'Arbitrary date string as integer' => ["Today", "integer", "0"], - 'Arbitrary date string as float' => ["Today", "float", "0.0", "'0'"], - 'Arbitrary date string as string' => ["Today", "string", "'Today'"], - 'Arbitrary date string as binary' => ["Today", "binary", "x'546f646179'"], - 'Arbitrary date string as datetime' => ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], - 'Arbitrary date string as boolean' => ["Today", "boolean", "1"], - 'Arbitrary date string as strict integer' => ["Today", "strict integer", "0"], - 'Arbitrary date string as strict float' => ["Today", "strict float", "0.0", "'0'"], - 'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"], - 'Arbitrary date string as strict binary' => ["Today", "strict binary", "x'546f646179'"], + 'ISO 8601 string as strict boolean' => ["2017-01-09T13:11:17", "strict boolean", "1"], + 'Arbitrary date string as integer' => ["Today", "integer", "0"], + 'Arbitrary date string as float' => ["Today", "float", "0.0"], + 'Arbitrary date string as string' => ["Today", "string", "'Today'"], + 'Arbitrary date string as binary' => ["Today", "binary", "x'546f646179'"], + 'Arbitrary date string as datetime' => ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], + 'Arbitrary date string as boolean' => ["Today", "boolean", "1"], + 'Arbitrary date string as strict integer' => ["Today", "strict integer", "0"], + 'Arbitrary date string as strict float' => ["Today", "strict float", "0.0"], + 'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"], + 'Arbitrary date string as strict binary' => ["Today", "strict binary", "x'546f646179'"], 'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], - 'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"], - 'DateTime as integer' => [$dateMutable, "integer", $dateUTC->getTimestamp()], - 'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0", "'".$dateUTC->getTimestamp()."'"], - 'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTime as binary' => [$dateMutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], - 'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTime as boolean' => [$dateMutable, "boolean", "1"], - 'DateTime as strict integer' => [$dateMutable, "strict integer", $dateUTC->getTimestamp()], - 'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0", "'".$dateUTC->getTimestamp()."'"], - 'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTime as strict binary' => [$dateMutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + 'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"], + 'DateTime as integer' => [$dateMutable, "integer", $dateUTC->getTimestamp()], + 'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"], + 'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTime as binary' => [$dateMutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + 'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTime as boolean' => [$dateMutable, "boolean", "1"], + 'DateTime as strict integer' => [$dateMutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"], + 'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTime as strict binary' => [$dateMutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], 'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"], - 'DateTimeImmutable as integer' => [$dateImmutable, "integer", $dateUTC->getTimestamp()], - 'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0", "'".$dateUTC->getTimestamp()."'"], - 'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], - 'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"], - 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", $dateUTC->getTimestamp()], - 'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0", "'".$dateUTC->getTimestamp()."'"], - 'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + 'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"], + 'DateTimeImmutable as integer' => [$dateImmutable, "integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"], + 'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], + 'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"], + 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"], + 'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], 'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], + 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], ]; foreach ($this->provideDrivers() as $drvName => list($drv, $stringCoersion, $class, $func)) { - foreach ($tests as $index => $test) { - if (sizeof($test) > 3) { - list($value, $type, $exp, $expPDO) = $test; - } else { - list($value, $type, $exp) = $test; - $expPDO = null; - } - yield "$index ($drvName)" => [$drv, $class, $func, $value, $type, $exp, $expPDO]; + switch ($drvName) { + case "PDO PostgreSQL": + $conv = (function($v, $t) { + switch ($t) { + case "float": + return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; + case "string": + if (preg_match("<^char\((\d+)\)$>", $v, $match)) { + return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; + } else { + return $v; + } + default: + return $v; + } + }); + break; + default: + if ($class == \JKingWeb\Arsse\Db\PDOStatement::class) { + $conv = (function($v, $t) { + if ($t=="float") { + return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; + } else { + return $v; + } + }); + } else { + $conv = (function($v, $t) { + return $v; + }); + } + } + foreach ($tests as $index => list($value, $type, $exp)) { + $t = preg_replace("<^strict >", "", $type); + $exp = ($exp=="null") ? $exp : $conv($exp, $t); + yield "$index ($drvName)" => [$drv, $class, $func, $value, $type, $exp]; } } } From e2b6cb83600395de15ee1d3470c6f6c696b2273d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 20 Nov 2018 15:46:22 -0500 Subject: [PATCH 06/58] Remove PicoFeed-related FIXMEs PicoFeed will never be fixed, so they are not helpful --- lib/Feed.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Feed.php b/lib/Feed.php index 836ecc86..9ede4e4b 100644 --- a/lib/Feed.php +++ b/lib/Feed.php @@ -33,14 +33,14 @@ class Feed { } else { $links = $f->reader->find($f->getUrl(), $f->getContent()); if (!$links) { - // work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory + // work around a PicoFeed memory leak libxml_use_internal_errors(false); throw new Feed\Exception($url, new \PicoFeed\Reader\SubscriptionNotFoundException('Unable to find a subscription')); } else { $out = $links[0]; } } - // work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory + // work around a PicoFeed memory leak libxml_use_internal_errors(false); return $out; } @@ -115,10 +115,10 @@ class Feed { // Some feeds might use a different domain (eg: feedburner), so the site url is // used instead of the feed's url. $this->favicon = (new Favicon)->find($feed->siteUrl); - // work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory + // work around a PicoFeed memory leak libxml_use_internal_errors(false); } catch (PicoFeedException $e) { - // work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory + // work around a PicoFeed memory leak libxml_use_internal_errors(false); throw new Feed\Exception($this->resource->getUrl(), $e); } From d52af6db5a7b3bdde98015578ea917d52fcf545d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 20 Nov 2018 15:48:03 -0500 Subject: [PATCH 07/58] PostgreSQL fixes Errors were not correctly throwing exceptions For the sake of SQLite compatibility booleans should be bound as integers in PDO --- lib/Db/PDOStatement.php | 2 +- lib/Db/PostgreSQL/PDODriver.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php index cfd9cefc..95b95a7c 100644 --- a/lib/Db/PDOStatement.php +++ b/lib/Db/PDOStatement.php @@ -15,7 +15,7 @@ class PDOStatement extends AbstractStatement { "datetime" => \PDO::PARAM_STR, "binary" => \PDO::PARAM_LOB, "string" => \PDO::PARAM_STR, - "boolean" => \PDO::PARAM_BOOL, + "boolean" => \PDO::PARAM_INT, // FIXME: using \PDO::PARAM_BOOL leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3 ]; protected $st; diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php index 9bb630ec..af983381 100644 --- a/lib/Db/PostgreSQL/PDODriver.php +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -22,7 +22,7 @@ class PDODriver extends Driver { protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { $dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port, $service); - $this->db = new \PDO("pgsql:$dsn", $user, $pass); + $this->db = new \PDO("pgsql:$dsn", $user, $pass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); } public function __destruct() { From 84b4cb746564026ca341a89dd8140ece85935e2d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 20 Nov 2018 16:32:18 -0500 Subject: [PATCH 08/58] Enable PostgreSQL statement testing Tests involving binary data are skipped for now --- lib/Db/PDOError.php | 3 + tests/cases/Db/TestStatement.php | 223 ++++++++++++++++++------------- 2 files changed, 135 insertions(+), 91 deletions(-) diff --git a/lib/Db/PDOError.php b/lib/Db/PDOError.php index 03e1f89c..206e0224 100644 --- a/lib/Db/PDOError.php +++ b/lib/Db/PDOError.php @@ -14,7 +14,10 @@ trait PDOError { $err = $this->db->errorInfo(); } switch ($err[0]) { + case "22P02": + return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; case "23000": + case "23502": return [ExceptionInput::class, "constraintViolation", $err[2]]; case "HY000": // engine-specific errors diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/TestStatement.php index 59759395..9b4143a8 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/TestStatement.php @@ -45,10 +45,10 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $s = $drvPdo->prepare($query); return [$drvPdo, $s, $types]; }], - /*'PDO PostgreSQL' => [isset($drvPgsql), true, \JKingWeb\Arsse\Db\PDOStatement::class, function(string $query, array $types = []) use($drvPgsql) { + 'PDO PostgreSQL' => [isset($drvPgsql), true, \JKingWeb\Arsse\Db\PDOStatement::class, function(string $query, array $types = []) use($drvPgsql) { $s = $drvPgsql->prepare($query); return [$drvPgsql, $s, $types]; - }],*/ + }], ]; } @@ -77,6 +77,23 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertTrue((bool) $act); } + /** @dataProvider provideBinaryBindings */ + public function testHandleBinaryData(bool $driverTestable, string $class, \Closure $func, $value, string $type, string $exp) { + if (!$driverTestable) { + $this->markTestSkipped(); + } + if ($exp=="null") { + $query = "SELECT (cast(? as text) is null) as pass"; + } else { + $query = "SELECT ($exp = ?) as pass"; + } + $typeStr = "'".str_replace("'", "''", $type)."'"; + $s = new $class(...$func($query)); + $s->retype(...[$type]); + $act = $s->run(...[$value])->getValue(); + $this->assertTrue((bool) $act); + } + /** @dataProvider provideDrivers */ public function testBindMissingValue(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { @@ -160,111 +177,169 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'Null as integer' => [null, "integer", "null"], 'Null as float' => [null, "float", "null"], 'Null as string' => [null, "string", "null"], - 'Null as binary' => [null, "binary", "null"], 'Null as datetime' => [null, "datetime", "null"], 'Null as boolean' => [null, "boolean", "null"], 'Null as strict integer' => [null, "strict integer", "0"], 'Null as strict float' => [null, "strict float", "0.0"], 'Null as strict string' => [null, "strict string", "''"], - 'Null as strict binary' => [null, "strict binary", "x''"], 'Null as strict datetime' => [null, "strict datetime", "'1970-01-01 00:00:00'"], 'Null as strict boolean' => [null, "strict boolean", "0"], 'True as integer' => [true, "integer", "1"], 'True as float' => [true, "float", "1.0"], 'True as string' => [true, "string", "'1'"], - 'True as binary' => [true, "binary", "x'31'"], 'True as datetime' => [true, "datetime", "null"], 'True as boolean' => [true, "boolean", "1"], 'True as strict integer' => [true, "strict integer", "1"], 'True as strict float' => [true, "strict float", "1.0"], 'True as strict string' => [true, "strict string", "'1'"], - 'True as strict binary' => [true, "strict binary", "x'31'"], 'True as strict datetime' => [true, "strict datetime", "'1970-01-01 00:00:00'"], 'True as strict boolean' => [true, "strict boolean", "1"], 'False as integer' => [false, "integer", "0"], 'False as float' => [false, "float", "0.0"], 'False as string' => [false, "string", "''"], - 'False as binary' => [false, "binary", "x''"], 'False as datetime' => [false, "datetime", "null"], 'False as boolean' => [false, "boolean", "0"], 'False as strict integer' => [false, "strict integer", "0"], 'False as strict float' => [false, "strict float", "0.0"], 'False as strict string' => [false, "strict string", "''"], - 'False as strict binary' => [false, "strict binary", "x''"], 'False as strict datetime' => [false, "strict datetime", "'1970-01-01 00:00:00'"], 'False as strict boolean' => [false, "strict boolean", "0"], 'Integer as integer' => [2112, "integer", "2112"], 'Integer as float' => [2112, "float", "2112.0"], 'Integer as string' => [2112, "string", "'2112'"], - 'Integer as binary' => [2112, "binary", "x'32313132'"], 'Integer as datetime' => [2112, "datetime", "'1970-01-01 00:35:12'"], 'Integer as boolean' => [2112, "boolean", "1"], 'Integer as strict integer' => [2112, "strict integer", "2112"], 'Integer as strict float' => [2112, "strict float", "2112.0"], 'Integer as strict string' => [2112, "strict string", "'2112'"], - 'Integer as strict binary' => [2112, "strict binary", "x'32313132'"], 'Integer as strict datetime' => [2112, "strict datetime", "'1970-01-01 00:35:12'"], 'Integer as strict boolean' => [2112, "strict boolean", "1"], 'Integer zero as integer' => [0, "integer", "0"], 'Integer zero as float' => [0, "float", "0.0"], 'Integer zero as string' => [0, "string", "'0'"], - 'Integer zero as binary' => [0, "binary", "x'30'"], 'Integer zero as datetime' => [0, "datetime", "'1970-01-01 00:00:00'"], 'Integer zero as boolean' => [0, "boolean", "0"], 'Integer zero as strict integer' => [0, "strict integer", "0"], 'Integer zero as strict float' => [0, "strict float", "0.0"], 'Integer zero as strict string' => [0, "strict string", "'0'"], - 'Integer zero as strict binary' => [0, "strict binary", "x'30'"], 'Integer zero as strict datetime' => [0, "strict datetime", "'1970-01-01 00:00:00'"], 'Integer zero as strict boolean' => [0, "strict boolean", "0"], 'Float as integer' => [2112.5, "integer", "2112"], 'Float as float' => [2112.5, "float", "2112.5"], 'Float as string' => [2112.5, "string", "'2112.5'"], - 'Float as binary' => [2112.5, "binary", "x'323131322e35'"], 'Float as datetime' => [2112.5, "datetime", "'1970-01-01 00:35:12'"], 'Float as boolean' => [2112.5, "boolean", "1"], 'Float as strict integer' => [2112.5, "strict integer", "2112"], 'Float as strict float' => [2112.5, "strict float", "2112.5"], 'Float as strict string' => [2112.5, "strict string", "'2112.5'"], - 'Float as strict binary' => [2112.5, "strict binary", "x'323131322e35'"], 'Float as strict datetime' => [2112.5, "strict datetime", "'1970-01-01 00:35:12'"], 'Float as strict boolean' => [2112.5, "strict boolean", "1"], 'Float zero as integer' => [0.0, "integer", "0"], 'Float zero as float' => [0.0, "float", "0.0"], 'Float zero as string' => [0.0, "string", "'0'"], - 'Float zero as binary' => [0.0, "binary", "x'30'"], 'Float zero as datetime' => [0.0, "datetime", "'1970-01-01 00:00:00'"], 'Float zero as boolean' => [0.0, "boolean", "0"], 'Float zero as strict integer' => [0.0, "strict integer", "0"], 'Float zero as strict float' => [0.0, "strict float", "0.0"], 'Float zero as strict string' => [0.0, "strict string", "'0'"], - 'Float zero as strict binary' => [0.0, "strict binary", "x'30'"], 'Float zero as strict datetime' => [0.0, "strict datetime", "'1970-01-01 00:00:00'"], 'Float zero as strict boolean' => [0.0, "strict boolean", "0"], 'ASCII string as integer' => ["Random string", "integer", "0"], 'ASCII string as float' => ["Random string", "float", "0.0"], 'ASCII string as string' => ["Random string", "string", "'Random string'"], - 'ASCII string as binary' => ["Random string", "binary", "x'52616e646f6d20737472696e67'"], 'ASCII string as datetime' => ["Random string", "datetime", "null"], 'ASCII string as boolean' => ["Random string", "boolean", "1"], 'ASCII string as strict integer' => ["Random string", "strict integer", "0"], 'ASCII string as strict float' => ["Random string", "strict float", "0.0"], 'ASCII string as strict string' => ["Random string", "strict string", "'Random string'"], - 'ASCII string as strict binary' => ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"], 'ASCII string as strict datetime' => ["Random string", "strict datetime", "'1970-01-01 00:00:00'"], 'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"], 'UTF-8 string as integer' => ["\u{e9}", "integer", "0"], 'UTF-8 string as float' => ["\u{e9}", "float", "0.0"], 'UTF-8 string as string' => ["\u{e9}", "string", "char(233)"], - 'UTF-8 string as binary' => ["\u{e9}", "binary", "x'c3a9'"], 'UTF-8 string as datetime' => ["\u{e9}", "datetime", "null"], 'UTF-8 string as boolean' => ["\u{e9}", "boolean", "1"], 'UTF-8 string as strict integer' => ["\u{e9}", "strict integer", "0"], 'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.0"], 'UTF-8 string as strict string' => ["\u{e9}", "strict string", "char(233)"], - 'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"], 'UTF-8 string as strict datetime' => ["\u{e9}", "strict datetime", "'1970-01-01 00:00:00'"], 'UTF-8 string as strict boolean' => ["\u{e9}", "strict boolean", "1"], + 'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"], + 'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0"], + 'ISO 8601 string as string' => ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"], + 'ISO 8601 string as datetime' => ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"], + 'ISO 8601 string as boolean' => ["2017-01-09T13:11:17", "boolean", "1"], + 'ISO 8601 string as strict integer' => ["2017-01-09T13:11:17", "strict integer", "0"], + 'ISO 8601 string as strict float' => ["2017-01-09T13:11:17", "strict float", "0.0"], + 'ISO 8601 string as strict string' => ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"], + 'ISO 8601 string as strict datetime' => ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"], + 'ISO 8601 string as strict boolean' => ["2017-01-09T13:11:17", "strict boolean", "1"], + 'Arbitrary date string as integer' => ["Today", "integer", "0"], + 'Arbitrary date string as float' => ["Today", "float", "0.0"], + 'Arbitrary date string as string' => ["Today", "string", "'Today'"], + 'Arbitrary date string as datetime' => ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], + 'Arbitrary date string as boolean' => ["Today", "boolean", "1"], + 'Arbitrary date string as strict integer' => ["Today", "strict integer", "0"], + 'Arbitrary date string as strict float' => ["Today", "strict float", "0.0"], + 'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"], + 'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], + 'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"], + 'DateTime as integer' => [$dateMutable, "integer", $dateUTC->getTimestamp()], + 'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"], + 'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTime as boolean' => [$dateMutable, "boolean", "1"], + 'DateTime as strict integer' => [$dateMutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"], + 'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"], + 'DateTimeImmutable as integer' => [$dateImmutable, "integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"], + 'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"], + 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"], + 'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], + 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], + ]; + $decorators = $this->provideSyntaxDecorators(); + foreach ($this->provideDrivers() as $drvName => list($drv, $stringCoersion, $class, $func)) { + $conv = $decorators[$drvName] ?? $conv = $decorators['']; + foreach ($tests as $index => list($value, $type, $exp)) { + $t = preg_replace("<^strict >", "", $type); + $exp = ($exp=="null") ? $exp : $conv($exp, $t); + yield "$index ($drvName)" => [$drv, $class, $func, $value, $type, $exp]; + } + } + } + + public function provideBinaryBindings() { + $dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto")); + $dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto")); + $dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC")); + $tests = [ + /* input, type, expected binding as SQL fragment */ + 'Null as binary' => [null, "binary", "null"], + 'Null as strict binary' => [null, "strict binary", "x''"], + 'True as binary' => [true, "binary", "x'31'"], + 'True as strict binary' => [true, "strict binary", "x'31'"], + 'False as binary' => [false, "binary", "x''"], + 'False as strict binary' => [false, "strict binary", "x''"], + 'Integer as binary' => [2112, "binary", "x'32313132'"], + 'Integer as strict binary' => [2112, "strict binary", "x'32313132'"], + 'Integer zero as binary' => [0, "binary", "x'30'"], + 'Integer zero as strict binary' => [0, "strict binary", "x'30'"], + 'Float as binary' => [2112.5, "binary", "x'323131322e35'"], + 'Float as strict binary' => [2112.5, "strict binary", "x'323131322e35'"], + 'Float zero as binary' => [0.0, "binary", "x'30'"], + 'Float zero as strict binary' => [0.0, "strict binary", "x'30'"], + 'ASCII string as binary' => ["Random string", "binary", "x'52616e646f6d20737472696e67'"], + 'ASCII string as strict binary' => ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"], + 'UTF-8 string as binary' => ["\u{e9}", "binary", "x'c3a9'"], + 'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"], 'Binary string as integer' => [chr(233).chr(233), "integer", "0"], 'Binary string as float' => [chr(233).chr(233), "float", "0.0"], 'Binary string as string' => [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"], @@ -277,87 +352,21 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"], 'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"], 'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"], - 'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"], - 'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0"], - 'ISO 8601 string as string' => ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"], 'ISO 8601 string as binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"], - 'ISO 8601 string as datetime' => ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"], - 'ISO 8601 string as boolean' => ["2017-01-09T13:11:17", "boolean", "1"], - 'ISO 8601 string as strict integer' => ["2017-01-09T13:11:17", "strict integer", "0"], - 'ISO 8601 string as strict float' => ["2017-01-09T13:11:17", "strict float", "0.0"], - 'ISO 8601 string as strict string' => ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"], 'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"], - 'ISO 8601 string as strict datetime' => ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"], - 'ISO 8601 string as strict boolean' => ["2017-01-09T13:11:17", "strict boolean", "1"], - 'Arbitrary date string as integer' => ["Today", "integer", "0"], - 'Arbitrary date string as float' => ["Today", "float", "0.0"], - 'Arbitrary date string as string' => ["Today", "string", "'Today'"], 'Arbitrary date string as binary' => ["Today", "binary", "x'546f646179'"], - 'Arbitrary date string as datetime' => ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], - 'Arbitrary date string as boolean' => ["Today", "boolean", "1"], - 'Arbitrary date string as strict integer' => ["Today", "strict integer", "0"], - 'Arbitrary date string as strict float' => ["Today", "strict float", "0.0"], - 'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"], 'Arbitrary date string as strict binary' => ["Today", "strict binary", "x'546f646179'"], - 'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], - 'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"], - 'DateTime as integer' => [$dateMutable, "integer", $dateUTC->getTimestamp()], - 'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"], - 'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as binary' => [$dateMutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], - 'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTime as boolean' => [$dateMutable, "boolean", "1"], - 'DateTime as strict integer' => [$dateMutable, "strict integer", $dateUTC->getTimestamp()], - 'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"], - 'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as strict binary' => [$dateMutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], - 'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"], - 'DateTimeImmutable as integer' => [$dateImmutable, "integer", $dateUTC->getTimestamp()], - 'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"], - 'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], - 'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"], - 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", $dateUTC->getTimestamp()], - 'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"], - 'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], - 'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], - 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], ]; + $decorators = $this->provideSyntaxDecorators(); foreach ($this->provideDrivers() as $drvName => list($drv, $stringCoersion, $class, $func)) { - switch ($drvName) { - case "PDO PostgreSQL": - $conv = (function($v, $t) { - switch ($t) { - case "float": - return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; - case "string": - if (preg_match("<^char\((\d+)\)$>", $v, $match)) { - return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; - } else { - return $v; - } - default: - return $v; - } - }); - break; - default: - if ($class == \JKingWeb\Arsse\Db\PDOStatement::class) { - $conv = (function($v, $t) { - if ($t=="float") { - return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; - } else { - return $v; - } - }); - } else { - $conv = (function($v, $t) { - return $v; - }); - } + $conv = $decorators[$drvName] ?? $conv = $decorators['']; + if ($drvName=="PDO PostgreSQL") { + // skip PostgreSQL for these tests + $drv = false; } foreach ($tests as $index => list($value, $type, $exp)) { $t = preg_replace("<^strict >", "", $type); @@ -366,4 +375,36 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } } } + + function provideSyntaxDecorators() { + return [ + 'PDO PostgreSQL' => (function($v, $t) { + switch ($t) { + case "float": + return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; + case "string": + if (preg_match("<^char\((\d+)\)$>", $v, $match)) { + return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; + } else { + return $v; + } + default: + return $v; + } + }), + 'PDO SQLite 3' => (function($v, $t) { + if ($t=="float") { + return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; + } else { + return $v; + } + }), + 'SQLite 3' => (function($v, $t) { + return $v; + }), + '' => (function($v, $t) { + return $v; + }), + ]; + } } From c0c4810662e29a2c2d29bbe0ecc5690a8dac6b86 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 21 Nov 2018 11:06:12 -0500 Subject: [PATCH 09/58] Nominally complete PostgreSQL driver Connection error handling as well as uprade error handling still need to be implemented. --- lib/Db/AbstractDriver.php | 6 +- lib/Db/PostgreSQL/Driver.php | 103 ++++++++++++++------------ sql/PostgreSQL/0.sql | 123 +++++++++++++++++++++++++++++++ sql/PostgreSQL/1.sql | 36 +++++++++ sql/PostgreSQL/2.sql | 22 ++++++ tests/cases/Db/TestStatement.php | 8 +- 6 files changed, 245 insertions(+), 53 deletions(-) create mode 100644 sql/PostgreSQL/0.sql create mode 100644 sql/PostgreSQL/1.sql create mode 100644 sql/PostgreSQL/2.sql diff --git a/lib/Db/AbstractDriver.php b/lib/Db/AbstractDriver.php index 9d2867bf..45b9476b 100644 --- a/lib/Db/AbstractDriver.php +++ b/lib/Db/AbstractDriver.php @@ -13,13 +13,13 @@ abstract class AbstractDriver implements Driver { protected $transDepth = 0; protected $transStatus = []; + abstract protected function lock(): bool; + abstract protected function unlock(bool $rollback = false): bool; abstract protected function getError(): string; - /** @codeCoverageIgnore */ public function schemaVersion(): int { - // FIXME: generic schemaVersion() will need to be covered for database engines other than SQLite try { - return (int) $this->query("SELECT value from arsse_meta where key is schema_version")->getValue(); + return (int) $this->query("SELECT value from arsse_meta where key = 'schema_version'")->getValue(); } catch (Exception $e) { return 0; } diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 99154ede..ef09aad8 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -13,11 +13,10 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionTimeout; class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { - public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) { // check to make sure required extension is loaded if (!static::requirementsMet()) { - throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore + throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore } $user = $user ?? Arsse::$conf->dbPostgreSQLUser; $pass = $pass ?? Arsse::$conf->dbPostgreSQLPass; @@ -27,16 +26,9 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $schema = $schema ?? Arsse::$conf->dbPostgreSQLSchema; $service = $service ?? Arsse::$conf->dbPostgreSQLService; $this->makeConnection($user, $pass, $db, $host, $port, $service); - } - - public static function requirementsMet(): bool { - // stub: native interface is not yet supported - return false; - } - - protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { - // stub: native interface is not yet supported - throw new \Exception; + foreach (static::makeSetupQueries($schema) as $q) { + $this->exec($q); + } } public static function makeConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service): string { @@ -73,7 +65,15 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return implode(" ", $out); } - public function __destruct() { + public static function makeSetupQueries(string $schema = ""): array { + $out = [ + "SET TIME ZONE UTC", + "SET DateStyle = 'ISO, MDY'" + ]; + if (strlen($schema) > 0) { + $out[] = 'SET search_path = \'"'.str_replace('"', '""', $schema).'", "$user", public\''; + } + return $out; } /** @codeCoverageIgnore */ @@ -96,48 +96,57 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return "PostgreSQL"; } - public function schemaVersion(): int { - // stub - return 0; - } - - public function schemaUpdate(int $to, string $basePath = null): bool { - // stub - return false; - } - public function charsetAcceptable(): bool { - // stub - return true; - } - - protected function getError(): string { - // stub - return ""; - } - - public function exec(string $query): bool { - // stub - return true; - } - - public function query(string $query): \JKingWeb\Arsse\Db\Result { - // stub - return new ResultEmpty; - } - - public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { - // stub - return new Statement($this->db, $s, $paramTypes); + return $this->query("SELECT pg_encoding_to_char(encoding) from pg_database where datname = current_database()")->getValue() == "UTF8"; } protected function lock(): bool { - // stub + $this->exec("BEGIN TRANSACTION"); + if ($this->schemaVersion()) { + $this->exec("LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"); + } return true; } protected function unlock(bool $rollback = false): bool { - // stub + $this->exec((!$rollback) ? "COMMIT" : "ROLLBACK"); return true; } + + public function __destruct() { + } + + public static function requirementsMet(): bool { + // stub: native interface is not yet supported + return false; + } + + protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { + // stub: native interface is not yet supported + throw new \Exception; + } + + /** @codeCoverageIgnore */ + protected function getError(): string { + // stub: native interface is not yet supported + return ""; + } + + /** @codeCoverageIgnore */ + public function exec(string $query): bool { + // stub: native interface is not yet supported + return true; + } + + /** @codeCoverageIgnore */ + public function query(string $query): \JKingWeb\Arsse\Db\Result { + // stub: native interface is not yet supported + return new ResultEmpty; + } + + /** @codeCoverageIgnore */ + public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { + // stub: native interface is not yet supported + return new Statement($this->db, $s, $paramTypes); + } } diff --git a/sql/PostgreSQL/0.sql b/sql/PostgreSQL/0.sql new file mode 100644 index 00000000..461b1f23 --- /dev/null +++ b/sql/PostgreSQL/0.sql @@ -0,0 +1,123 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + +-- metadata +create table arsse_meta( + key text primary key, + value text +); + +-- users +create table arsse_users( + id text primary key, + password text, + name text, + avatar_type text, + avatar_data bytea, + admin smallint default 0, + rights bigint not null default 0 +); + +-- extra user metadata +create table arsse_users_meta( + owner text not null references arsse_users(id) on delete cascade on update cascade, + key text not null, + value text, + primary key(owner,key) +); + +-- NextCloud News folders and TT-RSS categories +create table arsse_folders( + id bigserial primary key, + owner text not null references arsse_users(id) on delete cascade on update cascade, + parent bigint references arsse_folders(id) on delete cascade, + name text not null, + modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, -- + unique(owner,name,parent) +); + +-- newsfeeds, deduplicated +create table arsse_feeds( + id bigserial primary key, + url text not null, + title text, + favicon text, + source text, + updated timestamp(0) with time zone, + modified timestamp(0) with time zone, + next_fetch timestamp(0) with time zone, + orphaned timestamp(0) with time zone, + etag text not null default '', + err_count bigint not null default 0, + err_msg text, + username text not null default '', + password text not null default '', + size bigint not null default 0, + scrape smallint not null default 0, + unique(url,username,password) +); + +-- users' subscriptions to newsfeeds, with settings +create table arsse_subscriptions( + id bigserial primary key, + owner text not null references arsse_users(id) on delete cascade on update cascade, + feed bigint not null references arsse_feeds(id) on delete cascade, + added timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + title text, + order_type smallint not null default 0, + pinned smallint not null default 0, + folder bigint references arsse_folders(id) on delete cascade, + unique(owner,feed) +); + +-- entries in newsfeeds +create table arsse_articles( + id bigserial primary key, + feed bigint not null references arsse_feeds(id) on delete cascade, + url text, + title text, + author text, + published timestamp(0) with time zone, + edited timestamp(0) with time zone, + modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + content text, + guid text, + url_title_hash text not null, + url_content_hash text not null, + title_content_hash text not null +); + +-- enclosures associated with articles +create table arsse_enclosures( + article bigint not null references arsse_articles(id) on delete cascade, + url text, + type text +); + +-- users' actions on newsfeed entries +create table arsse_marks( + article bigint not null references arsse_articles(id) on delete cascade, + subscription bigint not null references arsse_subscriptions(id) on delete cascade on update cascade, + read smallint not null default 0, + starred smallint not null default 0, + modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + primary key(article,subscription) +); + +-- IDs for specific editions of articles (required for at least NextCloud News) +create table arsse_editions( + id bigserial primary key, + article bigint not null references arsse_articles(id) on delete cascade, + modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP +); + +-- author categories associated with newsfeed entries +create table arsse_categories( + article bigint not null references arsse_articles(id) on delete cascade, + name text +); + +-- set version marker +insert into arsse_meta(key,value) values('schema_version','1'); diff --git a/sql/PostgreSQL/1.sql b/sql/PostgreSQL/1.sql new file mode 100644 index 00000000..f8a950b9 --- /dev/null +++ b/sql/PostgreSQL/1.sql @@ -0,0 +1,36 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + +-- Sessions for Tiny Tiny RSS (and possibly others) +create table arsse_sessions ( + id text primary key, + created timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + expires timestamp(0) with time zone not null, + user text not null references arsse_users(id) on delete cascade on update cascade +); + +-- User-defined article labels for Tiny Tiny RSS +create table arsse_labels ( + id bigserial primary key, + owner text not null references arsse_users(id) on delete cascade on update cascade, + name text not null, + modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + unique(owner,name) +); + +-- Labels assignments for articles +create table arsse_label_members ( + label bigint not null references arsse_labels(id) on delete cascade, + article bigint not null references arsse_articles(id) on delete cascade, + subscription bigint not null references arsse_subscriptions(id) on delete cascade, + assigned smallint not null default 1, + modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + primary key(label,article) +); + +-- alter marks table to add Tiny Tiny RSS' notes +alter table arsse_marks add column note text not null default ''; + +-- set version marker +update arsse_meta set value = '2' where key = 'schema_version'; diff --git a/sql/PostgreSQL/2.sql b/sql/PostgreSQL/2.sql new file mode 100644 index 00000000..cf5cf3db --- /dev/null +++ b/sql/PostgreSQL/2.sql @@ -0,0 +1,22 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + +-- create a case-insensitive generic collation sequence +create collation nocase( + provider = icu, + locale = '@kf=false' +); + +-- Correct collation sequences +alter table arsse_users alter column id type text collate nocase; +alter table arsse_folders alter column name type text collate nocase; +alter table arsse_feeds alter column title type text collate nocase; +alter table arsse_subscriptions alter column title type text collate nocase; +alter table arsse_articles alter column title type text collate nocase; +alter table arsse_articles alter column author type text collate nocase; +alter table arsse_categories alter column name type text collate nocase; +alter table arsse_labels alter column name type text collate nocase; + +-- set version marker +update arsse_meta set value = '3' where key = 'schema_version'; diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/TestStatement.php index 9b4143a8..0b161adc 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/TestStatement.php @@ -28,7 +28,11 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $drvPgsql = (function() { if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) { $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); - return new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $c = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { + $c->exec($q); + } + return $c; } })(); $drvPdo = (function() { @@ -173,7 +177,6 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto")); $dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC")); $tests = [ - /* input, type, expected binding as SQL fragment */ 'Null as integer' => [null, "integer", "null"], 'Null as float' => [null, "float", "null"], 'Null as string' => [null, "string", "null"], @@ -321,7 +324,6 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto")); $dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC")); $tests = [ - /* input, type, expected binding as SQL fragment */ 'Null as binary' => [null, "binary", "null"], 'Null as strict binary' => [null, "strict binary", "x''"], 'True as binary' => [true, "binary", "x'31'"], From 4e444fd86c19ff70eb9dec35cb50aa161521cbae Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 21 Nov 2018 13:06:01 -0500 Subject: [PATCH 10/58] Generic database interface creation in tests --- tests/cases/Db/TestResult.php | 64 ++++++++++++++-------------- tests/cases/Db/TestStatement.php | 73 ++++++++++++-------------------- tests/lib/AbstractTest.php | 55 ++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 79 deletions(-) diff --git a/tests/cases/Db/TestResult.php b/tests/cases/Db/TestResult.php index eb2e695f..d0074bcc 100644 --- a/tests/cases/Db/TestResult.php +++ b/tests/cases/Db/TestResult.php @@ -16,37 +16,35 @@ use JKingWeb\Arsse\Db\SQLite3\PDODriver; * @covers \JKingWeb\Arsse\Db\SQLite3\Result */ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { - public function provideDrivers() { + public function provideResults() { $this->setConf(); - $drvSqlite3 = (function() { - if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { - $d = new \SQLite3(Arsse::$conf->dbSQLite3File); - $d->enableExceptions(true); - return $d; - } - })(); - $drvPdo = (function() { - if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { - return new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); - } - })(); - return [ - 'SQLite 3' => [isset($drvSqlite3), false, \JKingWeb\Arsse\Db\SQLite3\Result::class, function(string $query) use($drvSqlite3) { - $set = $drvSqlite3->query($query); - $rows = $drvSqlite3->changes(); - $id = $drvSqlite3->lastInsertRowID(); + $interfaces = $this->provideDbInterfaces(); + $constructors = [ + 'SQLite 3' => function(string $query) use($interfaces) { + $drv = $interfaces['SQLite 3']['interface']; + $set = $drv->query($query); + $rows = $drv->changes(); + $id = $drv->lastInsertRowID(); return [$set, [$rows, $id]]; - }], - 'PDO' => [isset($drvPdo), true, \JKingWeb\Arsse\Db\PDOResult::class, function(string $query) use($drvPdo) { - $set = $drvPdo->query($query); - $rows = $set->rowCount(); - $id = $drvPdo->lastInsertID(); - return [$set, [$rows, $id]]; - }], + }, ]; + foreach ($constructors as $drv => $func) { + yield $drv => [isset($interfaces[$drv]['interface']), $interfaces[$drv]['stringOutput'], $interfaces[$drv]['result'], $func]; + } + // there is only one PDO result implementation, so we test the first implementation we find + $pdo = array_reduce($interfaces, function ($carry, $item) { + return $carry ?? ($item['interface'] instanceof \PDO ? $item : null); + }) ?? $interfaces['PDO SQLite 3']; + yield "PDO" => [isset($pdo['interface']), $pdo['stringOutput'], $pdo['result'], function(string $query) use($pdo) { + $drv = $pdo['interface']; + $set = $drv->query($query); + $rows = $set->rowCount(); + $id = $drv->lastInsertID(); + return [$set, [$rows, $id]]; + }]; } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testConstructResult(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -54,7 +52,7 @@ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertInstanceOf(Result::class, new $class(...$func("SELECT 1"))); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testGetChangeCountAndLastInsertId(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -68,7 +66,7 @@ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame((int) $id, $r->lastId()); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testIterateOverResults(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -81,7 +79,7 @@ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame($exp, $rows); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testIterateOverResultsTwice(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -99,7 +97,7 @@ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { } } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testGetSingleValues(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -113,7 +111,7 @@ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame(null, $test->getValue()); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testGetFirstValuesOnly(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -127,7 +125,7 @@ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame(null, $test->getValue()); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testGetRows(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -142,7 +140,7 @@ class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame(null, $test->getRow()); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideResults */ public function testGetAllRows(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/TestStatement.php index 0b161adc..838ef678 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/TestStatement.php @@ -16,47 +16,28 @@ use JKingWeb\Arsse\Db\PDOStatement; * @covers \JKingWeb\Arsse\Db\PDOStatement * @covers \JKingWeb\Arsse\Db\PDOError */ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { - public function provideDrivers() { - $this->setConf(); - $drvSqlite3 = (function() { - if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { - $d = new \SQLite3(Arsse::$conf->dbSQLite3File); - $d->enableExceptions(true); - return $d; - } - })(); - $drvPgsql = (function() { - if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) { - $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); - $c = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); - foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { - $c->exec($q); - } - return $c; - } - })(); - $drvPdo = (function() { - if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { - return new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); - } - })(); - return [ - 'SQLite 3' => [isset($drvSqlite3), false, \JKingWeb\Arsse\Db\SQLite3\Statement::class, function(string $query, array $types = []) use($drvSqlite3) { - $s = $drvSqlite3->prepare($query); - return [$drvSqlite3, $s, $types]; - }], - 'PDO SQLite 3' => [isset($drvPdo), true, \JKingWeb\Arsse\Db\PDOStatement::class, function(string $query, array $types = []) use($drvPdo) { - $s = $drvPdo->prepare($query); - return [$drvPdo, $s, $types]; - }], - 'PDO PostgreSQL' => [isset($drvPgsql), true, \JKingWeb\Arsse\Db\PDOStatement::class, function(string $query, array $types = []) use($drvPgsql) { - $s = $drvPgsql->prepare($query); - return [$drvPgsql, $s, $types]; - }], + public function provideStatements() { + $interfaces = $this->provideDbInterfaces(); + $constructors = [ + 'SQLite 3' => function(string $query, array $types = []) use($interfaces) { + $s = $interfaces['SQLite 3']['interface']->prepare($query); + return [$interfaces['SQLite 3']['interface'], $s, $types]; + }, + 'PDO SQLite 3' => function(string $query, array $types = []) use($interfaces) { + $s = $interfaces['PDO SQLite 3']['interface']->prepare($query); + return [$interfaces['PDO SQLite 3']['interface'], $s, $types]; + }, + 'PDO PostgreSQL' => function(string $query, array $types = []) use($interfaces) { + $s = $interfaces['PDO PostgreSQL']['interface']->prepare($query); + return [$interfaces['PDO PostgreSQL']['interface'], $s, $types]; + }, ]; + foreach ($constructors as $drv => $func) { + yield $drv => [isset($interfaces[$drv]['interface']), $interfaces[$drv]['stringOutput'], $interfaces[$drv]['statement'], $func]; + } } - /** @dataProvider provideDrivers */ + /** @dataProvider provideStatements */ public function testConstructStatement(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -98,7 +79,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertTrue((bool) $act); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideStatements */ public function testBindMissingValue(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -108,7 +89,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame(null, $val); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideStatements */ public function testBindMultipleValues(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -123,7 +104,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame($exp, $val); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideStatements */ public function testBindRecursively(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -140,7 +121,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame($exp, $val); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideStatements */ public function testBindWithoutType(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -150,7 +131,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $s->runArray([1]); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideStatements */ public function testViolateConstraint(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -161,7 +142,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { $s->runArray([null]); } - /** @dataProvider provideDrivers */ + /** @dataProvider provideStatements */ public function testMismatchTypes(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { if (!$driverTestable) { $this->markTestSkipped(); @@ -309,7 +290,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], ]; $decorators = $this->provideSyntaxDecorators(); - foreach ($this->provideDrivers() as $drvName => list($drv, $stringCoersion, $class, $func)) { + foreach ($this->provideStatements() as $drvName => list($drv, $stringCoersion, $class, $func)) { $conv = $decorators[$drvName] ?? $conv = $decorators['']; foreach ($tests as $index => list($value, $type, $exp)) { $t = preg_replace("<^strict >", "", $type); @@ -364,7 +345,7 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], ]; $decorators = $this->provideSyntaxDecorators(); - foreach ($this->provideDrivers() as $drvName => list($drv, $stringCoersion, $class, $func)) { + foreach ($this->provideStatements() as $drvName => list($drv, $stringCoersion, $class, $func)) { $conv = $decorators[$drvName] ?? $conv = $decorators['']; if ($drvName=="PDO PostgreSQL") { // skip PostgreSQL for these tests diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 7ad4c7c2..750c6b0c 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -125,4 +125,59 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } return $value; } + + public function provideDbInterfaces(array $conf = []): array { + $this->setConf($conf); + return [ + 'SQLite 3' => [ + 'interface' => (function() { + if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { + try { + $d = new \SQLite3(Arsse::$conf->dbSQLite3File); + } catch (\Exception $e) { + return; + } + $d->enableExceptions(true); + return $d; + } + })(), + 'statement' => \JKingWeb\Arsse\Db\SQLite3\Statement::class, + 'result' => \JKingWeb\Arsse\Db\SQLite3\Result::class, + 'stringOutput' => false, + ], + 'PDO SQLite 3' => [ + 'interface' => (function() { + if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { + try { + return new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + } catch (\PDOException $e) { + return; + } + } + })(), + 'statement' => \JKingWeb\Arsse\Db\PDOStatement::class, + 'result' => \JKingWeb\Arsse\Db\PDOResult::class, + 'stringOutput' => true, + ], + 'PDO PostgreSQL' => [ + 'interface' => (function() { + if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) { + $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); + try { + $c = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + } catch (\PDOException $e) { + return; + } + foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { + $c->exec($q); + } + return $c; + } + })(), + 'statement' => \JKingWeb\Arsse\Db\PDOStatement::class, + 'result' => \JKingWeb\Arsse\Db\PDOResult::class, + 'stringOutput' => true, + ], + ]; + } } From 736a8c9d0c0cd146e843af82df690973b0a5719d Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 22 Nov 2018 13:30:13 -0500 Subject: [PATCH 11/58] Improved timeout handling for both SQlite and PostgreSQL --- lib/Conf.php | 8 +++-- lib/Db/PDOError.php | 3 ++ lib/Db/PostgreSQL/Driver.php | 29 +++++++++++++++++-- lib/Db/SQLite3/Driver.php | 22 ++++++++++---- .../{TestDriver.php => TestCreation.php} | 7 +++-- tests/phpunit.xml | 2 +- 6 files changed, 58 insertions(+), 13 deletions(-) rename tests/cases/Db/PostgreSQL/{TestDriver.php => TestCreation.php} (94%) diff --git a/lib/Conf.php b/lib/Conf.php index f15926c3..4572cd39 100644 --- a/lib/Conf.php +++ b/lib/Conf.php @@ -19,12 +19,16 @@ class Conf { public $dbDriver = Db\SQLite3\Driver::class; /** @var boolean Whether to attempt to automatically update the database when updated to a new version with schema changes */ public $dbAutoUpdate = true; + /** @var float Number of seconds to wait before returning a timeout error when connecting to a database (zero waits forever; not applicable to SQLite) */ + public $dbTimeoutConnect = 5.0; + /** @var float Number of seconds to wait before returning a timeout error when executing a database operation (zero waits forever; not applicable to SQLite) */ + public $dbTimeoutExec = 0.0; /** @var string|null Full path and file name of SQLite database (if using SQLite) */ public $dbSQLite3File = null; /** @var string Encryption key to use for SQLite database (if using a version of SQLite with SEE) */ public $dbSQLite3Key = ""; - /** @var integer Number of seconds for SQLite to wait before returning a timeout error when writing to the database */ - public $dbSQLite3Timeout = 60; + /** @var float Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */ + public $dbSQLite3Timeout = 60.0; /** @var string Host name, address, or socket path of PostgreSQL database server (if using PostgreSQL) */ public $dbPostgreSQLHost = ""; /** @var string Log-in user name for PostgreSQL database server (if using PostgreSQL) */ diff --git a/lib/Db/PDOError.php b/lib/Db/PDOError.php index 206e0224..d9ee7c86 100644 --- a/lib/Db/PDOError.php +++ b/lib/Db/PDOError.php @@ -19,6 +19,9 @@ trait PDOError { case "23000": case "23502": return [ExceptionInput::class, "constraintViolation", $err[2]]; + case "55P03": + case "57014": + return [ExceptionTimeout::class, 'general', $err[2]]; case "HY000": // engine-specific errors switch ($this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) { diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index ef09aad8..5e9f6372 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -35,6 +35,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $base = [ 'client_encoding' => "UTF8", 'application_name' => "arsse", + 'connect_timeout' => (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0), ]; $out = []; if ($service != "") { @@ -66,9 +67,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } public static function makeSetupQueries(string $schema = ""): array { + $timeout = ceil(Arsse::$conf->dbTimeoutExec * 1000); $out = [ "SET TIME ZONE UTC", - "SET DateStyle = 'ISO, MDY'" + "SET DateStyle = 'ISO, MDY'", + "SET statement_timeout = '$timeout'", ]; if (strlen($schema) > 0) { $out[] = 'SET search_path = \'"'.str_replace('"', '""', $schema).'", "$user", public\''; @@ -100,8 +103,30 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return $this->query("SELECT pg_encoding_to_char(encoding) from pg_database where datname = current_database()")->getValue() == "UTF8"; } + public function savepointCreate(bool $lock = false): int { + if (!$this->transDepth) { + $this->exec("BEGIN TRANSACTION"); + } + return parent::savepointCreate($lock); + } + + public function savepointRelease(int $index = null): bool { + $out = parent::savepointUndo($index); + if ($out && !$this->transDepth) { + $this->exec("COMMIT TRANSACTION"); + } + return $out; + } + + public function savepointUndo(int $index = null): bool { + $out = parent::savepointUndo($index); + if ($out && !$this->transDepth) { + $this->exec("ROLLBACK TRANSACTION"); + } + return $out; + } + protected function lock(): bool { - $this->exec("BEGIN TRANSACTION"); if ($this->schemaVersion()) { $this->exec("LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"); } diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 5b94bc00..f3660488 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -27,13 +27,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } // if no database file is specified in the configuration, use a suitable default $dbFile = $dbFile ?? Arsse::$conf->dbSQLite3File ?? \JKingWeb\Arsse\BASE."arsse.db"; - $timeout = Arsse::$conf->dbSQLite3Timeout * 1000; try { $this->makeConnection($dbFile, Arsse::$conf->dbSQLite3Key); - // set the timeout; parameters are not allowed for pragmas, but this usage should be safe - $this->exec("PRAGMA busy_timeout = $timeout"); - // set other initial options - $this->exec("PRAGMA foreign_keys = yes"); } catch (\Throwable $e) { // if opening the database doesn't work, check various pre-conditions to find out what the problem might be $files = [ @@ -55,6 +50,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { // otherwise the database is probably corrupt throw new Exception("fileCorrupt", $dbFile); } + // set the timeout + $timeout = (int) ceil((Arsse::$conf->dbSQLite3Timeout ?? 0) * 1000); + $this->setTimeout($timeout); + // set other initial options + $this->exec("PRAGMA foreign_keys = yes"); } public static function requirementsMet(): bool { @@ -67,6 +67,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $this->db->enableExceptions(true); } + protected function setTimeout(int $msec) { + $this->exec("PRAGMA busy_timeout = $msec"); + } + public function __destruct() { try { $this->db->close(); @@ -157,7 +161,13 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } protected function lock(): bool { - $this->exec("BEGIN EXCLUSIVE TRANSACTION"); + $timeout = (int) $this->query("PRAGMA busy_timeout")->getValue(); + $this->setTimeout(0); + try { + $this->exec("BEGIN EXCLUSIVE TRANSACTION"); + } finally { + $this->setTimeout($timeout); + } return true; } diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestCreation.php similarity index 94% rename from tests/cases/Db/PostgreSQL/TestDriver.php rename to tests/cases/Db/PostgreSQL/TestCreation.php index 59e113bb..1190736c 100644 --- a/tests/cases/Db/PostgreSQL/TestDriver.php +++ b/tests/cases/Db/PostgreSQL/TestCreation.php @@ -6,14 +6,17 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; +use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Db\PostgreSQL\Driver; /** * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver */ -class TestDriver extends \JKingWeb\Arsse\Test\AbstractTest { +class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideConnectionStrings */ public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { - $postfix = "application_name='arsse' client_encoding='UTF8'"; + $this->setConf(); + $timeout = (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0); + $postfix = "application_name='arsse' client_encoding='UTF8' connect_timeout='$timeout'"; $act = Driver::makeConnectionString($pdo, $user, $pass, $db, $host, $port, $service); if ($act==$postfix) { $this->assertSame($exp, ""); diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 564ced7b..67914ae7 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -56,7 +56,7 @@ cases/Db/SQLite3PDO/TestDriver.php cases/Db/SQLite3PDO/TestUpdate.php - cases/Db/PostgreSQL/TestDriver.php + cases/Db/PostgreSQL/TestCreation.php cases/Db/SQLite3/Database/TestMiscellany.php From 8103d37bc77772534a571d9052496000edffa910 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 22 Nov 2018 13:36:25 -0500 Subject: [PATCH 12/58] Dev dependency update --- vendor-bin/csfixer/composer.lock | 10 ++-- vendor-bin/robo/composer.lock | 100 +++++++++++++++---------------- 2 files changed, 52 insertions(+), 58 deletions(-) diff --git a/vendor-bin/csfixer/composer.lock b/vendor-bin/csfixer/composer.lock index 65c2fd7f..9a892683 100644 --- a/vendor-bin/csfixer/composer.lock +++ b/vendor-bin/csfixer/composer.lock @@ -423,16 +423,16 @@ }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -466,7 +466,7 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-11-20T15:27:04+00:00" }, { "name": "symfony/console", diff --git a/vendor-bin/robo/composer.lock b/vendor-bin/robo/composer.lock index e1106e30..09a7ac7e 100644 --- a/vendor-bin/robo/composer.lock +++ b/vendor-bin/robo/composer.lock @@ -8,20 +8,20 @@ "packages": [ { "name": "consolidation/annotated-command", - "version": "2.9.1", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/consolidation/annotated-command.git", - "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac" + "reference": "8e7d1a05230dc1159c751809e98b74f2b7f71873" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", - "reference": "4bdbb8fa149e1cc1511bd77b0bc4729fd66bccac", + "url": "https://api.github.com/repos/consolidation/annotated-command/zipball/8e7d1a05230dc1159c751809e98b74f2b7f71873", + "reference": "8e7d1a05230dc1159c751809e98b74f2b7f71873", "shasum": "" }, "require": { - "consolidation/output-formatters": "^3.1.12", + "consolidation/output-formatters": "^3.4", "php": ">=5.4.0", "psr/log": "^1", "symfony/console": "^2.8|^3|^4", @@ -56,7 +56,7 @@ } ], "description": "Initialize Symfony Console commands from annotated command class methods.", - "time": "2018-09-19T17:47:18+00:00" + "time": "2018-11-15T01:46:18+00:00" }, { "name": "consolidation/config", @@ -219,16 +219,16 @@ }, { "name": "consolidation/robo", - "version": "1.3.1", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/consolidation/Robo.git", - "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d" + "reference": "a9bd9ecf00751aa92754903c0d17612c4e840ce8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/consolidation/Robo/zipball/31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", - "reference": "31f2d2562c4e1dcde70f2659eefd59aa9c7f5b2d", + "url": "https://api.github.com/repos/consolidation/Robo/zipball/a9bd9ecf00751aa92754903c0d17612c4e840ce8", + "reference": "a9bd9ecf00751aa92754903c0d17612c4e840ce8", "shasum": "" }, "require": { @@ -237,7 +237,6 @@ "consolidation/log": "~1", "consolidation/output-formatters": "^3.1.13", "consolidation/self-update": "^1", - "g1a/composer-test-scenarios": "^2", "grasmash/yaml-expander": "^1.3", "league/container": "^2.2", "php": ">=5.5.0", @@ -254,14 +253,15 @@ "codeception/aspect-mock": "^1|^2.1.1", "codeception/base": "^2.3.7", "codeception/verify": "^0.3.2", + "g1a/composer-test-scenarios": "^3", "goaop/framework": "~2.1.2", "goaop/parser-reflection": "^1.1.0", "natxet/cssmin": "3.0.4", "nikic/php-parser": "^3.1.5", "patchwork/jsqueeze": "~2", "pear/archive_tar": "^1.4.2", + "php-coveralls/php-coveralls": "^1", "phpunit/php-code-coverage": "~2|~4", - "satooshi/php-coveralls": "^2", "squizlabs/php_codesniffer": "^2.8" }, "suggest": { @@ -275,9 +275,36 @@ ], "type": "library", "extra": { + "scenarios": { + "symfony4": { + "require": { + "symfony/console": "^4" + }, + "config": { + "platform": { + "php": "7.1.3" + } + } + }, + "symfony2": { + "require": { + "symfony/console": "^2.8" + }, + "remove": [ + "goaop/framework" + ], + "config": { + "platform": { + "php": "5.5.9" + } + }, + "scenario-options": { + "create-lockfile": "false" + } + } + }, "branch-alias": { - "dev-master": "1.x-dev", - "dev-state": "1.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { @@ -296,7 +323,7 @@ } ], "description": "Modern task runner", - "time": "2018-08-17T18:44:18+00:00" + "time": "2018-11-22T05:43:44+00:00" }, { "name": "consolidation/self-update", @@ -438,39 +465,6 @@ ], "time": "2017-01-20T21:14:22+00:00" }, - { - "name": "g1a/composer-test-scenarios", - "version": "2.2.0", - "source": { - "type": "git", - "url": "https://github.com/g1a/composer-test-scenarios.git", - "reference": "a166fd15191aceab89f30c097e694b7cf3db4880" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/g1a/composer-test-scenarios/zipball/a166fd15191aceab89f30c097e694b7cf3db4880", - "reference": "a166fd15191aceab89f30c097e694b7cf3db4880", - "shasum": "" - }, - "bin": [ - "scripts/create-scenario", - "scripts/dependency-licenses", - "scripts/install-scenario" - ], - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Greg Anderson", - "email": "greg.1.anderson@greenknowe.org" - } - ], - "description": "Useful scripts for testing multiple sets of Composer dependencies.", - "time": "2018-08-08T23:37:23+00:00" - }, { "name": "grasmash/expander", "version": "1.0.0", @@ -894,16 +888,16 @@ }, { "name": "psr/log", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", - "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", "shasum": "" }, "require": { @@ -937,7 +931,7 @@ "psr", "psr-3" ], - "time": "2016-10-10T12:19:37+00:00" + "time": "2018-11-20T15:27:04+00:00" }, { "name": "symfony/console", From aa1b65b5d42f6f163870377622c7384d8b0bc01b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 22 Nov 2018 13:55:57 -0500 Subject: [PATCH 13/58] Take a different tack on shared database tests Tests for different drivers will have their own files, but all derive from a common prototype test series where applicable, similar to the existing arrangement for database function tests. However, the prototype will reside with other test cases rather than in the library path. The database function test series will hopefully be moved as well in time. --- composer.json | 3 +- tests/cases/Db/BaseDriver.php | 357 ++++++++++++++++++++ tests/cases/Db/SQLite3/TestDriver.php | 406 +++++------------------ tests/cases/Db/SQLite3PDO/TestDriver.php | 346 ++----------------- tests/lib/AbstractTest.php | 109 +++++- tests/phpunit.xml | 1 + 6 files changed, 571 insertions(+), 651 deletions(-) create mode 100644 tests/cases/Db/BaseDriver.php diff --git a/composer.json b/composer.json index aa4ac4a8..5f943d94 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ }, "autoload-dev": { "psr-4": { - "JKingWeb\\Arsse\\Test\\": "tests/lib/" + "JKingWeb\\Arsse\\Test\\": "tests/lib/", + "JKingWeb\\Arsse\\TestCase\\": "tests/cases/" } } } diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php new file mode 100644 index 00000000..4b6d68ab --- /dev/null +++ b/tests/cases/Db/BaseDriver.php @@ -0,0 +1,357 @@ + 0.5, + 'dbSQLite3Timeout' => 0, + ]; + + public function setUp() { + $this->setConf($this->conf); + $this->interface = $this->getDbInterface($this->implementation); + if (!$this->interface) { + $this->markTestSkipped("$this->implementation database driver not available"); + } + $this->drv = $this->getDbDriver($this->implementation); + $this->exec("DROP TABLE IF EXISTS arsse_test"); + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + $this->exec("CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)"); + $this->exec("INSERT INTO arsse_meta(key,value) values('schema_version','0')"); + } + + public function tearDown() { + unset($this->drv); + try { + $this->exec("ROLLBACK"); + } catch(\Throwable $e) { + } + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + $this->exec("DROP TABLE IF EXISTS arsse_test"); + } + + protected function exec(string $q): bool { + // PDO implementation + $this->interface->exec($q); + return true; + } + + protected function query(string $q) { + // PDO implementation + return $this->interface->query($q)->fetchColumn(); + } + + # TESTS + + public function testFetchDriverName() { + $class = get_class($this->drv); + $this->assertTrue(strlen($class::driverName()) > 0); + } + + public function testCheckCharacterSetAcceptability() { + $this->assertTrue($this->drv->charsetAcceptable()); + } + + public function testExecAValidStatement() { + $this->assertTrue($this->drv->exec($this->create)); + } + + public function testExecAnInvalidStatement() { + $this->assertException("engineErrorGeneral", "Db"); + $this->drv->exec("And the meek shall inherit the earth..."); + } + + public function testExecMultipleStatements() { + $this->assertTrue($this->drv->exec("$this->create; INSERT INTO arsse_test(id) values(2112)")); + $this->assertEquals(2112, $this->query("SELECT id from arsse_test")); + } + + public function testExecTimeout() { + $this->exec($this->create); + $this->exec($this->lock); + $this->assertException("general", "Db", "ExceptionTimeout"); + $this->drv->exec($this->lock); + } + + 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"); + } + + public function testExecTypeViolation() { + $this->drv->exec($this->create); + $this->assertException("typeViolation", "Db", "ExceptionInput"); + $this->drv->exec("INSERT INTO arsse_test(id) values('ook')"); + } + + public function testMakeAValidQuery() { + $this->assertInstanceOf(Result::class, $this->drv->query("SELECT 1")); + } + + public function testMakeAnInvalidQuery() { + $this->assertException("engineErrorGeneral", "Db"); + $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"); + $this->drv->exec($this->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"); + } + + public function testQueryTypeViolation() { + $this->drv->exec($this->create); + $this->assertException("typeViolation", "Db", "ExceptionInput"); + $this->drv->query("INSERT INTO arsse_test(id) values('ook')"); + } + + public function testPrepareAValidQuery() { + $s = $this->drv->prepare("SELECT ?, ?", "int", "int"); + $this->assertInstanceOf(Statement::class, $s); + } + + public function testPrepareAnInvalidQuery() { + $this->assertException("engineErrorGeneral", "Db"); + $s = $this->drv->prepare("This is an invalid query", "int", "int")->run(); + } + + public function testCreateASavepoint() { + $this->assertEquals(1, $this->drv->savepointCreate()); + $this->assertEquals(2, $this->drv->savepointCreate()); + $this->assertEquals(3, $this->drv->savepointCreate()); + } + + public function testReleaseASavepoint() { + $this->assertEquals(1, $this->drv->savepointCreate()); + $this->assertEquals(true, $this->drv->savepointRelease()); + $this->assertException("savepointInvalid", "Db"); + $this->drv->savepointRelease(); + } + + public function testUndoASavepoint() { + $this->assertEquals(1, $this->drv->savepointCreate()); + $this->assertEquals(true, $this->drv->savepointUndo()); + $this->assertException("savepointInvalid", "Db"); + $this->drv->savepointUndo(); + } + + public function testManipulateSavepoints() { + $this->assertEquals(1, $this->drv->savepointCreate()); + $this->assertEquals(2, $this->drv->savepointCreate()); + $this->assertEquals(3, $this->drv->savepointCreate()); + $this->assertEquals(4, $this->drv->savepointCreate()); + $this->assertEquals(5, $this->drv->savepointCreate()); + $this->assertTrue($this->drv->savepointUndo(3)); + $this->assertFalse($this->drv->savepointRelease(4)); + $this->assertEquals(6, $this->drv->savepointCreate()); + $this->assertFalse($this->drv->savepointRelease(5)); + $this->assertTrue($this->drv->savepointRelease(6)); + $this->assertEquals(3, $this->drv->savepointCreate()); + $this->assertTrue($this->drv->savepointRelease(2)); + $this->assertException("savepointStale", "Db"); + $this->drv->savepointRelease(2); + } + + public function testManipulateSavepointsSomeMore() { + $this->assertEquals(1, $this->drv->savepointCreate()); + $this->assertEquals(2, $this->drv->savepointCreate()); + $this->assertEquals(3, $this->drv->savepointCreate()); + $this->assertEquals(4, $this->drv->savepointCreate()); + $this->assertTrue($this->drv->savepointRelease(2)); + $this->assertFalse($this->drv->savepointUndo(3)); + $this->assertException("savepointStale", "Db"); + $this->drv->savepointUndo(2); + } + + 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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $this->drv->query($insert); + $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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr->commit(); + $this->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(1, $this->query($select)); + } + + 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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr->rollback(); + $this->assertEquals(0, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + } + + 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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2 = $this->drv->begin(); + $this->drv->query($insert); + $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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2 = $this->drv->begin(); + $this->drv->query($insert); + $this->assertEquals(2, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2->commit(); + $this->assertEquals(0, $this->query($select)); + $tr1->commit(); + $this->assertEquals(2, $this->query($select)); + } + + 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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2 = $this->drv->begin(); + $this->drv->query($insert); + $this->assertEquals(2, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr1->commit(); + $this->assertEquals(2, $this->query($select)); + $tr2->commit(); + } + + 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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2 = $this->drv->begin(); + $this->drv->query($insert); + $this->assertEquals(2, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2->rollback(); + $this->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr1->rollback(); + $this->assertEquals(0, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + } + + 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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2 = $this->drv->begin(); + $this->drv->query($insert); + $this->assertEquals(2, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr1->rollback(); + $this->assertEquals(0, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2->rollback(); + $this->assertEquals(0, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + } + + 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->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2 = $this->drv->begin(); + $this->drv->query($insert); + $this->assertEquals(2, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr2->rollback(); + $this->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(0, $this->query($select)); + $tr1->commit(); + $this->assertEquals(1, $this->drv->query($select)->getValue()); + $this->assertEquals(1, $this->query($select)); + } + + public function testFetchSchemaVersion() { + $this->assertSame(0, $this->drv->schemaVersion()); + $this->drv->exec(str_replace("#", "1", $this->setVersion)); + $this->assertSame(1, $this->drv->schemaVersion()); + $this->drv->exec(str_replace("#", "2", $this->setVersion)); + $this->assertSame(2, $this->drv->schemaVersion()); + } + + public function testLockTheDatabase() { + $this->drv->savepointCreate(true); + $this->assertException(); + $this->exec($this->create); + } + + public function testUnlockTheDatabase() { + $this->drv->savepointCreate(true); + $this->drv->savepointRelease(); + $this->drv->savepointCreate(true); + $this->drv->savepointUndo(); + $this->assertTrue($this->exec($this->create)); + } +} diff --git a/tests/cases/Db/SQLite3/TestDriver.php b/tests/cases/Db/SQLite3/TestDriver.php index 835ad095..3316a01a 100644 --- a/tests/cases/Db/SQLite3/TestDriver.php +++ b/tests/cases/Db/SQLite3/TestDriver.php @@ -6,338 +6,96 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\SQLite3; -use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Conf; -use JKingWeb\Arsse\Database; -use JKingWeb\Arsse\Db\SQLite3\Driver; -use JKingWeb\Arsse\Db\Result; -use JKingWeb\Arsse\Db\Statement; - /** * @covers \JKingWeb\Arsse\Db\SQLite3\Driver * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ -class TestDriver extends \JKingWeb\Arsse\Test\AbstractTest { - protected $data; - protected $drv; - protected $ch; +class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { + protected $implementation = "SQLite 3"; + protected $create = "CREATE TABLE arsse_test(id integer primary key)"; + protected $lock = "BEGIN EXCLUSIVE TRANSACTION"; + protected $setVersion = "PRAGMA user_version=#"; + protected static $file; + public static function setUpBeforeClass() { + self::$file = tempnam(sys_get_temp_dir(), 'ook'); + } + + public static function tearDownAfterClass() { + @unlink(self::$file); + self::$file = null; + } + public function setUp() { - if (!Driver::requirementsMet()) { - $this->markTestSkipped("SQLite extension not loaded"); - } - $this->clearData(); - $this->setConf([ - 'dbDriver' => Driver::class, - 'dbSQLite3Timeout' => 0, - 'dbSQLite3File' => tempnam(sys_get_temp_dir(), 'ook'), - ]); - $this->drv = new Driver(); - $this->ch = new \SQLite3(Arsse::$conf->dbSQLite3File); - $this->ch->enableExceptions(true); + $this->conf['dbSQLite3File'] = self::$file; + parent::setUp(); + $this->exec("PRAGMA user_version=0"); } public function tearDown() { - unset($this->drv); - unset($this->ch); - if (isset(Arsse::$conf)) { - unlink(Arsse::$conf->dbSQLite3File); - } + parent::tearDown(); + $this->exec("PRAGMA user_version=0"); + $this->interface->close(); + unset($this->interface); + } + + protected function exec(string $q): bool { + $this->interface->exec($q); + return true; + } + + protected function query(string $q) { + return $this->interface->querySingle($q); + } + + public function provideDrivers() { $this->clearData(); - } - - public function testFetchDriverName() { - $class = Arsse::$conf->dbDriver; - $this->assertTrue(strlen($class::driverName()) > 0); - } - - public function testCheckCharacterSetAcceptability() { - $this->assertTrue($this->drv->charsetAcceptable()); - } - - public function testExecAValidStatement() { - $this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key)")); - } - - public function testExecAnInvalidStatement() { - $this->assertException("engineErrorGeneral", "Db"); - $this->drv->exec("And the meek shall inherit the earth..."); - } - - public function testExecMultipleStatements() { - $this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key); INSERT INTO test(id) values(2112)")); - $this->assertEquals(2112, $this->ch->querySingle("SELECT id from test")); - } - - public function testExecTimeout() { - $this->ch->exec("BEGIN EXCLUSIVE TRANSACTION"); - $this->assertException("general", "Db", "ExceptionTimeout"); - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - } - - public function testExecConstraintViolation() { - $this->drv->exec("CREATE TABLE test(id integer not null)"); - $this->assertException("constraintViolation", "Db", "ExceptionInput"); - $this->drv->exec("INSERT INTO test(id) values(null)"); - } - - public function testExecTypeViolation() { - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $this->assertException("typeViolation", "Db", "ExceptionInput"); - $this->drv->exec("INSERT INTO test(id) values('ook')"); - } - - public function testMakeAValidQuery() { - $this->assertInstanceOf(Result::class, $this->drv->query("SELECT 1")); - } - - public function testMakeAnInvalidQuery() { - $this->assertException("engineErrorGeneral", "Db"); - $this->drv->query("Apollo was astonished; Dionysus thought me mad"); - } - - public function testQueryTimeout() { - $this->ch->exec("BEGIN EXCLUSIVE TRANSACTION"); - $this->assertException("general", "Db", "ExceptionTimeout"); - $this->drv->query("CREATE TABLE test(id integer primary key)"); - } - - public function testQueryConstraintViolation() { - $this->drv->exec("CREATE TABLE test(id integer not null)"); - $this->assertException("constraintViolation", "Db", "ExceptionInput"); - $this->drv->query("INSERT INTO test(id) values(null)"); - } - - public function testQueryTypeViolation() { - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $this->assertException("typeViolation", "Db", "ExceptionInput"); - $this->drv->query("INSERT INTO test(id) values('ook')"); - } - - public function testPrepareAValidQuery() { - $s = $this->drv->prepare("SELECT ?, ?", "int", "int"); - $this->assertInstanceOf(Statement::class, $s); - } - - public function testPrepareAnInvalidQuery() { - $this->assertException("engineErrorGeneral", "Db"); - $s = $this->drv->prepare("This is an invalid query", "int", "int"); - } - - public function testCreateASavepoint() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(2, $this->drv->savepointCreate()); - $this->assertEquals(3, $this->drv->savepointCreate()); - } - - public function testReleaseASavepoint() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(true, $this->drv->savepointRelease()); - $this->assertException("savepointInvalid", "Db"); - $this->drv->savepointRelease(); - } - - public function testUndoASavepoint() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(true, $this->drv->savepointUndo()); - $this->assertException("savepointInvalid", "Db"); - $this->drv->savepointUndo(); - } - - public function testManipulateSavepoints() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(2, $this->drv->savepointCreate()); - $this->assertEquals(3, $this->drv->savepointCreate()); - $this->assertEquals(4, $this->drv->savepointCreate()); - $this->assertEquals(5, $this->drv->savepointCreate()); - $this->assertTrue($this->drv->savepointUndo(3)); - $this->assertFalse($this->drv->savepointRelease(4)); - $this->assertEquals(6, $this->drv->savepointCreate()); - $this->assertFalse($this->drv->savepointRelease(5)); - $this->assertTrue($this->drv->savepointRelease(6)); - $this->assertEquals(3, $this->drv->savepointCreate()); - $this->assertTrue($this->drv->savepointRelease(2)); - $this->assertException("savepointStale", "Db"); - $this->drv->savepointRelease(2); - } - - public function testManipulateSavepointsSomeMore() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(2, $this->drv->savepointCreate()); - $this->assertEquals(3, $this->drv->savepointCreate()); - $this->assertEquals(4, $this->drv->savepointCreate()); - $this->assertTrue($this->drv->savepointRelease(2)); - $this->assertFalse($this->drv->savepointUndo(3)); - $this->assertException("savepointStale", "Db"); - $this->drv->savepointUndo(2); - } - - public function testBeginATransaction() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - } - - public function testCommitATransaction() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr->commit(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(1, $this->ch->querySingle($select)); - } - - public function testRollbackATransaction() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - } - - public function testBeginChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - } - - public function testCommitChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2->commit(); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr1->commit(); - $this->assertEquals(2, $this->ch->querySingle($select)); - } - - public function testCommitChainedTransactionsOutOfOrder() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr1->commit(); - $this->assertEquals(2, $this->ch->querySingle($select)); - $tr2->commit(); - } - - public function testRollbackChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2->rollback(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr1->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - } - - public function testRollbackChainedTransactionsOutOfOrder() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr1->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - } - - public function testPartiallyRollbackChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr2->rollback(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->querySingle($select)); - $tr1->commit(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(1, $this->ch->querySingle($select)); - } - - public function testFetchSchemaVersion() { - $this->assertSame(0, $this->drv->schemaVersion()); - $this->drv->exec("PRAGMA user_version=1"); - $this->assertSame(1, $this->drv->schemaVersion()); - $this->drv->exec("PRAGMA user_version=2"); - $this->assertSame(2, $this->drv->schemaVersion()); - } - - public function testLockTheDatabase() { - $this->drv->savepointCreate(true); - $this->assertException(); - $this->ch->exec("CREATE TABLE test(id integer primary key)"); - } - - public function testUnlockTheDatabase() { - $this->drv->savepointCreate(true); - $this->drv->savepointRelease(); - $this->drv->savepointCreate(true); - $this->drv->savepointUndo(); - $this->assertSame(true, $this->ch->exec("CREATE TABLE test(id integer primary key)")); + $this->setConf([ + 'dbTimeoutExec' => 0.5, + 'dbSQLite3Timeout' => 0, + 'dbSQLite3File' => tempnam(sys_get_temp_dir(), 'ook'), + ]); + $i = $this->provideDbInterfaces(); + $d = $this->provideDbDrivers(); + $pdoExec = function (string $q) { + $this->interface->exec($q); + return true; + }; + $pdoQuery = function (string $q) { + return $this->interface->query($q)->fetchColumn(); + }; + return [ + 'SQLite 3' => [ + $i['SQLite 3']['interface'], + $d['SQLite 3'], + "CREATE TABLE arsse_test(id integer primary key)", + "BEGIN EXCLUSIVE TRANSACTION", + "PRAGMA user_version=#", + function (string $q) { + $this->interface->exec($q); + return true; + }, + function (string $q) { + return $this->interface->querySingle($q); + }, + ], + 'PDO SQLite 3' => [ + $i['PDO SQLite 3']['interface'], + $d['PDO SQLite 3'], + "CREATE TABLE arsse_test(id integer primary key)", + "BEGIN EXCLUSIVE TRANSACTION", + "PRAGMA user_version=#", + $pdoExec, + $pdoQuery, + ], + 'PDO PostgreSQL' => [ + $i['PDO PostgreSQL']['interface'], + $d['PDO PostgreSQL'], + "CREATE TABLE arsse_test(id bigserial primary key)", + "BEGIN; LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT", + "UPDATE arsse_meta set value = '#' where key = 'schema_version'", + $pdoExec, + $pdoQuery, + ], + ]; } } diff --git a/tests/cases/Db/SQLite3PDO/TestDriver.php b/tests/cases/Db/SQLite3PDO/TestDriver.php index 6d58f51b..73ae773e 100644 --- a/tests/cases/Db/SQLite3PDO/TestDriver.php +++ b/tests/cases/Db/SQLite3PDO/TestDriver.php @@ -6,339 +6,35 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO; -use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Conf; -use JKingWeb\Arsse\Database; -use JKingWeb\Arsse\Db\SQLite3\PDODriver; -use JKingWeb\Arsse\Db\Result; -use JKingWeb\Arsse\Db\Statement; - /** * @covers \JKingWeb\Arsse\Db\SQLite3\PDODriver * @covers \JKingWeb\Arsse\Db\PDODriver * @covers \JKingWeb\Arsse\Db\PDOError */ -class TestDriver extends \JKingWeb\Arsse\Test\AbstractTest { - protected $data; - protected $drv; - protected $ch; +class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { + protected $implementation = "PDO SQLite 3"; + protected $create = "CREATE TABLE arsse_test(id integer primary key)"; + protected $lock = "BEGIN EXCLUSIVE TRANSACTION"; + protected $setVersion = "PRAGMA user_version=#"; + protected static $file; + public static function setUpBeforeClass() { + self::$file = tempnam(sys_get_temp_dir(), 'ook'); + } + + public static function tearDownAfterClass() { + @unlink(self::$file); + self::$file = null; + } + public function setUp() { - if (!PDODriver::requirementsMet()) { - $this->markTestSkipped("PDO-SQLite extension not loaded"); - } - $this->clearData(); - $this->setConf([ - 'dbDriver' => PDODriver::class, - 'dbSQLite3Timeout' => 0, - 'dbSQLite3File' => tempnam(sys_get_temp_dir(), 'ook'), - ]); - $this->drv = new PDODriver(); - $this->ch = new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $this->conf['dbSQLite3File'] = self::$file; + parent::setUp(); + $this->exec("PRAGMA user_version=0"); } public function tearDown() { - unset($this->drv); - unset($this->ch); - if (isset(Arsse::$conf)) { - unlink(Arsse::$conf->dbSQLite3File); - } - $this->clearData(); - } - - public function testFetchDriverName() { - $class = Arsse::$conf->dbDriver; - $this->assertTrue(strlen($class::driverName()) > 0); - } - - public function testCheckCharacterSetAcceptability() { - $this->assertTrue($this->drv->charsetAcceptable()); - } - - public function testExecAValidStatement() { - $this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key)")); - } - - public function testExecAnInvalidStatement() { - $this->assertException("engineErrorGeneral", "Db"); - $this->drv->exec("And the meek shall inherit the earth..."); - } - - public function testExecMultipleStatements() { - $this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key); INSERT INTO test(id) values(2112)")); - $this->assertEquals(2112, $this->ch->query("SELECT id from test")->fetchColumn()); - } - - public function testExecTimeout() { - $this->ch->exec("BEGIN EXCLUSIVE TRANSACTION"); - $this->assertException("general", "Db", "ExceptionTimeout"); - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - } - - public function testExecConstraintViolation() { - $this->drv->exec("CREATE TABLE test(id integer not null)"); - $this->assertException("constraintViolation", "Db", "ExceptionInput"); - $this->drv->exec("INSERT INTO test(id) values(null)"); - } - - public function testExecTypeViolation() { - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $this->assertException("typeViolation", "Db", "ExceptionInput"); - $this->drv->exec("INSERT INTO test(id) values('ook')"); - } - - public function testMakeAValidQuery() { - $this->assertInstanceOf(Result::class, $this->drv->query("SELECT 1")); - } - - public function testMakeAnInvalidQuery() { - $this->assertException("engineErrorGeneral", "Db"); - $this->drv->query("Apollo was astonished; Dionysus thought me mad"); - } - - public function testQueryTimeout() { - $this->ch->exec("BEGIN EXCLUSIVE TRANSACTION"); - $this->assertException("general", "Db", "ExceptionTimeout"); - $this->drv->query("CREATE TABLE test(id integer primary key)"); - } - - public function testQueryConstraintViolation() { - $this->drv->exec("CREATE TABLE test(id integer not null)"); - $this->assertException("constraintViolation", "Db", "ExceptionInput"); - $this->drv->query("INSERT INTO test(id) values(null)"); - } - - public function testQueryTypeViolation() { - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $this->assertException("typeViolation", "Db", "ExceptionInput"); - $this->drv->query("INSERT INTO test(id) values('ook')"); - } - - public function testPrepareAValidQuery() { - $s = $this->drv->prepare("SELECT ?, ?", "int", "int"); - $this->assertInstanceOf(Statement::class, $s); - } - - public function testPrepareAnInvalidQuery() { - $this->assertException("engineErrorGeneral", "Db"); - $s = $this->drv->prepare("This is an invalid query", "int", "int"); - } - - public function testCreateASavepoint() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(2, $this->drv->savepointCreate()); - $this->assertEquals(3, $this->drv->savepointCreate()); - } - - public function testReleaseASavepoint() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(true, $this->drv->savepointRelease()); - $this->assertException("savepointInvalid", "Db"); - $this->drv->savepointRelease(); - } - - public function testUndoASavepoint() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(true, $this->drv->savepointUndo()); - $this->assertException("savepointInvalid", "Db"); - $this->drv->savepointUndo(); - } - - public function testManipulateSavepoints() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(2, $this->drv->savepointCreate()); - $this->assertEquals(3, $this->drv->savepointCreate()); - $this->assertEquals(4, $this->drv->savepointCreate()); - $this->assertEquals(5, $this->drv->savepointCreate()); - $this->assertTrue($this->drv->savepointUndo(3)); - $this->assertFalse($this->drv->savepointRelease(4)); - $this->assertEquals(6, $this->drv->savepointCreate()); - $this->assertFalse($this->drv->savepointRelease(5)); - $this->assertTrue($this->drv->savepointRelease(6)); - $this->assertEquals(3, $this->drv->savepointCreate()); - $this->assertTrue($this->drv->savepointRelease(2)); - $this->assertException("savepointStale", "Db"); - $this->drv->savepointRelease(2); - } - - public function testManipulateSavepointsSomeMore() { - $this->assertEquals(1, $this->drv->savepointCreate()); - $this->assertEquals(2, $this->drv->savepointCreate()); - $this->assertEquals(3, $this->drv->savepointCreate()); - $this->assertEquals(4, $this->drv->savepointCreate()); - $this->assertTrue($this->drv->savepointRelease(2)); - $this->assertFalse($this->drv->savepointUndo(3)); - $this->assertException("savepointStale", "Db"); - $this->drv->savepointUndo(2); - } - - public function testBeginATransaction() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - } - - public function testCommitATransaction() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr->commit(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(1, $this->ch->query($select)->fetchColumn()); - } - - public function testRollbackATransaction() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - } - - public function testBeginChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - } - - public function testCommitChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2->commit(); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr1->commit(); - $this->assertEquals(2, $this->ch->query($select)->fetchColumn()); - } - - public function testCommitChainedTransactionsOutOfOrder() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr1->commit(); - $this->assertEquals(2, $this->ch->query($select)->fetchColumn()); - $tr2->commit(); - } - - public function testRollbackChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2->rollback(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr1->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - } - - public function testRollbackChainedTransactionsOutOfOrder() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr1->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2->rollback(); - $this->assertEquals(0, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - } - - public function testPartiallyRollbackChainedTransactions() { - $select = "SELECT count(*) FROM test"; - $insert = "INSERT INTO test(id) values(null)"; - $this->drv->exec("CREATE TABLE test(id integer primary key)"); - $tr1 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2 = $this->drv->begin(); - $this->drv->query($insert); - $this->assertEquals(2, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr2->rollback(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(0, $this->ch->query($select)->fetchColumn()); - $tr1->commit(); - $this->assertEquals(1, $this->drv->query($select)->getValue()); - $this->assertEquals(1, $this->ch->query($select)->fetchColumn()); - } - - public function testFetchSchemaVersion() { - $this->assertSame(0, $this->drv->schemaVersion()); - $this->drv->exec("PRAGMA user_version=1"); - $this->assertSame(1, $this->drv->schemaVersion()); - $this->drv->exec("PRAGMA user_version=2"); - $this->assertSame(2, $this->drv->schemaVersion()); - } - - public function testLockTheDatabase() { - $this->drv->savepointCreate(true); - $this->ch->exec("PRAGMA busy_timeout = 0"); - $this->assertException(); - $this->ch->exec("CREATE TABLE test(id integer primary key)"); - } - - public function testUnlockTheDatabase() { - $this->drv->savepointCreate(true); - $this->drv->savepointRelease(); - $this->drv->savepointCreate(true); - $this->drv->savepointUndo(); - $this->assertSame(0, $this->ch->exec("CREATE TABLE test(id integer primary key)")); + parent::tearDown(); + $this->exec("PRAGMA user_version=0"); + unset($this->interface); } } diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 750c6b0c..e4930f04 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -43,11 +43,12 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { public function setConf(array $conf = []) { $defaults = [ 'dbSQLite3File' => ":memory:", + 'dbSQLite3Timeout' => 0, 'dbPostgreSQLUser' => "arsse_test", 'dbPostgreSQLPass' => "arsse_test", 'dbPostgreSQLDb' => "arsse_test", ]; - Arsse::$conf = (new Conf)->import($defaults)->import($conf); + Arsse::$conf = Arsse::$conf ?? (new Conf)->import($defaults)->import($conf); } public function assertException(string $msg = "", string $prefix = "", string $type = "Exception") { @@ -126,6 +127,33 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { return $value; } + public function provideDbDrivers(array $conf = []): array { + $this->setConf($conf); + return [ + 'SQLite 3' => (function() { + try { + return new \JKingWeb\Arsse\Db\SQLite3\Driver; + } catch (\Exception $e) { + return; + } + })(), + 'PDO SQLite 3' => (function() { + try { + return new \JKingWeb\Arsse\Db\SQLite3\PDODriver; + } catch (\Exception $e) { + return; + } + })(), + 'PDO PostgreSQL' => (function() { + try { + return new \JKingWeb\Arsse\Db\PostgreSQL\PDODriver; + } catch (\Exception $e) { + return; + } + })(), + ]; + } + public function provideDbInterfaces(array $conf = []): array { $this->setConf($conf); return [ @@ -180,4 +208,83 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { ], ]; } + + public function getDbDriver(string $name, array $conf = []) { + $this->setConf($conf); + switch ($name) { + case 'SQLite 3': + return (function() { + try { + return new \JKingWeb\Arsse\Db\SQLite3\Driver; + } catch (\Exception $e) { + return; + } + })(); + case 'PDO SQLite 3': + return (function() { + try { + return new \JKingWeb\Arsse\Db\SQLite3\PDODriver; + } catch (\Exception $e) { + return; + } + })(); + case 'PDO PostgreSQL': + return (function() { + try { + return new \JKingWeb\Arsse\Db\PostgreSQL\PDODriver; + } catch (\Exception $e) { + return; + } + })(); + default: + throw new \Exception("Invalid database driver name"); + } + } + + public function getDbInterface(string $name, array $conf = []) { + $this->setConf($conf); + switch ($name) { + case 'SQLite 3': + return (function() { + if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { + try { + $d = new \SQLite3(Arsse::$conf->dbSQLite3File); + } catch (\Exception $e) { + return; + } + $d->enableExceptions(true); + return $d; + } + })(); + case 'PDO SQLite 3': + return (function() { + if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { + try { + $d = new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $d->exec("PRAGMA busy_timeout=0"); + return $d; + } catch (\PDOException $e) { + return; + } + } + })(); + case 'PDO PostgreSQL': + return (function() { + if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) { + $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); + try { + $c = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + } catch (\PDOException $e) { + return; + } + foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { + $c->exec($q); + } + return $c; + } + })(); + default: + throw new \Exception("Invalid database driver name"); + } + } } diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 67914ae7..faf634e7 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -57,6 +57,7 @@ cases/Db/SQLite3PDO/TestUpdate.php cases/Db/PostgreSQL/TestCreation.php + cases/Db/PostgreSQL/TestDriver.php cases/Db/SQLite3/Database/TestMiscellany.php From f22e53fdc9352e90a798aa77b74b1157018444da Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 22 Nov 2018 19:55:54 -0500 Subject: [PATCH 14/58] Align result tests with driver tests --- tests/cases/Db/BaseDriver.php | 9 +- tests/cases/Db/BaseResult.php | 112 ++++++++++++++ tests/cases/Db/PostgreSQL/TestCreation.php | 2 +- tests/cases/Db/SQLite3/TestCreation.php | 2 +- tests/cases/Db/SQLite3/TestDriver.php | 2 +- tests/cases/Db/SQLite3/TestResult.php | 33 ++++ tests/cases/Db/SQLite3/TestUpdate.php | 2 +- tests/cases/Db/SQLite3PDO/TestCreation.php | 2 +- tests/cases/Db/SQLite3PDO/TestUpdate.php | 2 +- tests/cases/Db/TestResult.php | 155 ------------------- tests/cases/Db/TestResultPDO.php | 52 +++++++ tests/cases/Db/TestStatement.php | 52 ++----- tests/cases/Feed/TestFeed.php | 2 +- tests/cases/Feed/TestFetching.php | 2 +- tests/cases/REST/NextCloudNews/TestV1_2.php | 2 +- tests/cases/REST/TestREST.php | 4 +- tests/cases/REST/TinyTinyRSS/TestAPI.php | 6 +- tests/cases/REST/TinyTinyRSS/TestIcon.php | 4 +- tests/cases/Service/TestService.php | 2 +- tests/cases/User/TestInternal.php | 2 +- tests/cases/User/TestUser.php | 2 +- tests/lib/AbstractTest.php | 163 +------------------- tests/lib/Database/Setup.php | 2 +- tests/lib/DatabaseInformation.php | 111 +++++++++++++ tests/phpunit.xml | 10 +- 25 files changed, 358 insertions(+), 379 deletions(-) create mode 100644 tests/cases/Db/BaseResult.php create mode 100644 tests/cases/Db/SQLite3/TestResult.php delete mode 100644 tests/cases/Db/TestResult.php create mode 100644 tests/cases/Db/TestResultPDO.php create mode 100644 tests/lib/DatabaseInformation.php diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index 4b6d68ab..a6e7f372 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -8,7 +8,7 @@ namespace JKingWeb\Arsse\TestCase\Db; use JKingWeb\Arsse\Db\Statement; use JKingWeb\Arsse\Db\Result; - +use JKingWeb\Arsse\Test\DatabaseInformation; abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { protected $drv; @@ -22,12 +22,13 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { ]; public function setUp() { - $this->setConf($this->conf); - $this->interface = $this->getDbInterface($this->implementation); + self::setConf($this->conf); + $info = new DatabaseInformation($this->implementation); + $this->interface = ($info->interfaceConstructor)(); if (!$this->interface) { $this->markTestSkipped("$this->implementation database driver not available"); } - $this->drv = $this->getDbDriver($this->implementation); + $this->drv = new $info->driverClass; $this->exec("DROP TABLE IF EXISTS arsse_test"); $this->exec("DROP TABLE IF EXISTS arsse_meta"); $this->exec("CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)"); diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php new file mode 100644 index 00000000..a4708dde --- /dev/null +++ b/tests/cases/Db/BaseResult.php @@ -0,0 +1,112 @@ +implementation); + $this->interface = ($info->interfaceConstructor)(); + if (!$this->interface) { + $this->markTestSkipped("$this->implementation database driver not available"); + } + $this->resultClass = $info->resultClass; + $this->stringOutput = $info->stringOutput; + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + } + + public function tearDown() { + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + } + + public function testConstructResult() { + $this->assertInstanceOf(Result::class, new $this->resultClass(...$this->makeResult("SELECT 1"))); + } + + public function testGetChangeCountAndLastInsertId() { + $this->makeResult("CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)"); + $out = $this->makeResult("INSERT INTO arsse_meta(key,value) values('test', 1)"); + $rows = $out[1][0]; + $id = $out[1][1]; + $r = new $this->resultClass(...$out); + $this->assertSame((int) $rows, $r->changes()); + $this->assertSame((int) $id, $r->lastId()); + } + + public function testIterateOverResults() { + $exp = [0 => 1, 1 => 2, 2 => 3]; + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + foreach (new $this->resultClass(...$this->makeResult("SELECT 1 as col union select 2 as col union select 3 as col")) as $index => $row) { + $rows[$index] = $row['col']; + } + $this->assertSame($exp, $rows); + } + + public function testIterateOverResultsTwice() { + $exp = [0 => 1, 1 => 2, 2 => 3]; + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + $result = new $this->resultClass(...$this->makeResult("SELECT 1 as col union select 2 as col union select 3 as col")); + foreach ($result as $index => $row) { + $rows[$index] = $row['col']; + } + $this->assertSame($exp, $rows); + $this->assertException("resultReused", "Db"); + foreach ($result as $row) { + $rows[] = $row['col']; + } + } + + public function testGetSingleValues() { + $exp = [1867, 1970, 2112]; + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + $test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year union select 1970 as year union select 2112 as year")); + $this->assertSame($exp[0], $test->getValue()); + $this->assertSame($exp[1], $test->getValue()); + $this->assertSame($exp[2], $test->getValue()); + $this->assertSame(null, $test->getValue()); + } + + public function testGetFirstValuesOnly() { + $exp = [1867, 1970, 2112]; + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + $test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year, 19 as century union select 1970 as year, 20 as century union select 2112 as year, 22 as century")); + $this->assertSame($exp[0], $test->getValue()); + $this->assertSame($exp[1], $test->getValue()); + $this->assertSame($exp[2], $test->getValue()); + $this->assertSame(null, $test->getValue()); + } + + public function testGetRows() { + $exp = [ + ['album' => '2112', 'track' => '2112'], + ['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], + ]; + $test = new $this->resultClass(...$this->makeResult("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track")); + $this->assertSame($exp[0], $test->getRow()); + $this->assertSame($exp[1], $test->getRow()); + $this->assertSame(null, $test->getRow()); + } + + public function testGetAllRows() { + $exp = [ + ['album' => '2112', 'track' => '2112'], + ['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], + ]; + $test = new $this->resultClass(...$this->makeResult("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track")); + $this->assertEquals($exp, $test->getAll()); + } +} diff --git a/tests/cases/Db/PostgreSQL/TestCreation.php b/tests/cases/Db/PostgreSQL/TestCreation.php index 1190736c..fdc473b1 100644 --- a/tests/cases/Db/PostgreSQL/TestCreation.php +++ b/tests/cases/Db/PostgreSQL/TestCreation.php @@ -14,7 +14,7 @@ use JKingWeb\Arsse\Db\PostgreSQL\Driver; class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideConnectionStrings */ public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { - $this->setConf(); + self::setConf(); $timeout = (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0); $postfix = "application_name='arsse' client_encoding='UTF8' connect_timeout='$timeout'"; $act = Driver::makeConnectionString($pdo, $user, $pass, $db, $host, $port, $service); diff --git a/tests/cases/Db/SQLite3/TestCreation.php b/tests/cases/Db/SQLite3/TestCreation.php index 86ba40be..124c76a0 100644 --- a/tests/cases/Db/SQLite3/TestCreation.php +++ b/tests/cases/Db/SQLite3/TestCreation.php @@ -107,7 +107,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { chmod($path."Awal/arsse.db-wal", 0111); chmod($path."Ashm/arsse.db-shm", 0111); // set up configuration - $this->setConf(); + self::setConf(); } public function tearDown() { diff --git a/tests/cases/Db/SQLite3/TestDriver.php b/tests/cases/Db/SQLite3/TestDriver.php index 3316a01a..58ef4bfd 100644 --- a/tests/cases/Db/SQLite3/TestDriver.php +++ b/tests/cases/Db/SQLite3/TestDriver.php @@ -49,7 +49,7 @@ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { public function provideDrivers() { $this->clearData(); - $this->setConf([ + self::setConf([ 'dbTimeoutExec' => 0.5, 'dbSQLite3Timeout' => 0, 'dbSQLite3File' => tempnam(sys_get_temp_dir(), 'ook'), diff --git a/tests/cases/Db/SQLite3/TestResult.php b/tests/cases/Db/SQLite3/TestResult.php new file mode 100644 index 00000000..2f655e7e --- /dev/null +++ b/tests/cases/Db/SQLite3/TestResult.php @@ -0,0 +1,33 @@ + + */ +class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { + protected $implementation = "SQLite 3"; + + public function tearDown() { + parent::tearDown(); + $this->interface->close(); + unset($this->interface); + } + + protected function exec(string $q) { + $this->interface->exec($q); + } + + protected function makeResult(string $q): array { + $set = $this->interface->query($q); + $rows = $this->interface->changes(); + $id = $this->interface->lastInsertRowID(); + return [$set, [$rows, $id]]; + } +} diff --git a/tests/cases/Db/SQLite3/TestUpdate.php b/tests/cases/Db/SQLite3/TestUpdate.php index 7347eca3..a2a70b0b 100644 --- a/tests/cases/Db/SQLite3/TestUpdate.php +++ b/tests/cases/Db/SQLite3/TestUpdate.php @@ -31,7 +31,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { } $this->clearData(); $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); - $this->setConf($conf); + self::setConf($conf); $this->base = $this->vfs->url(); $this->path = $this->base."/SQLite3/"; $this->drv = new Driver(); diff --git a/tests/cases/Db/SQLite3PDO/TestCreation.php b/tests/cases/Db/SQLite3PDO/TestCreation.php index bec51619..acae72b1 100644 --- a/tests/cases/Db/SQLite3PDO/TestCreation.php +++ b/tests/cases/Db/SQLite3PDO/TestCreation.php @@ -108,7 +108,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { chmod($path."Awal/arsse.db-wal", 0111); chmod($path."Ashm/arsse.db-shm", 0111); // set up configuration - $this->setConf(); + self::setConf(); } public function tearDown() { diff --git a/tests/cases/Db/SQLite3PDO/TestUpdate.php b/tests/cases/Db/SQLite3PDO/TestUpdate.php index d58f971c..409de79c 100644 --- a/tests/cases/Db/SQLite3PDO/TestUpdate.php +++ b/tests/cases/Db/SQLite3PDO/TestUpdate.php @@ -32,7 +32,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { $this->clearData(); $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); $conf['dbDriver'] = PDODriver::class; - $this->setConf($conf); + self::setConf($conf); $this->base = $this->vfs->url(); $this->path = $this->base."/SQLite3/"; $this->drv = new PDODriver(); diff --git a/tests/cases/Db/TestResult.php b/tests/cases/Db/TestResult.php deleted file mode 100644 index d0074bcc..00000000 --- a/tests/cases/Db/TestResult.php +++ /dev/null @@ -1,155 +0,0 @@ - - * @covers \JKingWeb\Arsse\Db\SQLite3\Result - */ -class TestResult extends \JKingWeb\Arsse\Test\AbstractTest { - public function provideResults() { - $this->setConf(); - $interfaces = $this->provideDbInterfaces(); - $constructors = [ - 'SQLite 3' => function(string $query) use($interfaces) { - $drv = $interfaces['SQLite 3']['interface']; - $set = $drv->query($query); - $rows = $drv->changes(); - $id = $drv->lastInsertRowID(); - return [$set, [$rows, $id]]; - }, - ]; - foreach ($constructors as $drv => $func) { - yield $drv => [isset($interfaces[$drv]['interface']), $interfaces[$drv]['stringOutput'], $interfaces[$drv]['result'], $func]; - } - // there is only one PDO result implementation, so we test the first implementation we find - $pdo = array_reduce($interfaces, function ($carry, $item) { - return $carry ?? ($item['interface'] instanceof \PDO ? $item : null); - }) ?? $interfaces['PDO SQLite 3']; - yield "PDO" => [isset($pdo['interface']), $pdo['stringOutput'], $pdo['result'], function(string $query) use($pdo) { - $drv = $pdo['interface']; - $set = $drv->query($query); - $rows = $set->rowCount(); - $id = $drv->lastInsertID(); - return [$set, [$rows, $id]]; - }]; - } - - /** @dataProvider provideResults */ - public function testConstructResult(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $this->assertInstanceOf(Result::class, new $class(...$func("SELECT 1"))); - } - - /** @dataProvider provideResults */ - public function testGetChangeCountAndLastInsertId(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $func("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)"); - $out = $func("INSERT INTO arsse_meta(key,value) values('test', 1)"); - $rows = $out[1][0]; - $id = $out[1][1]; - $r = new $class(...$out); - $this->assertSame((int) $rows, $r->changes()); - $this->assertSame((int) $id, $r->lastId()); - } - - /** @dataProvider provideResults */ - public function testIterateOverResults(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $exp = [0 => 1, 1 => 2, 2 => 3]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - foreach (new $class(...$func("SELECT 1 as col union select 2 as col union select 3 as col")) as $index => $row) { - $rows[$index] = $row['col']; - } - $this->assertSame($exp, $rows); - } - - /** @dataProvider provideResults */ - public function testIterateOverResultsTwice(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $exp = [0 => 1, 1 => 2, 2 => 3]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - $result = new $class(...$func("SELECT 1 as col union select 2 as col union select 3 as col")); - foreach ($result as $index => $row) { - $rows[$index] = $row['col']; - } - $this->assertSame($exp, $rows); - $this->assertException("resultReused", "Db"); - foreach ($result as $row) { - $rows[] = $row['col']; - } - } - - /** @dataProvider provideResults */ - public function testGetSingleValues(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $exp = [1867, 1970, 2112]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - $test = new $class(...$func("SELECT 1867 as year union select 1970 as year union select 2112 as year")); - $this->assertSame($exp[0], $test->getValue()); - $this->assertSame($exp[1], $test->getValue()); - $this->assertSame($exp[2], $test->getValue()); - $this->assertSame(null, $test->getValue()); - } - - /** @dataProvider provideResults */ - public function testGetFirstValuesOnly(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $exp = [1867, 1970, 2112]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - $test = new $class(...$func("SELECT 1867 as year, 19 as century union select 1970 as year, 20 as century union select 2112 as year, 22 as century")); - $this->assertSame($exp[0], $test->getValue()); - $this->assertSame($exp[1], $test->getValue()); - $this->assertSame($exp[2], $test->getValue()); - $this->assertSame(null, $test->getValue()); - } - - /** @dataProvider provideResults */ - public function testGetRows(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $exp = [ - ['album' => '2112', 'track' => '2112'], - ['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], - ]; - $test = new $class(...$func("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track")); - $this->assertSame($exp[0], $test->getRow()); - $this->assertSame($exp[1], $test->getRow()); - $this->assertSame(null, $test->getRow()); - } - - /** @dataProvider provideResults */ - public function testGetAllRows(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } - $exp = [ - ['album' => '2112', 'track' => '2112'], - ['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], - ]; - $test = new $class(...$func("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track")); - $this->assertEquals($exp, $test->getAll()); - } -} diff --git a/tests/cases/Db/TestResultPDO.php b/tests/cases/Db/TestResultPDO.php new file mode 100644 index 00000000..f4d2eec7 --- /dev/null +++ b/tests/cases/Db/TestResultPDO.php @@ -0,0 +1,52 @@ + + */ +class TestResultPDO extends \JKingWeb\Arsse\TestCase\Db\BaseResult { + protected static $firstAvailableDriver; + + public static function setUpBeforeClass() { + self::setConf(); + // we only need to test one PDO implementation (they all use the same result class), so we find the first usable one + $drivers = DatabaseInformation::listPDO(); + self::$firstAvailableDriver = $drivers[0]; + foreach ($drivers as $driver) { + $info = new DatabaseInformation($driver); + $interface = ($info->interfaceConstructor)(); + if ($interface) { + self::$firstAvailableDriver = $driver; + break; + } + } + } + + public function setUp() { + $this->implementation = self::$firstAvailableDriver; + parent::setUp(); + } + + public function tearDown() { + parent::tearDown(); + unset($this->interface); + } + + protected function exec(string $q) { + $this->interface->exec($q); + } + + protected function makeResult(string $q): array { + $set = $this->interface->query($q); + $rows = $set->rowCount(); + $id = $this->interface->lastInsertID(); + return [$set, [$rows, $id]]; + } +} diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/TestStatement.php index 838ef678..d6a3cb81 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/TestStatement.php @@ -38,18 +38,14 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideStatements */ - public function testConstructStatement(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testConstructStatement() { + $class = $this->statementClass; $this->assertInstanceOf(Statement::class, new $class(...$func("SELECT ? as value"))); } /** @dataProvider provideBindings */ public function testBindATypedValue(bool $driverTestable, string $class, \Closure $func, $value, string $type, string $exp) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + $class = $this->statementClass; if ($exp=="null") { $query = "SELECT (cast(? as text) is null) as pass"; } else { @@ -63,10 +59,8 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideBinaryBindings */ - public function testHandleBinaryData(bool $driverTestable, string $class, \Closure $func, $value, string $type, string $exp) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testHandleBinaryData($value, string $type, string $exp) { + $class = $this->statementClass; if ($exp=="null") { $query = "SELECT (cast(? as text) is null) as pass"; } else { @@ -80,20 +74,16 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideStatements */ - public function testBindMissingValue(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testBindMissingValue() { + $class = $this->statementClass; $s = new $class(...$func("SELECT ? as value", ["int"])); $val = $s->runArray()->getRow()['value']; $this->assertSame(null, $val); } /** @dataProvider provideStatements */ - public function testBindMultipleValues(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testBindMultipleValues() { + $class = $this->statementClass; $exp = [ 'one' => 1, 'two' => 2, @@ -105,10 +95,8 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideStatements */ - public function testBindRecursively(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testBindRecursively() { + $class = $this->statementClass; $exp = [ 'one' => 1, 'two' => 2, @@ -122,20 +110,16 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideStatements */ - public function testBindWithoutType(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testBindWithoutType() { + $class = $this->statementClass; $this->assertException("paramTypeMissing", "Db"); $s = new $class(...$func("SELECT ? as value", [])); $s->runArray([1]); } /** @dataProvider provideStatements */ - public function testViolateConstraint(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testViolateConstraint() { + $class = $this->statementClass; (new $class(...$func("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run(); $s = new $class(...$func("INSERT INTO arsse_meta(key) values(?)", ["str"])); $this->assertException("constraintViolation", "Db", "ExceptionInput"); @@ -143,10 +127,8 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { } /** @dataProvider provideStatements */ - public function testMismatchTypes(bool $driverTestable, bool $stringCoersion, string $class, \Closure $func) { - if (!$driverTestable) { - $this->markTestSkipped(); - } + public function testMismatchTypes() { + $class = $this->statementClass; (new $class(...$func("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run(); $s = new $class(...$func("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"])); $this->assertException("typeViolation", "Db", "ExceptionInput"); diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index a1ca84bd..01e9022d 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -96,7 +96,7 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { } $this->base = self::$host."Feed/"; $this->clearData(); - $this->setConf(); + self::setConf(); Arsse::$db = Phake::mock(Database::class); } diff --git a/tests/cases/Feed/TestFetching.php b/tests/cases/Feed/TestFetching.php index 25d4d7c4..64102b9d 100644 --- a/tests/cases/Feed/TestFetching.php +++ b/tests/cases/Feed/TestFetching.php @@ -26,7 +26,7 @@ class TestFetching extends \JKingWeb\Arsse\Test\AbstractTest { } $this->base = self::$host."Feed/"; $this->clearData(); - $this->setConf(); + self::setConf(); } public function testHandle400() { diff --git a/tests/cases/REST/NextCloudNews/TestV1_2.php b/tests/cases/REST/NextCloudNews/TestV1_2.php index 90375b70..22f3ab6a 100644 --- a/tests/cases/REST/NextCloudNews/TestV1_2.php +++ b/tests/cases/REST/NextCloudNews/TestV1_2.php @@ -340,7 +340,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { $this->clearData(); - $this->setConf(); + self::setConf(); // create a mock user manager Arsse::$user = Phake::mock(User::class); Phake::when(Arsse::$user)->auth->thenReturn(true); diff --git a/tests/cases/REST/TestREST.php b/tests/cases/REST/TestREST.php index fcc4965a..19e119b0 100644 --- a/tests/cases/REST/TestREST.php +++ b/tests/cases/REST/TestREST.php @@ -95,7 +95,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest { } public function testSendAuthenticationChallenges() { - $this->setConf(); + self::setConf(); $r = new REST(); $in = new EmptyResponse(401); $exp = $in->withHeader("WWW-Authenticate", 'Basic realm="OOK"'); @@ -151,7 +151,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideCorsNegotiations */ public function testNegotiateCors($origin, bool $exp, string $allowed = null, string $denied = null) { - $this->setConf(); + self::setConf(); $r = Phake::partialMock(REST::class); Phake::when($r)->corsNormalizeOrigin->thenReturnCallback(function ($origin) { return $origin; diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index b6bfb4f2..eb6ead75 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -177,7 +177,7 @@ LONG_STRING; public function setUp() { $this->clearData(); - $this->setConf(); + self::setConf(); // create a mock user manager Arsse::$user = Phake::mock(User::class); Phake::when(Arsse::$user)->auth->thenReturn(true); @@ -225,7 +225,7 @@ LONG_STRING; /** @dataProvider provideLoginRequests */ public function testLogIn(array $conf, $httpUser, array $data, $sessions) { Arsse::$user->id = null; - $this->setConf($conf); + self::setConf($conf); Phake::when(Arsse::$user)->auth->thenReturn(false); Phake::when(Arsse::$user)->auth("john.doe@example.com", "secret")->thenReturn(true); Phake::when(Arsse::$user)->auth("jane.doe@example.com", "superman")->thenReturn(true); @@ -259,7 +259,7 @@ LONG_STRING; /** @dataProvider provideResumeRequests */ public function testValidateASession(array $conf, $httpUser, string $data, $result) { Arsse::$user->id = null; - $this->setConf($conf); + self::setConf($conf); Phake::when(Arsse::$db)->sessionResume("PriestsOfSyrinx")->thenReturn([ 'id' => "PriestsOfSyrinx", 'created' => "2000-01-01 00:00:00", diff --git a/tests/cases/REST/TinyTinyRSS/TestIcon.php b/tests/cases/REST/TinyTinyRSS/TestIcon.php index 53db57c6..e25c6712 100644 --- a/tests/cases/REST/TinyTinyRSS/TestIcon.php +++ b/tests/cases/REST/TinyTinyRSS/TestIcon.php @@ -24,7 +24,7 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { $this->clearData(); - $this->setConf(); + self::setConf(); // create a mock user manager Arsse::$user = Phake::mock(User::class); // create a mock database interface @@ -108,7 +108,7 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertMessage($exp, $this->reqAuthFailed("42.ico")); $this->assertMessage($exp, $this->reqAuthFailed("1337.ico")); // with HTTP auth required, only authenticated requests should succeed - $this->setConf(['userHTTPAuthRequired' => true]); + self::setConf(['userHTTPAuthRequired' => true]); $exp = new Response(301, ['Location' => "http://example.org/icon.gif"]); $this->assertMessage($exp, $this->reqAuth("42.ico")); $this->assertMessage($exp, $this->reqAuth("1337.ico")); diff --git a/tests/cases/Service/TestService.php b/tests/cases/Service/TestService.php index 3ba18e02..69eec5f0 100644 --- a/tests/cases/Service/TestService.php +++ b/tests/cases/Service/TestService.php @@ -19,7 +19,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { $this->clearData(); - $this->setConf(); + self::setConf(); Arsse::$db = Phake::mock(Database::class); $this->srv = new Service(); } diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index 68b597d4..a1f95dea 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -20,7 +20,7 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { $this->clearData(); - $this->setConf(); + self::setConf(); // create a mock database interface Arsse::$db = Phake::mock(Database::class); Phake::when(Arsse::$db)->begin->thenReturn(Phake::mock(\JKingWeb\Arsse\Db\Transaction::class)); diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index 806bfff2..c576245f 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -20,7 +20,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { $this->clearData(); - $this->setConf(); + self::setConf(); // create a mock database interface Arsse::$db = Phake::mock(Database::class); Phake::when(Arsse::$db)->begin->thenReturn(Phake::mock(\JKingWeb\Arsse\Db\Transaction::class)); diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index e4930f04..1b244dbd 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -40,7 +40,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } } - public function setConf(array $conf = []) { + public static function setConf(array $conf = []) { $defaults = [ 'dbSQLite3File' => ":memory:", 'dbSQLite3Timeout' => 0, @@ -126,165 +126,4 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } return $value; } - - public function provideDbDrivers(array $conf = []): array { - $this->setConf($conf); - return [ - 'SQLite 3' => (function() { - try { - return new \JKingWeb\Arsse\Db\SQLite3\Driver; - } catch (\Exception $e) { - return; - } - })(), - 'PDO SQLite 3' => (function() { - try { - return new \JKingWeb\Arsse\Db\SQLite3\PDODriver; - } catch (\Exception $e) { - return; - } - })(), - 'PDO PostgreSQL' => (function() { - try { - return new \JKingWeb\Arsse\Db\PostgreSQL\PDODriver; - } catch (\Exception $e) { - return; - } - })(), - ]; - } - - public function provideDbInterfaces(array $conf = []): array { - $this->setConf($conf); - return [ - 'SQLite 3' => [ - 'interface' => (function() { - if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { - try { - $d = new \SQLite3(Arsse::$conf->dbSQLite3File); - } catch (\Exception $e) { - return; - } - $d->enableExceptions(true); - return $d; - } - })(), - 'statement' => \JKingWeb\Arsse\Db\SQLite3\Statement::class, - 'result' => \JKingWeb\Arsse\Db\SQLite3\Result::class, - 'stringOutput' => false, - ], - 'PDO SQLite 3' => [ - 'interface' => (function() { - if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { - try { - return new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); - } catch (\PDOException $e) { - return; - } - } - })(), - 'statement' => \JKingWeb\Arsse\Db\PDOStatement::class, - 'result' => \JKingWeb\Arsse\Db\PDOResult::class, - 'stringOutput' => true, - ], - 'PDO PostgreSQL' => [ - 'interface' => (function() { - if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) { - $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); - try { - $c = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); - } catch (\PDOException $e) { - return; - } - foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { - $c->exec($q); - } - return $c; - } - })(), - 'statement' => \JKingWeb\Arsse\Db\PDOStatement::class, - 'result' => \JKingWeb\Arsse\Db\PDOResult::class, - 'stringOutput' => true, - ], - ]; - } - - public function getDbDriver(string $name, array $conf = []) { - $this->setConf($conf); - switch ($name) { - case 'SQLite 3': - return (function() { - try { - return new \JKingWeb\Arsse\Db\SQLite3\Driver; - } catch (\Exception $e) { - return; - } - })(); - case 'PDO SQLite 3': - return (function() { - try { - return new \JKingWeb\Arsse\Db\SQLite3\PDODriver; - } catch (\Exception $e) { - return; - } - })(); - case 'PDO PostgreSQL': - return (function() { - try { - return new \JKingWeb\Arsse\Db\PostgreSQL\PDODriver; - } catch (\Exception $e) { - return; - } - })(); - default: - throw new \Exception("Invalid database driver name"); - } - } - - public function getDbInterface(string $name, array $conf = []) { - $this->setConf($conf); - switch ($name) { - case 'SQLite 3': - return (function() { - if (\JKingWeb\Arsse\Db\SQLite3\Driver::requirementsMet()) { - try { - $d = new \SQLite3(Arsse::$conf->dbSQLite3File); - } catch (\Exception $e) { - return; - } - $d->enableExceptions(true); - return $d; - } - })(); - case 'PDO SQLite 3': - return (function() { - if (\JKingWeb\Arsse\Db\SQLite3\PDODriver::requirementsMet()) { - try { - $d = new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); - $d->exec("PRAGMA busy_timeout=0"); - return $d; - } catch (\PDOException $e) { - return; - } - } - })(); - case 'PDO PostgreSQL': - return (function() { - if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) { - $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); - try { - $c = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); - } catch (\PDOException $e) { - return; - } - foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { - $c->exec($q); - } - return $c; - } - })(); - default: - throw new \Exception("Invalid database driver name"); - } - } } diff --git a/tests/lib/Database/Setup.php b/tests/lib/Database/Setup.php index 0a98edf0..88a01d11 100644 --- a/tests/lib/Database/Setup.php +++ b/tests/lib/Database/Setup.php @@ -22,7 +22,7 @@ trait Setup { public function setUp() { // establish a clean baseline $this->clearData(); - $this->setConf(); + self::setConf(); // configure and create the relevant database driver $this->setUpDriver(); // create the database interface with the suitable driver diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php new file mode 100644 index 00000000..d22a6185 --- /dev/null +++ b/tests/lib/DatabaseInformation.php @@ -0,0 +1,111 @@ +name = $name; + foreach (self::$data[$name] as $key => $value) { + $this->$key = $value; + } + } + + public static function list(): array { + if (!isset(self::$data)) { + self::$data = self::getData(); + } + return array_keys(self::$data); + } + + public static function listPDO(): array { + if (!isset(self::$data)) { + self::$data = self::getData(); + } + return array_values(array_filter(array_keys(self::$data), function($k) { + return self::$data[$k]['pdo']; + })); + } + + protected static function getData() { + return [ + 'SQLite 3' => [ + 'pdo' => false, + 'backend' => "SQLite 3", + 'statementClass' => \JKingWeb\Arsse\Db\SQLite3\Statement::class, + 'resultClass' => \JKingWeb\Arsse\Db\SQLite3\Result::class, + 'driverClass' => \JKingWeb\Arsse\Db\SQLite3\Driver::class, + 'stringOutput' => false, + 'interfaceConstructor' => function() { + try { + $d = new \SQLite3(Arsse::$conf->dbSQLite3File); + } catch (\Throwable $e) { + return; + } + $d->enableExceptions(true); + return $d; + }, + + ], + 'PDO SQLite 3' => [ + 'pdo' => true, + 'backend' => "SQLite 3", + 'statementClass' => \JKingWeb\Arsse\Db\PDOStatement::class, + 'resultClass' => \JKingWeb\Arsse\Db\PDOResult::class, + 'driverClass' => \JKingWeb\Arsse\Db\SQLite3\PDODriver::class, + 'stringOutput' => true, + 'interfaceConstructor' => function() { + try { + $d = new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $d->exec("PRAGMA busy_timeout=0"); + return $d; + } catch (\Throwable $e) { + return; + } + }, + ], + 'PDO PostgreSQL' => [ + 'pdo' => true, + 'backend' => "PostgreSQL", + 'statementClass' => \JKingWeb\Arsse\Db\PDOStatement::class, + 'resultClass' => \JKingWeb\Arsse\Db\PDOResult::class, + 'driverClass' => \JKingWeb\Arsse\Db\PostgreSQL\PDODriver::class, + 'stringOutput' => true, + 'interfaceConstructor' => function() { + $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); + try { + $d = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + } catch (\Throwable $e) { + return; + } + foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { + $d->exec($q); + } + return $d; + }, + ], + ]; + } +} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index faf634e7..e863737d 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -45,19 +45,23 @@ cases/Db/TestTransaction.php cases/Db/TestResultAggregate.php cases/Db/TestResultEmpty.php - cases/Db/TestResult.php - cases/Db/TestStatement.php + cases/Db/TestResultPDO.php + cases/Db/SQLite3/TestResult.php + cases/Db/SQLite3/TestStatement.php cases/Db/SQLite3/TestCreation.php cases/Db/SQLite3/TestDriver.php cases/Db/SQLite3/TestUpdate.php + cases/Db/SQLite3PDO/TestStatement.php cases/Db/SQLite3PDO/TestCreation.php cases/Db/SQLite3PDO/TestDriver.php cases/Db/SQLite3PDO/TestUpdate.php + cases/Db/PostgreSQL/TestStatement.php cases/Db/PostgreSQL/TestCreation.php - cases/Db/PostgreSQL/TestDriver.php + + cases/Db/SQLite3/Database/TestMiscellany.php From 8c20411359e2ac4d5c96fac2db9b8b3f2a1fea10 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 22 Nov 2018 23:18:20 -0500 Subject: [PATCH 15/58] Align statement tests with other database driver tests --- tests/cases/Db/BaseDriver.php | 2 + tests/cases/Db/BaseResult.php | 2 + .../{TestStatement.php => BaseStatement.php} | 168 ++++++------------ tests/cases/Db/PostgreSQL/TestDriver.php | 23 +++ tests/cases/Db/PostgreSQL/TestStatement.php | 42 +++++ tests/cases/Db/SQLite3/TestStatement.php | 32 ++++ tests/cases/Db/SQLite3PDO/TestStatement.php | 35 ++++ tests/lib/AbstractTest.php | 4 +- 8 files changed, 194 insertions(+), 114 deletions(-) rename tests/cases/Db/{TestStatement.php => BaseStatement.php} (74%) create mode 100644 tests/cases/Db/PostgreSQL/TestDriver.php create mode 100644 tests/cases/Db/PostgreSQL/TestStatement.php create mode 100644 tests/cases/Db/SQLite3/TestStatement.php create mode 100644 tests/cases/Db/SQLite3PDO/TestStatement.php diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index a6e7f372..653da327 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -22,6 +22,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { ]; public function setUp() { + $this->clearData(); self::setConf($this->conf); $info = new DatabaseInformation($this->implementation); $this->interface = ($info->interfaceConstructor)(); @@ -36,6 +37,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { + $this->clearData(); unset($this->drv); try { $this->exec("ROLLBACK"); diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index a4708dde..8a79d747 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -18,6 +18,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { abstract protected function makeResult(string $q): array; public function setUp() { + $this->clearData(); self::setConf(); $info = new DatabaseInformation($this->implementation); $this->interface = ($info->interfaceConstructor)(); @@ -30,6 +31,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { + $this->clearData(); $this->exec("DROP TABLE IF EXISTS arsse_meta"); } diff --git a/tests/cases/Db/TestStatement.php b/tests/cases/Db/BaseStatement.php similarity index 74% rename from tests/cases/Db/TestStatement.php rename to tests/cases/Db/BaseStatement.php index d6a3cb81..86705446 100644 --- a/tests/cases/Db/TestStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -6,53 +6,49 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db; -use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Db\Statement; -use JKingWeb\Arsse\Db\PDOStatement; +use JKingWeb\Arsse\Test\DatabaseInformation; -/** - * @covers \JKingWeb\Arsse\Db\SQLite3\Statement - * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder - * @covers \JKingWeb\Arsse\Db\PDOStatement - * @covers \JKingWeb\Arsse\Db\PDOError */ -class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { - public function provideStatements() { - $interfaces = $this->provideDbInterfaces(); - $constructors = [ - 'SQLite 3' => function(string $query, array $types = []) use($interfaces) { - $s = $interfaces['SQLite 3']['interface']->prepare($query); - return [$interfaces['SQLite 3']['interface'], $s, $types]; - }, - 'PDO SQLite 3' => function(string $query, array $types = []) use($interfaces) { - $s = $interfaces['PDO SQLite 3']['interface']->prepare($query); - return [$interfaces['PDO SQLite 3']['interface'], $s, $types]; - }, - 'PDO PostgreSQL' => function(string $query, array $types = []) use($interfaces) { - $s = $interfaces['PDO PostgreSQL']['interface']->prepare($query); - return [$interfaces['PDO PostgreSQL']['interface'], $s, $types]; - }, - ]; - foreach ($constructors as $drv => $func) { - yield $drv => [isset($interfaces[$drv]['interface']), $interfaces[$drv]['stringOutput'], $interfaces[$drv]['statement'], $func]; +abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { + protected $statementClass; + protected $stringOutput; + protected $interface; + + abstract protected function exec(string $q); + abstract protected function makeStatement(string $q, array $types = []): array; + abstract protected function decorateTypeSyntax(string $value, string $type): string; + + public function setUp() { + $this->clearData(); + self::setConf(); + $info = new DatabaseInformation($this->implementation); + $this->interface = ($info->interfaceConstructor)(); + if (!$this->interface) { + $this->markTestSkipped("$this->implementation database driver not available"); } + $this->statementClass = $info->statementClass; + $this->stringOutput = $info->stringOutput; + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + } + + public function tearDown() { + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + $this->clearData(); } - /** @dataProvider provideStatements */ public function testConstructStatement() { - $class = $this->statementClass; - $this->assertInstanceOf(Statement::class, new $class(...$func("SELECT ? as value"))); + $this->assertInstanceOf(Statement::class, new $this->statementClass(...$this->makeStatement("SELECT ? as value"))); } /** @dataProvider provideBindings */ - public function testBindATypedValue(bool $driverTestable, string $class, \Closure $func, $value, string $type, string $exp) { - $class = $this->statementClass; + public function testBindATypedValue($value, string $type, string $exp) { if ($exp=="null") { $query = "SELECT (cast(? as text) is null) as pass"; } else { $query = "SELECT ($exp = ?) as pass"; } $typeStr = "'".str_replace("'", "''", $type)."'"; - $s = new $class(...$func($query)); + $s = new $this->statementClass(...$this->makeStatement($query)); $s->retype(...[$type]); $act = $s->run(...[$value])->getValue(); $this->assertTrue((bool) $act); @@ -60,77 +56,67 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideBinaryBindings */ public function testHandleBinaryData($value, string $type, string $exp) { - $class = $this->statementClass; + if (in_array($this->implementation, ["PostgreSQL", "PDO PostgreSQL"])) { + $this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown"); + } if ($exp=="null") { $query = "SELECT (cast(? as text) is null) as pass"; } else { $query = "SELECT ($exp = ?) as pass"; } $typeStr = "'".str_replace("'", "''", $type)."'"; - $s = new $class(...$func($query)); + $s = new $this->statementClass(...$this->makeStatement($query)); $s->retype(...[$type]); $act = $s->run(...[$value])->getValue(); $this->assertTrue((bool) $act); } - /** @dataProvider provideStatements */ public function testBindMissingValue() { - $class = $this->statementClass; - $s = new $class(...$func("SELECT ? as value", ["int"])); + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", ["int"])); $val = $s->runArray()->getRow()['value']; $this->assertSame(null, $val); } - /** @dataProvider provideStatements */ public function testBindMultipleValues() { - $class = $this->statementClass; $exp = [ 'one' => 1, 'two' => 2, ]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - $s = new $class(...$func("SELECT ? as one, ? as two", ["int", "int"])); + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two", ["int", "int"])); $val = $s->runArray([1,2])->getRow(); $this->assertSame($exp, $val); } - /** @dataProvider provideStatements */ public function testBindRecursively() { - $class = $this->statementClass; $exp = [ 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4, ]; - $exp = $stringCoersion ? $this->stringify($exp) : $exp; - $s = new $class(...$func("SELECT ? as one, ? as two, ? as three, ? as four", ["int", ["int", "int"], "int"])); + $exp = $this->stringOutput ? $this->stringify($exp) : $exp; + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two, ? as three, ? as four", ["int", ["int", "int"], "int"])); $val = $s->runArray([1, [2, 3], 4])->getRow(); $this->assertSame($exp, $val); } - /** @dataProvider provideStatements */ public function testBindWithoutType() { - $class = $this->statementClass; $this->assertException("paramTypeMissing", "Db"); - $s = new $class(...$func("SELECT ? as value", [])); + $s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", [])); $s->runArray([1]); } - /** @dataProvider provideStatements */ public function testViolateConstraint() { - $class = $this->statementClass; - (new $class(...$func("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run(); - $s = new $class(...$func("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]); } - /** @dataProvider provideStatements */ public function testMismatchTypes() { - $class = $this->statementClass; - (new $class(...$func("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run(); - $s = new $class(...$func("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"])); + (new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run(); + $s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"])); $this->assertException("typeViolation", "Db", "ExceptionInput"); $s->runArray(['ook', 'eek']); } @@ -250,35 +236,32 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"], 'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], 'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"], - 'DateTime as integer' => [$dateMutable, "integer", $dateUTC->getTimestamp()], + 'DateTime as integer' => [$dateMutable, "integer", (string) $dateUTC->getTimestamp()], 'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"], 'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as boolean' => [$dateMutable, "boolean", "1"], - 'DateTime as strict integer' => [$dateMutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTime as strict integer' => [$dateMutable, "strict integer", (string) $dateUTC->getTimestamp()], 'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"], 'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"], - 'DateTimeImmutable as integer' => [$dateImmutable, "integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as integer' => [$dateImmutable, "integer", (string) $dateUTC->getTimestamp()], 'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"], 'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"], - 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", $dateUTC->getTimestamp()], + 'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", (string) $dateUTC->getTimestamp()], 'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"], 'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], 'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], ]; - $decorators = $this->provideSyntaxDecorators(); - foreach ($this->provideStatements() as $drvName => list($drv, $stringCoersion, $class, $func)) { - $conv = $decorators[$drvName] ?? $conv = $decorators['']; - foreach ($tests as $index => list($value, $type, $exp)) { - $t = preg_replace("<^strict >", "", $type); - $exp = ($exp=="null") ? $exp : $conv($exp, $t); - yield "$index ($drvName)" => [$drv, $class, $func, $value, $type, $exp]; - } + foreach ($tests as $index => list($value, $type, $exp)) { + $t = preg_replace("<^strict >", "", $type); + if (gettype($exp) != "string") var_export($index); + $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); + yield $index => [$value, $type, $exp]; } } @@ -326,50 +309,11 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest { 'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], 'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], ]; - $decorators = $this->provideSyntaxDecorators(); - foreach ($this->provideStatements() as $drvName => list($drv, $stringCoersion, $class, $func)) { - $conv = $decorators[$drvName] ?? $conv = $decorators['']; - if ($drvName=="PDO PostgreSQL") { - // skip PostgreSQL for these tests - $drv = false; - } - foreach ($tests as $index => list($value, $type, $exp)) { - $t = preg_replace("<^strict >", "", $type); - $exp = ($exp=="null") ? $exp : $conv($exp, $t); - yield "$index ($drvName)" => [$drv, $class, $func, $value, $type, $exp]; - } + foreach ($tests as $index => list($value, $type, $exp)) { + $t = preg_replace("<^strict >", "", $type); + if (gettype($exp) != "string") var_export($index); + $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); + yield $index => [$value, $type, $exp]; } } - - function provideSyntaxDecorators() { - return [ - 'PDO PostgreSQL' => (function($v, $t) { - switch ($t) { - case "float": - return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; - case "string": - if (preg_match("<^char\((\d+)\)$>", $v, $match)) { - return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; - } else { - return $v; - } - default: - return $v; - } - }), - 'PDO SQLite 3' => (function($v, $t) { - if ($t=="float") { - return (substr($v, -2)==".0") ? "'".substr($v, 0, strlen($v) - 2)."'" : "'$v'"; - } else { - return $v; - } - }), - 'SQLite 3' => (function($v, $t) { - return $v; - }), - '' => (function($v, $t) { - return $v; - }), - ]; - } } diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php new file mode 100644 index 00000000..497488f7 --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -0,0 +1,23 @@ + + * @covers \JKingWeb\Arsse\Db\PDODriver + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { + protected $implementation = "PDO PostgreSQL"; + protected $create = "CREATE TABLE arsse_test(id bigserial primary key)"; + protected $lock = "BEGIN; LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT"; + protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'"; + + public function tearDown() { + parent::tearDown(); + unset($this->interface); + } +} diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php new file mode 100644 index 00000000..2c8586fc --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -0,0 +1,42 @@ + + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { + protected $implementation = "PDO PostgreSQL"; + + public function tearDown() { + parent::tearDown(); + unset($this->interface); + } + + protected function exec(string $q) { + $this->interface->exec($q); + } + + protected function makeStatement(string $q, array $types = []): array { + return [$this->interface, $this->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 "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; + } else { + return $value; + } + default: + return $value; + } + } +} diff --git a/tests/cases/Db/SQLite3/TestStatement.php b/tests/cases/Db/SQLite3/TestStatement.php new file mode 100644 index 00000000..fc06dbd0 --- /dev/null +++ b/tests/cases/Db/SQLite3/TestStatement.php @@ -0,0 +1,32 @@ + + * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ +class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { + protected $implementation = "SQLite 3"; + + public function tearDown() { + parent::tearDown(); + $this->interface->close(); + unset($this->interface); + } + + protected function exec(string $q) { + $this->interface->exec($q); + } + + protected function makeStatement(string $q, array $types = []): array { + return [$this->interface, $this->interface->prepare($q), $types]; + } + + protected function decorateTypeSyntax(string $value, string $type): string { + return $value; + } +} diff --git a/tests/cases/Db/SQLite3PDO/TestStatement.php b/tests/cases/Db/SQLite3PDO/TestStatement.php new file mode 100644 index 00000000..74f05f19 --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestStatement.php @@ -0,0 +1,35 @@ + + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { + protected $implementation = "PDO SQLite 3"; + + public function tearDown() { + parent::tearDown(); + unset($this->interface); + } + + protected function exec(string $q) { + $this->interface->exec($q); + } + + protected function makeStatement(string $q, array $types = []): array { + return [$this->interface, $this->interface->prepare($q), $types]; + } + + protected function decorateTypeSyntax(string $value, string $type): string { + if ($type=="float") { + return (substr($value, -2)==".0") ? "'".substr($value, 0, strlen($value) - 2)."'" : "'$value'"; + } else { + return $value; + } + } +} diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 1b244dbd..a15d6748 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -40,7 +40,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } } - public static function setConf(array $conf = []) { + public static function setConf(array $conf = [], bool $force = true) { $defaults = [ 'dbSQLite3File' => ":memory:", 'dbSQLite3Timeout' => 0, @@ -48,7 +48,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { 'dbPostgreSQLPass' => "arsse_test", 'dbPostgreSQLDb' => "arsse_test", ]; - Arsse::$conf = Arsse::$conf ?? (new Conf)->import($defaults)->import($conf); + Arsse::$conf = ($force ? null : Arsse::$conf) ?? (new Conf)->import($defaults)->import($conf); } public function assertException(string $msg = "", string $prefix = "", string $type = "Exception") { From 39110858b7ffc7d02d40c4cbdaefd4d04c30dd36 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 23 Nov 2018 09:29:06 -0500 Subject: [PATCH 16/58] Move database function test series as first step in re-organization --- tests/cases/Database/Base.php | 167 ++++++++++++++++++ .../{lib => cases}/Database/SeriesArticle.php | 0 .../{lib => cases}/Database/SeriesCleanup.php | 0 tests/{lib => cases}/Database/SeriesFeed.php | 0 .../{lib => cases}/Database/SeriesFolder.php | 0 tests/{lib => cases}/Database/SeriesLabel.php | 0 tests/{lib => cases}/Database/SeriesMeta.php | 0 .../Database/SeriesMiscellany.php | 0 .../{lib => cases}/Database/SeriesSession.php | 0 .../Database/SeriesSubscription.php | 0 tests/{lib => cases}/Database/SeriesUser.php | 0 tests/phpunit.xml | 24 +-- 12 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 tests/cases/Database/Base.php rename tests/{lib => cases}/Database/SeriesArticle.php (100%) rename tests/{lib => cases}/Database/SeriesCleanup.php (100%) rename tests/{lib => cases}/Database/SeriesFeed.php (100%) rename tests/{lib => cases}/Database/SeriesFolder.php (100%) rename tests/{lib => cases}/Database/SeriesLabel.php (100%) rename tests/{lib => cases}/Database/SeriesMeta.php (100%) rename tests/{lib => cases}/Database/SeriesMiscellany.php (100%) rename tests/{lib => cases}/Database/SeriesSession.php (100%) rename tests/{lib => cases}/Database/SeriesSubscription.php (100%) rename tests/{lib => cases}/Database/SeriesUser.php (100%) diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php new file mode 100644 index 00000000..711c3ccd --- /dev/null +++ b/tests/cases/Database/Base.php @@ -0,0 +1,167 @@ +clearData(); + self::setConf(); + // configure and create the relevant database driver + $this->setUpDriver(); + // create the database interface with the suitable driver + Arsse::$db = new Database($this->drv); + Arsse::$db->driverSchemaUpdate(); + // create a mock user manager + Arsse::$user = Phake::mock(User::class); + Phake::when(Arsse::$user)->authorize->thenReturn(true); + // call the additional setup method if it exists + if (method_exists($this, "setUpSeries")) { + $this->setUpSeries(); + } + // prime the database with series data if it hasn't already been done + if (!$this->primed && isset($this->data)) { + $this->primeDatabase($this->data); + } + } + + public function tearDown() { + // call the additional teardiwn method if it exists + if (method_exists($this, "tearDownSeries")) { + $this->tearDownSeries(); + } + // clean up + $this->primed = false; + $this->drv = null; + $this->clearData(); + } + + public function primeDatabase(array $data, \JKingWeb\Arsse\Db\Driver $drv = null): bool { + $drv = $drv ?? $this->drv; + $tr = $drv->begin(); + foreach ($data as $table => $info) { + $cols = implode(",", array_keys($info['columns'])); + $bindings = array_values($info['columns']); + $params = implode(",", array_fill(0, sizeof($info['columns']), "?")); + $s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings); + foreach ($info['rows'] as $row) { + $s->runArray($row); + } + } + $tr->commit(); + $this->primed = true; + return true; + } + + public function compareExpectations(array $expected): bool { + foreach ($expected as $table => $info) { + $cols = implode(",", array_keys($info['columns'])); + $types = $info['columns']; + $data = $this->drv->prepare("SELECT $cols from $table")->run()->getAll(); + $cols = array_keys($info['columns']); + foreach ($info['rows'] as $index => $row) { + $this->assertCount(sizeof($cols), $row, "The number of values for array index $index does not match the number of fields"); + $row = array_combine($cols, $row); + foreach ($data as $index => $test) { + foreach ($test as $col => $value) { + switch ($types[$col]) { + case "datetime": + $test[$col] = $this->approximateTime($row[$col], $value); + break; + case "int": + $test[$col] = ValueInfo::normalize($value, ValueInfo::T_INT | ValueInfo::M_DROP | valueInfo::M_NULL); + break; + case "float": + $test[$col] = ValueInfo::normalize($value, ValueInfo::T_FLOAT | ValueInfo::M_DROP | valueInfo::M_NULL); + break; + case "bool": + $test[$col] = (int) ValueInfo::normalize($value, ValueInfo::T_BOOL | ValueInfo::M_DROP | valueInfo::M_NULL); + break; + } + } + if ($row===$test) { + $data[$index] = $test; + break; + } + } + $this->assertContains($row, $data, "Table $table does not contain record at array index $index."); + $found = array_search($row, $data, true); + unset($data[$found]); + } + $this->assertSame([], $data); + } + return true; + } + + public function primeExpectations(array $source, array $tableSpecs = null): array { + $out = []; + foreach ($tableSpecs as $table => $columns) { + // make sure the source has the table we want + $this->assertArrayHasKey($table, $source, "Source for expectations does not contain requested table $table."); + $out[$table] = [ + 'columns' => [], + 'rows' => array_fill(0, sizeof($source[$table]['rows']), []), + ]; + // make sure the source has all the columns we want for the table + $cols = array_flip($columns); + $cols = array_intersect_key($cols, $source[$table]['columns']); + $this->assertSame(array_keys($cols), $columns, "Source for table $table does not contain all requested columns"); + // get a map of source value offsets and keys + $targets = array_flip(array_keys($source[$table]['columns'])); + foreach ($cols as $key => $order) { + // fill the column-spec + $out[$table]['columns'][$key] = $source[$table]['columns'][$key]; + foreach ($source[$table]['rows'] as $index => $row) { + // fill each row column-wise with re-ordered values + $out[$table]['rows'][$index][$order] = $row[$targets[$key]]; + } + } + } + return $out; + } + + public function assertResult(array $expected, Result $data) { + $data = $data->getAll(); + $this->assertCount(sizeof($expected), $data, "Number of result rows (".sizeof($data).") differs from number of expected rows (".sizeof($expected).")"); + if (sizeof($expected)) { + // make sure the expectations are consistent + foreach ($expected as $exp) { + if (!isset($keys)) { + $keys = $exp; + continue; + } + $this->assertSame(array_keys($keys), array_keys($exp), "Result set expectations are irregular"); + } + // filter the result set to contain just the desired keys (we don't care if the result has extra keys) + $rows = []; + foreach ($data as $row) { + $rows[] = array_intersect_key($row, $keys); + } + // compare the result set to the expectations + foreach ($rows as $row) { + $this->assertContains($row, $expected, "Result set contains unexpected record."); + $found = array_search($row, $expected); + unset($expected[$found]); + } + $this->assertArraySubset($expected, [], "Expectations not in result set."); + } + } +} diff --git a/tests/lib/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php similarity index 100% rename from tests/lib/Database/SeriesArticle.php rename to tests/cases/Database/SeriesArticle.php diff --git a/tests/lib/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php similarity index 100% rename from tests/lib/Database/SeriesCleanup.php rename to tests/cases/Database/SeriesCleanup.php diff --git a/tests/lib/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php similarity index 100% rename from tests/lib/Database/SeriesFeed.php rename to tests/cases/Database/SeriesFeed.php diff --git a/tests/lib/Database/SeriesFolder.php b/tests/cases/Database/SeriesFolder.php similarity index 100% rename from tests/lib/Database/SeriesFolder.php rename to tests/cases/Database/SeriesFolder.php diff --git a/tests/lib/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php similarity index 100% rename from tests/lib/Database/SeriesLabel.php rename to tests/cases/Database/SeriesLabel.php diff --git a/tests/lib/Database/SeriesMeta.php b/tests/cases/Database/SeriesMeta.php similarity index 100% rename from tests/lib/Database/SeriesMeta.php rename to tests/cases/Database/SeriesMeta.php diff --git a/tests/lib/Database/SeriesMiscellany.php b/tests/cases/Database/SeriesMiscellany.php similarity index 100% rename from tests/lib/Database/SeriesMiscellany.php rename to tests/cases/Database/SeriesMiscellany.php diff --git a/tests/lib/Database/SeriesSession.php b/tests/cases/Database/SeriesSession.php similarity index 100% rename from tests/lib/Database/SeriesSession.php rename to tests/cases/Database/SeriesSession.php diff --git a/tests/lib/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php similarity index 100% rename from tests/lib/Database/SeriesSubscription.php rename to tests/cases/Database/SeriesSubscription.php diff --git a/tests/lib/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php similarity index 100% rename from tests/lib/Database/SeriesUser.php rename to tests/cases/Database/SeriesUser.php diff --git a/tests/phpunit.xml b/tests/phpunit.xml index e863737d..343f381f 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -64,27 +64,9 @@ - cases/Db/SQLite3/Database/TestMiscellany.php - cases/Db/SQLite3/Database/TestMeta.php - cases/Db/SQLite3/Database/TestUser.php - cases/Db/SQLite3/Database/TestSession.php - cases/Db/SQLite3/Database/TestFolder.php - cases/Db/SQLite3/Database/TestFeed.php - cases/Db/SQLite3/Database/TestSubscription.php - cases/Db/SQLite3/Database/TestArticle.php - cases/Db/SQLite3/Database/TestLabel.php - cases/Db/SQLite3/Database/TestCleanup.php - - cases/Db/SQLite3PDO/Database/TestMiscellany.php - cases/Db/SQLite3PDO/Database/TestMeta.php - cases/Db/SQLite3PDO/Database/TestUser.php - cases/Db/SQLite3PDO/Database/TestSession.php - cases/Db/SQLite3PDO/Database/TestFolder.php - cases/Db/SQLite3PDO/Database/TestFeed.php - cases/Db/SQLite3PDO/Database/TestSubscription.php - cases/Db/SQLite3PDO/Database/TestArticle.php - cases/Db/SQLite3PDO/Database/TestLabel.php - cases/Db/SQLite3PDO/Database/TestCleanup.php + cases/Db/SQLite3/TestDatabase.php + cases/Db/SQLite3PDO/TestDatabase.php + cases/REST/TestTarget.php From 7340d65c0e1daa4511cd5c2bf0ef4961ff68d91e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 23 Nov 2018 10:01:17 -0500 Subject: [PATCH 17/58] Make data clearing in tests static --- tests/cases/CLI/TestCLI.php | 2 +- tests/cases/Conf/TestConf.php | 4 ++-- tests/cases/Database/Base.php | 8 ++++---- tests/cases/Database/SeriesArticle.php | 2 +- tests/cases/Database/SeriesCleanup.php | 2 +- tests/cases/Database/SeriesFeed.php | 2 +- tests/cases/Database/SeriesFolder.php | 2 +- tests/cases/Database/SeriesLabel.php | 2 +- tests/cases/Database/SeriesMeta.php | 2 +- tests/cases/Database/SeriesMiscellany.php | 2 +- tests/cases/Database/SeriesSession.php | 2 +- tests/cases/Database/SeriesSubscription.php | 2 +- tests/cases/Database/SeriesUser.php | 2 +- tests/cases/Db/BaseDriver.php | 4 ++-- tests/cases/Db/BaseResult.php | 4 ++-- tests/cases/Db/BaseStatement.php | 4 ++-- tests/cases/Db/SQLite3/Database/TestArticle.php | 17 ----------------- tests/cases/Db/SQLite3/Database/TestCleanup.php | 17 ----------------- tests/cases/Db/SQLite3/Database/TestFeed.php | 17 ----------------- tests/cases/Db/SQLite3/Database/TestFolder.php | 17 ----------------- tests/cases/Db/SQLite3/Database/TestLabel.php | 13 ------------- tests/cases/Db/SQLite3/Database/TestMeta.php | 17 ----------------- .../Db/SQLite3/Database/TestMiscellany.php | 17 ----------------- tests/cases/Db/SQLite3/Database/TestSession.php | 13 ------------- .../Db/SQLite3/Database/TestSubscription.php | 17 ----------------- tests/cases/Db/SQLite3/Database/TestUser.php | 17 ----------------- tests/cases/Db/SQLite3/TestCreation.php | 4 ++-- tests/cases/Db/SQLite3/TestDriver.php | 2 +- tests/cases/Db/SQLite3/TestUpdate.php | 4 ++-- .../Db/SQLite3PDO/Database/TestArticle.php | 17 ----------------- .../Db/SQLite3PDO/Database/TestCleanup.php | 17 ----------------- tests/cases/Db/SQLite3PDO/Database/TestFeed.php | 17 ----------------- .../cases/Db/SQLite3PDO/Database/TestFolder.php | 17 ----------------- .../cases/Db/SQLite3PDO/Database/TestLabel.php | 13 ------------- tests/cases/Db/SQLite3PDO/Database/TestMeta.php | 17 ----------------- .../Db/SQLite3PDO/Database/TestMiscellany.php | 17 ----------------- .../Db/SQLite3PDO/Database/TestSession.php | 13 ------------- .../Db/SQLite3PDO/Database/TestSubscription.php | 17 ----------------- tests/cases/Db/SQLite3PDO/Database/TestUser.php | 17 ----------------- tests/cases/Db/SQLite3PDO/TestCreation.php | 4 ++-- tests/cases/Db/SQLite3PDO/TestUpdate.php | 4 ++-- tests/cases/Db/TestTransaction.php | 2 +- tests/cases/Exception/TestException.php | 4 ++-- tests/cases/Feed/TestFeed.php | 2 +- tests/cases/Feed/TestFetching.php | 2 +- tests/cases/Misc/TestDate.php | 2 +- tests/cases/Misc/TestValueInfo.php | 2 +- tests/cases/REST/NextCloudNews/TestV1_2.php | 4 ++-- tests/cases/REST/NextCloudNews/TestVersions.php | 2 +- tests/cases/REST/TinyTinyRSS/TestAPI.php | 4 ++-- tests/cases/REST/TinyTinyRSS/TestIcon.php | 4 ++-- tests/cases/Service/TestService.php | 2 +- tests/cases/User/TestInternal.php | 2 +- tests/cases/User/TestUser.php | 2 +- tests/lib/AbstractTest.php | 6 +++--- tests/lib/Database/Setup.php | 4 ++-- tests/lib/Lang/Setup.php | 4 ++-- 57 files changed, 56 insertions(+), 380 deletions(-) delete mode 100644 tests/cases/Db/SQLite3/Database/TestArticle.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestCleanup.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestFeed.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestFolder.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestLabel.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestMeta.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestMiscellany.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestSession.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestSubscription.php delete mode 100644 tests/cases/Db/SQLite3/Database/TestUser.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestArticle.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestCleanup.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestFeed.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestFolder.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestLabel.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestMeta.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestMiscellany.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestSession.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestSubscription.php delete mode 100644 tests/cases/Db/SQLite3PDO/Database/TestUser.php diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index 108e3280..d34e8b96 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -18,7 +18,7 @@ use Phake; class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { - $this->clearData(false); + self::clearData(false); } public function assertConsole(CLI $cli, string $command, int $exitStatus, string $output = "", bool $pattern = false) { diff --git a/tests/cases/Conf/TestConf.php b/tests/cases/Conf/TestConf.php index 5aa56d8d..aab95b9f 100644 --- a/tests/cases/Conf/TestConf.php +++ b/tests/cases/Conf/TestConf.php @@ -15,7 +15,7 @@ class TestConf extends \JKingWeb\Arsse\Test\AbstractTest { public static $path; public function setUp() { - $this->clearData(); + self::clearData(); self::$vfs = vfsStream::setup("root", null, [ 'confGood' => ' "xx");', 'confNotArray' => 'clearData(); + self::clearData(); } public function testLoadDefaultValues() { diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php index 711c3ccd..8f6f7b67 100644 --- a/tests/cases/Database/Base.php +++ b/tests/cases/Database/Base.php @@ -11,7 +11,7 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Conf; use JKingWeb\Arsse\User; use JKingWeb\Arsse\Misc\ValueInfo; -use JKingWeb\Arsse\Test\Database; +use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\Result; use Phake; @@ -23,12 +23,12 @@ abstract class Base { public function setUp() { // establish a clean baseline - $this->clearData(); + self::clearData(); self::setConf(); // configure and create the relevant database driver $this->setUpDriver(); // create the database interface with the suitable driver - Arsse::$db = new Database($this->drv); + Arsse::$db = new Database; Arsse::$db->driverSchemaUpdate(); // create a mock user manager Arsse::$user = Phake::mock(User::class); @@ -51,7 +51,7 @@ abstract class Base { // clean up $this->primed = false; $this->drv = null; - $this->clearData(); + self::clearData(); } public function primeDatabase(array $data, \JKingWeb\Arsse\Db\Driver $drv = null): bool { diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 7ffae2d1..695fe3cb 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Arsse; diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index 532c18de..fcb2393d 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use Phake; diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index 24a0097e..00475aef 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Feed; diff --git a/tests/cases/Database/SeriesFolder.php b/tests/cases/Database/SeriesFolder.php index d2d5b251..6199d232 100644 --- a/tests/cases/Database/SeriesFolder.php +++ b/tests/cases/Database/SeriesFolder.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use Phake; diff --git a/tests/cases/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php index c764b046..062d23e9 100644 --- a/tests/cases/Database/SeriesLabel.php +++ b/tests/cases/Database/SeriesLabel.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Misc\Context; diff --git a/tests/cases/Database/SeriesMeta.php b/tests/cases/Database/SeriesMeta.php index 58ae20dc..467c8e0b 100644 --- a/tests/cases/Database/SeriesMeta.php +++ b/tests/cases/Database/SeriesMeta.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Test\Database; use JKingWeb\Arsse\Arsse; diff --git a/tests/cases/Database/SeriesMiscellany.php b/tests/cases/Database/SeriesMiscellany.php index e58c4306..c5e0eb97 100644 --- a/tests/cases/Database/SeriesMiscellany.php +++ b/tests/cases/Database/SeriesMiscellany.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; diff --git a/tests/cases/Database/SeriesSession.php b/tests/cases/Database/SeriesSession.php index 26cf58ab..e605b868 100644 --- a/tests/cases/Database/SeriesSession.php +++ b/tests/cases/Database/SeriesSession.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Misc\Date; diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index a04fcf62..7e3a3941 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Test\Database; diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 78d1f81b..402dec6f 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\Test\Database; +namespace JKingWeb\Arsse\TestCase\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\User\Driver as UserDriver; diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index 653da327..46244080 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -22,7 +22,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { ]; public function setUp() { - $this->clearData(); + self::clearData(); self::setConf($this->conf); $info = new DatabaseInformation($this->implementation); $this->interface = ($info->interfaceConstructor)(); @@ -37,7 +37,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - $this->clearData(); + self::clearData(); unset($this->drv); try { $this->exec("ROLLBACK"); diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index 8a79d747..96dae042 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -18,7 +18,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { abstract protected function makeResult(string $q): array; public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); $info = new DatabaseInformation($this->implementation); $this->interface = ($info->interfaceConstructor)(); @@ -31,7 +31,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - $this->clearData(); + self::clearData(); $this->exec("DROP TABLE IF EXISTS arsse_meta"); } diff --git a/tests/cases/Db/BaseStatement.php b/tests/cases/Db/BaseStatement.php index 86705446..4369f7ac 100644 --- a/tests/cases/Db/BaseStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -19,7 +19,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { abstract protected function decorateTypeSyntax(string $value, string $type): string; public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); $info = new DatabaseInformation($this->implementation); $this->interface = ($info->interfaceConstructor)(); @@ -33,7 +33,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { public function tearDown() { $this->exec("DROP TABLE IF EXISTS arsse_meta"); - $this->clearData(); + self::clearData(); } public function testConstructStatement() { diff --git a/tests/cases/Db/SQLite3/Database/TestArticle.php b/tests/cases/Db/SQLite3/Database/TestArticle.php deleted file mode 100644 index 9531d4d1..00000000 --- a/tests/cases/Db/SQLite3/Database/TestArticle.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestArticle extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesArticle; -} diff --git a/tests/cases/Db/SQLite3/Database/TestCleanup.php b/tests/cases/Db/SQLite3/Database/TestCleanup.php deleted file mode 100644 index 5374e1b4..00000000 --- a/tests/cases/Db/SQLite3/Database/TestCleanup.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestCleanup extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesCleanup; -} diff --git a/tests/cases/Db/SQLite3/Database/TestFeed.php b/tests/cases/Db/SQLite3/Database/TestFeed.php deleted file mode 100644 index e46a17fe..00000000 --- a/tests/cases/Db/SQLite3/Database/TestFeed.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesFeed; -} diff --git a/tests/cases/Db/SQLite3/Database/TestFolder.php b/tests/cases/Db/SQLite3/Database/TestFolder.php deleted file mode 100644 index bc88e9af..00000000 --- a/tests/cases/Db/SQLite3/Database/TestFolder.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestFolder extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesFolder; -} diff --git a/tests/cases/Db/SQLite3/Database/TestLabel.php b/tests/cases/Db/SQLite3/Database/TestLabel.php deleted file mode 100644 index 70923207..00000000 --- a/tests/cases/Db/SQLite3/Database/TestLabel.php +++ /dev/null @@ -1,13 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestLabel extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesLabel; -} diff --git a/tests/cases/Db/SQLite3/Database/TestMeta.php b/tests/cases/Db/SQLite3/Database/TestMeta.php deleted file mode 100644 index 0693d302..00000000 --- a/tests/cases/Db/SQLite3/Database/TestMeta.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestMeta extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesMeta; -} diff --git a/tests/cases/Db/SQLite3/Database/TestMiscellany.php b/tests/cases/Db/SQLite3/Database/TestMiscellany.php deleted file mode 100644 index 77014288..00000000 --- a/tests/cases/Db/SQLite3/Database/TestMiscellany.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestMiscellany extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesMiscellany; -} diff --git a/tests/cases/Db/SQLite3/Database/TestSession.php b/tests/cases/Db/SQLite3/Database/TestSession.php deleted file mode 100644 index f8344b52..00000000 --- a/tests/cases/Db/SQLite3/Database/TestSession.php +++ /dev/null @@ -1,13 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestSession extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesSession; -} diff --git a/tests/cases/Db/SQLite3/Database/TestSubscription.php b/tests/cases/Db/SQLite3/Database/TestSubscription.php deleted file mode 100644 index c7c6c57e..00000000 --- a/tests/cases/Db/SQLite3/Database/TestSubscription.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestSubscription extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesSubscription; -} diff --git a/tests/cases/Db/SQLite3/Database/TestUser.php b/tests/cases/Db/SQLite3/Database/TestUser.php deleted file mode 100644 index 3659bf9f..00000000 --- a/tests/cases/Db/SQLite3/Database/TestUser.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3; - use \JKingWeb\Arsse\Test\Database\SeriesUser; -} diff --git a/tests/cases/Db/SQLite3/TestCreation.php b/tests/cases/Db/SQLite3/TestCreation.php index 124c76a0..d85aecdc 100644 --- a/tests/cases/Db/SQLite3/TestCreation.php +++ b/tests/cases/Db/SQLite3/TestCreation.php @@ -24,7 +24,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { if (!Driver::requirementsMet()) { $this->markTestSkipped("SQLite extension not loaded"); } - $this->clearData(); + self::clearData(); // test files $this->files = [ // cannot create files @@ -111,7 +111,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - $this->clearData(); + self::clearData(); } public function testFailToCreateDatabase() { diff --git a/tests/cases/Db/SQLite3/TestDriver.php b/tests/cases/Db/SQLite3/TestDriver.php index 58ef4bfd..df802106 100644 --- a/tests/cases/Db/SQLite3/TestDriver.php +++ b/tests/cases/Db/SQLite3/TestDriver.php @@ -48,7 +48,7 @@ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { } public function provideDrivers() { - $this->clearData(); + self::clearData(); self::setConf([ 'dbTimeoutExec' => 0.5, 'dbSQLite3Timeout' => 0, diff --git a/tests/cases/Db/SQLite3/TestUpdate.php b/tests/cases/Db/SQLite3/TestUpdate.php index a2a70b0b..1c219a1d 100644 --- a/tests/cases/Db/SQLite3/TestUpdate.php +++ b/tests/cases/Db/SQLite3/TestUpdate.php @@ -29,7 +29,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { if (!Driver::requirementsMet()) { $this->markTestSkipped("SQLite extension not loaded"); } - $this->clearData(); + self::clearData(); $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); self::setConf($conf); $this->base = $this->vfs->url(); @@ -41,7 +41,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { unset($this->drv); unset($this->data); unset($this->vfs); - $this->clearData(); + self::clearData(); } public function testLoadMissingFile() { diff --git a/tests/cases/Db/SQLite3PDO/Database/TestArticle.php b/tests/cases/Db/SQLite3PDO/Database/TestArticle.php deleted file mode 100644 index 30521b42..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestArticle.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestArticle extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesArticle; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestCleanup.php b/tests/cases/Db/SQLite3PDO/Database/TestCleanup.php deleted file mode 100644 index 708001d4..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestCleanup.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestCleanup extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesCleanup; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestFeed.php b/tests/cases/Db/SQLite3PDO/Database/TestFeed.php deleted file mode 100644 index e662d8e6..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestFeed.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesFeed; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestFolder.php b/tests/cases/Db/SQLite3PDO/Database/TestFolder.php deleted file mode 100644 index 777a0110..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestFolder.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestFolder extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesFolder; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestLabel.php b/tests/cases/Db/SQLite3PDO/Database/TestLabel.php deleted file mode 100644 index b2fe1580..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestLabel.php +++ /dev/null @@ -1,13 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestLabel extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesLabel; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestMeta.php b/tests/cases/Db/SQLite3PDO/Database/TestMeta.php deleted file mode 100644 index 96981311..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestMeta.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestMeta extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesMeta; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestMiscellany.php b/tests/cases/Db/SQLite3PDO/Database/TestMiscellany.php deleted file mode 100644 index 868e7fc5..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestMiscellany.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestMiscellany extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesMiscellany; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestSession.php b/tests/cases/Db/SQLite3PDO/Database/TestSession.php deleted file mode 100644 index 88535b2b..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestSession.php +++ /dev/null @@ -1,13 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestSession extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesSession; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestSubscription.php b/tests/cases/Db/SQLite3PDO/Database/TestSubscription.php deleted file mode 100644 index 83e7daf8..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestSubscription.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestSubscription extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesSubscription; -} diff --git a/tests/cases/Db/SQLite3PDO/Database/TestUser.php b/tests/cases/Db/SQLite3PDO/Database/TestUser.php deleted file mode 100644 index 18b0c05a..00000000 --- a/tests/cases/Db/SQLite3PDO/Database/TestUser.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @covers \JKingWeb\Arsse\Misc\Query - */ -class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { - use \JKingWeb\Arsse\Test\Database\Setup; - use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; - use \JKingWeb\Arsse\Test\Database\SeriesUser; -} diff --git a/tests/cases/Db/SQLite3PDO/TestCreation.php b/tests/cases/Db/SQLite3PDO/TestCreation.php index acae72b1..526400b8 100644 --- a/tests/cases/Db/SQLite3PDO/TestCreation.php +++ b/tests/cases/Db/SQLite3PDO/TestCreation.php @@ -25,7 +25,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { if (!Driver::requirementsMet()) { $this->markTestSkipped("PDO-SQLite extension not loaded"); } - $this->clearData(); + self::clearData(); // test files $this->files = [ // cannot create files @@ -112,7 +112,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - $this->clearData(); + self::clearData(); } public function testFailToCreateDatabase() { diff --git a/tests/cases/Db/SQLite3PDO/TestUpdate.php b/tests/cases/Db/SQLite3PDO/TestUpdate.php index 409de79c..58caca37 100644 --- a/tests/cases/Db/SQLite3PDO/TestUpdate.php +++ b/tests/cases/Db/SQLite3PDO/TestUpdate.php @@ -29,7 +29,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { if (!PDODriver::requirementsMet()) { $this->markTestSkipped("PDO-SQLite extension not loaded"); } - $this->clearData(); + self::clearData(); $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); $conf['dbDriver'] = PDODriver::class; self::setConf($conf); @@ -42,7 +42,7 @@ class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { unset($this->drv); unset($this->data); unset($this->vfs); - $this->clearData(); + self::clearData(); } public function testLoadMissingFile() { diff --git a/tests/cases/Db/TestTransaction.php b/tests/cases/Db/TestTransaction.php index 9469d6c2..22b445a5 100644 --- a/tests/cases/Db/TestTransaction.php +++ b/tests/cases/Db/TestTransaction.php @@ -16,7 +16,7 @@ class TestTransaction extends \JKingWeb\Arsse\Test\AbstractTest { protected $drv; public function setUp() { - $this->clearData(); + self::clearData(); $drv = Phake::mock(\JKingWeb\Arsse\Db\SQLite3\Driver::class); Phake::when($drv)->savepointRelease->thenReturn(true); Phake::when($drv)->savepointUndo->thenReturn(true); diff --git a/tests/cases/Exception/TestException.php b/tests/cases/Exception/TestException.php index f77ce371..05d1d928 100644 --- a/tests/cases/Exception/TestException.php +++ b/tests/cases/Exception/TestException.php @@ -15,7 +15,7 @@ use Phake; /** @covers \JKingWeb\Arsse\AbstractException */ class TestException extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { - $this->clearData(false); + self::clearData(false); // create a mock Lang object so as not to create a dependency loop Arsse::$lang = Phake::mock(Lang::class); Phake::when(Arsse::$lang)->msg->thenReturn(""); @@ -26,7 +26,7 @@ class TestException extends \JKingWeb\Arsse\Test\AbstractTest { Phake::verify(Arsse::$lang, Phake::atLeast(0))->msg($this->isType("string"), $this->anything()); Phake::verifyNoOtherInteractions(Arsse::$lang); // clean up - $this->clearData(true); + self::clearData(true); } public function testBaseClass() { diff --git a/tests/cases/Feed/TestFeed.php b/tests/cases/Feed/TestFeed.php index 01e9022d..d133ee7f 100644 --- a/tests/cases/Feed/TestFeed.php +++ b/tests/cases/Feed/TestFeed.php @@ -95,7 +95,7 @@ class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { $this->markTestSkipped("Test Web server is not accepting requests"); } $this->base = self::$host."Feed/"; - $this->clearData(); + self::clearData(); self::setConf(); Arsse::$db = Phake::mock(Database::class); } diff --git a/tests/cases/Feed/TestFetching.php b/tests/cases/Feed/TestFetching.php index 64102b9d..11602b55 100644 --- a/tests/cases/Feed/TestFetching.php +++ b/tests/cases/Feed/TestFetching.php @@ -25,7 +25,7 @@ class TestFetching extends \JKingWeb\Arsse\Test\AbstractTest { $this->markTestSkipped("Test Web server is not accepting requests"); } $this->base = self::$host."Feed/"; - $this->clearData(); + self::clearData(); self::setConf(); } diff --git a/tests/cases/Misc/TestDate.php b/tests/cases/Misc/TestDate.php index e82f6c8b..7fdae602 100644 --- a/tests/cases/Misc/TestDate.php +++ b/tests/cases/Misc/TestDate.php @@ -11,7 +11,7 @@ use JKingWeb\Arsse\Misc\Date; /** @covers \JKingWeb\Arsse\Misc\Date */ class TestDate extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { - $this->clearData(); + self::clearData(); } public function testNormalizeADate() { diff --git a/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index e6dbdf4d..2d0973e1 100644 --- a/tests/cases/Misc/TestValueInfo.php +++ b/tests/cases/Misc/TestValueInfo.php @@ -14,7 +14,7 @@ use JKingWeb\Arsse\Test\Result; /** @covers \JKingWeb\Arsse\Misc\ValueInfo */ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { - $this->clearData(); + self::clearData(); } public function testGetIntegerInfo() { diff --git a/tests/cases/REST/NextCloudNews/TestV1_2.php b/tests/cases/REST/NextCloudNews/TestV1_2.php index 22f3ab6a..f7936f45 100644 --- a/tests/cases/REST/NextCloudNews/TestV1_2.php +++ b/tests/cases/REST/NextCloudNews/TestV1_2.php @@ -339,7 +339,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { } public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); // create a mock user manager Arsse::$user = Phake::mock(User::class); @@ -352,7 +352,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - $this->clearData(); + self::clearData(); } protected function v($value) { diff --git a/tests/cases/REST/NextCloudNews/TestVersions.php b/tests/cases/REST/NextCloudNews/TestVersions.php index 28c6e0ca..c803f8d6 100644 --- a/tests/cases/REST/NextCloudNews/TestVersions.php +++ b/tests/cases/REST/NextCloudNews/TestVersions.php @@ -15,7 +15,7 @@ use Zend\Diactoros\Response\EmptyResponse; /** @covers \JKingWeb\Arsse\REST\NextCloudNews\Versions */ class TestVersions extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { - $this->clearData(); + self::clearData(); } protected function req(string $method, string $target): ResponseInterface { diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index eb6ead75..10fc535d 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -176,7 +176,7 @@ LONG_STRING; } public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); // create a mock user manager Arsse::$user = Phake::mock(User::class); @@ -196,7 +196,7 @@ LONG_STRING; } public function tearDown() { - $this->clearData(); + self::clearData(); } public function testHandleInvalidPaths() { diff --git a/tests/cases/REST/TinyTinyRSS/TestIcon.php b/tests/cases/REST/TinyTinyRSS/TestIcon.php index e25c6712..bacf3bec 100644 --- a/tests/cases/REST/TinyTinyRSS/TestIcon.php +++ b/tests/cases/REST/TinyTinyRSS/TestIcon.php @@ -23,7 +23,7 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { protected $user = "john.doe@example.com"; public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); // create a mock user manager Arsse::$user = Phake::mock(User::class); @@ -33,7 +33,7 @@ class TestIcon extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - $this->clearData(); + self::clearData(); } protected function req(string $target, string $method = "GET", string $user = null): ResponseInterface { diff --git a/tests/cases/Service/TestService.php b/tests/cases/Service/TestService.php index 69eec5f0..4373c632 100644 --- a/tests/cases/Service/TestService.php +++ b/tests/cases/Service/TestService.php @@ -18,7 +18,7 @@ class TestService extends \JKingWeb\Arsse\Test\AbstractTest { protected $srv; public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); Arsse::$db = Phake::mock(Database::class); $this->srv = new Service(); diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index a1f95dea..bc43377f 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -19,7 +19,7 @@ use Phake; class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); // create a mock database interface Arsse::$db = Phake::mock(Database::class); diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index c576245f..fbb47627 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -19,7 +19,7 @@ use Phake; class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { - $this->clearData(); + self::clearData(); self::setConf(); // create a mock database interface Arsse::$db = Phake::mock(Database::class); diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index a15d6748..addcc79c 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -21,14 +21,14 @@ use Zend\Diactoros\Response\EmptyResponse; /** @coversNothing */ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { public function setUp() { - $this->clearData(); + self::clearData(); } public function tearDown() { - $this->clearData(); + self::clearData(); } - public function clearData(bool $loadLang = true) { + public static function clearData(bool $loadLang = true) { date_default_timezone_set("America/Toronto"); $r = new \ReflectionClass(\JKingWeb\Arsse\Arsse::class); $props = array_keys($r->getStaticProperties()); diff --git a/tests/lib/Database/Setup.php b/tests/lib/Database/Setup.php index 88a01d11..b3c749d1 100644 --- a/tests/lib/Database/Setup.php +++ b/tests/lib/Database/Setup.php @@ -21,7 +21,7 @@ trait Setup { public function setUp() { // establish a clean baseline - $this->clearData(); + self::clearData(); self::setConf(); // configure and create the relevant database driver $this->setUpDriver(); @@ -49,7 +49,7 @@ trait Setup { // clean up $this->primed = false; $this->drv = null; - $this->clearData(); + self::clearData(); } public function primeDatabase(array $data, \JKingWeb\Arsse\Db\Driver $drv = null): bool { diff --git a/tests/lib/Lang/Setup.php b/tests/lib/Lang/Setup.php index 76843bb5..861dd413 100644 --- a/tests/lib/Lang/Setup.php +++ b/tests/lib/Lang/Setup.php @@ -39,7 +39,7 @@ trait Setup { // make the test Lang class use the vfs files $this->l = new TestLang($this->path); // create a mock Lang object so as not to create a dependency loop - $this->clearData(false); + self::clearData(false); Arsse::$lang = Phake::mock(Lang::class); Phake::when(Arsse::$lang)->msg->thenReturn(""); // call the additional setup method if it exists @@ -53,7 +53,7 @@ trait Setup { Phake::verify(Arsse::$lang, Phake::atLeast(0))->msg($this->isType("string"), $this->anything()); Phake::verifyNoOtherInteractions(Arsse::$lang); // clean up - $this->clearData(true); + self::clearData(true); // call the additional teardiwn method if it exists if (method_exists($this, "tearDownSeries")) { $this->tearDownSeries(); From 36c5984c4799c7f80d0810e2b330b4b586a1f0e8 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 23 Nov 2018 12:53:56 -0500 Subject: [PATCH 18/58] Add drop statements to database schemata to simplify testing --- sql/PostgreSQL/0.sql | 26 +++++---- sql/PostgreSQL/1.sql | 11 ++-- sql/PostgreSQL/2.sql | 7 ++- sql/SQLite3/0.sql | 81 ++++++++++++++++---------- sql/SQLite3/1.sql | 40 ++++++++----- sql/SQLite3/2.sql | 135 ++++++++++++++++++++++++------------------- 6 files changed, 173 insertions(+), 127 deletions(-) diff --git a/sql/PostgreSQL/0.sql b/sql/PostgreSQL/0.sql index 461b1f23..c76f6143 100644 --- a/sql/PostgreSQL/0.sql +++ b/sql/PostgreSQL/0.sql @@ -2,13 +2,25 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details --- metadata +-- Please consult the SQLite 3 schemata for commented version + +drop table if exists arsse_meta cascade; +drop table if exists arsse_users cascade; +drop table if exists arsse_users_meta cascade; +drop table if exists arsse_folders cascade; +drop table if exists arsse_feeds cascade; +drop table if exists arsse_subscriptions cascade; +drop table if exists arsse_articles cascade; +drop table if exists arsse_enclosures cascade; +drop table if exists arsse_marks cascade; +drop table if exists arsse_editions cascade; +drop table if exists arsse_categories cascade; + create table arsse_meta( key text primary key, value text ); --- users create table arsse_users( id text primary key, password text, @@ -19,7 +31,6 @@ create table arsse_users( rights bigint not null default 0 ); --- extra user metadata create table arsse_users_meta( owner text not null references arsse_users(id) on delete cascade on update cascade, key text not null, @@ -27,7 +38,6 @@ create table arsse_users_meta( primary key(owner,key) ); --- NextCloud News folders and TT-RSS categories create table arsse_folders( id bigserial primary key, owner text not null references arsse_users(id) on delete cascade on update cascade, @@ -37,7 +47,6 @@ create table arsse_folders( unique(owner,name,parent) ); --- newsfeeds, deduplicated create table arsse_feeds( id bigserial primary key, url text not null, @@ -58,7 +67,6 @@ create table arsse_feeds( unique(url,username,password) ); --- users' subscriptions to newsfeeds, with settings create table arsse_subscriptions( id bigserial primary key, owner text not null references arsse_users(id) on delete cascade on update cascade, @@ -72,7 +80,6 @@ create table arsse_subscriptions( unique(owner,feed) ); --- entries in newsfeeds create table arsse_articles( id bigserial primary key, feed bigint not null references arsse_feeds(id) on delete cascade, @@ -89,14 +96,12 @@ create table arsse_articles( title_content_hash text not null ); --- enclosures associated with articles create table arsse_enclosures( article bigint not null references arsse_articles(id) on delete cascade, url text, type text ); --- users' actions on newsfeed entries create table arsse_marks( article bigint not null references arsse_articles(id) on delete cascade, subscription bigint not null references arsse_subscriptions(id) on delete cascade on update cascade, @@ -106,18 +111,15 @@ create table arsse_marks( primary key(article,subscription) ); --- IDs for specific editions of articles (required for at least NextCloud News) create table arsse_editions( id bigserial primary key, article bigint not null references arsse_articles(id) on delete cascade, modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP ); --- author categories associated with newsfeed entries create table arsse_categories( article bigint not null references arsse_articles(id) on delete cascade, name text ); --- set version marker insert into arsse_meta(key,value) values('schema_version','1'); diff --git a/sql/PostgreSQL/1.sql b/sql/PostgreSQL/1.sql index f8a950b9..5c35d6b2 100644 --- a/sql/PostgreSQL/1.sql +++ b/sql/PostgreSQL/1.sql @@ -2,7 +2,12 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details --- Sessions for Tiny Tiny RSS (and possibly others) +-- Please consult the SQLite 3 schemata for commented version + +drop table if exists arsse_sessions cascade; +drop table if exists arsse_labels cascade; +drop table if exists arsse_label_members cascade; + create table arsse_sessions ( id text primary key, created timestamp(0) with time zone not null default CURRENT_TIMESTAMP, @@ -10,7 +15,6 @@ create table arsse_sessions ( user text not null references arsse_users(id) on delete cascade on update cascade ); --- User-defined article labels for Tiny Tiny RSS create table arsse_labels ( id bigserial primary key, owner text not null references arsse_users(id) on delete cascade on update cascade, @@ -19,7 +23,6 @@ create table arsse_labels ( unique(owner,name) ); --- Labels assignments for articles create table arsse_label_members ( label bigint not null references arsse_labels(id) on delete cascade, article bigint not null references arsse_articles(id) on delete cascade, @@ -29,8 +32,6 @@ create table arsse_label_members ( primary key(label,article) ); --- alter marks table to add Tiny Tiny RSS' notes alter table arsse_marks add column note text not null default ''; --- set version marker update arsse_meta set value = '2' where key = 'schema_version'; diff --git a/sql/PostgreSQL/2.sql b/sql/PostgreSQL/2.sql index cf5cf3db..cd1fbf65 100644 --- a/sql/PostgreSQL/2.sql +++ b/sql/PostgreSQL/2.sql @@ -2,13 +2,17 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details +-- Please consult the SQLite 3 schemata for commented version + -- create a case-insensitive generic collation sequence +-- this collation is Unicode-aware, whereas SQLite's built-in nocase +-- collation is ASCII-only +drop collation if exists nocase cascade; create collation nocase( provider = icu, locale = '@kf=false' ); --- Correct collation sequences alter table arsse_users alter column id type text collate nocase; alter table arsse_folders alter column name type text collate nocase; alter table arsse_feeds alter column title type text collate nocase; @@ -18,5 +22,4 @@ alter table arsse_articles alter column author type text collate nocase; alter table arsse_categories alter column name type text collate nocase; alter table arsse_labels alter column name type text collate nocase; --- set version marker update arsse_meta set value = '3' where key = 'schema_version'; diff --git a/sql/SQLite3/0.sql b/sql/SQLite3/0.sql index add9b56b..c8ae67f9 100644 --- a/sql/SQLite3/0.sql +++ b/sql/SQLite3/0.sql @@ -5,14 +5,27 @@ -- Make the database WAL-journalled; this is persitent PRAGMA journal_mode = wal; --- metadata +-- drop any existing tables, just in case +drop table if exists arsse_meta; +drop table if exists arsse_users; +drop table if exists arsse_users_meta; +drop table if exists arsse_folders; +drop table if exists arsse_feeds; +drop table if exists arsse_subscriptions; +drop table if exists arsse_articles; +drop table if exists arsse_enclosures; +drop table if exists arsse_marks; +drop table if exists arsse_editions; +drop table if exists arsse_categories; + create table arsse_meta( +-- application metadata key text primary key not null, -- metadata key value text -- metadata value, serialized as a string ); --- users create table arsse_users( +-- users id text primary key not null, -- user id password text, -- password, salted and hashed; if using external authentication this would be blank name text, -- display name @@ -22,35 +35,38 @@ create table arsse_users( rights integer not null default 0 -- temporary admin-rights marker FIXME: remove reliance on this ); --- extra user metadata create table arsse_users_meta( +-- extra user metadata (not currently used and will be removed) owner text not null references arsse_users(id) on delete cascade on update cascade, key text not null, value text, primary key(owner,key) ); --- NextCloud News folders create table arsse_folders( +-- folders, used by NextCloud News and Tiny Tiny RSS +-- feed subscriptions may belong to at most one folder; +-- in Tiny Tiny RSS folders may nest id integer primary key, -- sequence number owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of folder parent integer references arsse_folders(id) on delete cascade, -- parent folder id name text not null, -- folder name - modified text not null default CURRENT_TIMESTAMP, -- + modified text not null default CURRENT_TIMESTAMP, -- time at which the folder itself (not its contents) was changed; not currently used unique(owner,name,parent) -- cannot have multiple folders with the same name under the same parent for the same owner ); --- newsfeeds, deduplicated create table arsse_feeds( +-- newsfeeds, deduplicated +-- users have subscriptions to these feeds in another table id integer primary key, -- sequence number url text not null, -- URL of feed - title text, -- default title of feed + title text, -- default title of feed (users can set the title of their subscription to the feed) favicon text, -- URL of favicon source text, -- URL of site to which the feed belongs updated text, -- time at which the feed was last fetched modified text, -- time at which the feed last actually changed next_fetch text, -- time at which the feed should next be fetched - orphaned text, -- time at which the feed last had no subscriptions + orphaned text, -- time at which the feed last had no subscriptions etag text not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes err_count integer not null default 0, -- count of successive times update resulted in error since last successful update err_msg text, -- last error message @@ -61,13 +77,13 @@ create table arsse_feeds( unique(url,username,password) -- a URL with particular credentials should only appear once ); --- users' subscriptions to newsfeeds, with settings create table arsse_subscriptions( +-- users' subscriptions to newsfeeds, with settings id integer primary key, -- sequence number owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of subscription feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription added text not null default CURRENT_TIMESTAMP, -- time at which feed was added - modified text not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified + modified text not null default CURRENT_TIMESTAMP, -- time at which subscription properties were last modified title text, -- user-supplied title order_type int not null default 0, -- NextCloud sort order pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) @@ -75,16 +91,16 @@ create table arsse_subscriptions( unique(owner,feed) -- a given feed should only appear once for a given owner ); --- entries in newsfeeds create table arsse_articles( +-- entries in newsfeeds id integer primary key, -- sequence number feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription url text, -- URL of article title text, -- article title author text, -- author's name published text, -- time of original publication - edited text, -- time of last edit - modified text not null default CURRENT_TIMESTAMP, -- date when article properties were last modified + edited text, -- time of last edit by author + modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database content text, -- content, as (X)HTML guid text, -- GUID url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid. @@ -92,34 +108,37 @@ create table arsse_articles( title_content_hash text not null -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. ); --- enclosures associated with articles create table arsse_enclosures( - article integer not null references arsse_articles(id) on delete cascade, - url text, - type text +-- enclosures (attachments) associated with articles + article integer not null references arsse_articles(id) on delete cascade, -- article to which the enclosure belongs + url text, -- URL of the enclosure + type text -- content-type (MIME type) of the enclosure ); --- users' actions on newsfeed entries create table arsse_marks( - article integer not null references arsse_articles(id) on delete cascade, - subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, - read boolean not null default 0, - starred boolean not null default 0, - modified text not null default CURRENT_TIMESTAMP, - primary key(article,subscription) + article integer not null references arsse_articles(id) on delete cascade, -- article associated with the marks + subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- subscription associated with the marks; the subscription in turn belongs to a user + read boolean not null default 0, -- whether the article has been read + starred boolean not null default 0, -- whether the article is starred + modified text not null default CURRENT_TIMESTAMP, -- time at which an article was last modified by a given user + primary key(article,subscription) -- no more than one mark-set per article per user ); --- IDs for specific editions of articles (required for at least NextCloud News) create table arsse_editions( - id integer primary key, - article integer not null references arsse_articles(id) on delete cascade, - modified datetime not null default CURRENT_TIMESTAMP +-- IDs for specific editions of articles (required for at least NextCloud News) +-- every time an article is updated by its author, a new unique edition number is assigned +-- with NextCloud News this prevents users from marking as read an article which has been +-- updated since the client state was last refreshed + id integer primary key, -- sequence number + article integer not null references arsse_articles(id) on delete cascade, -- the article of which this is an edition + modified datetime not null default CURRENT_TIMESTAMP -- tiem at which the edition was modified (practically, when it was created) ); --- author categories associated with newsfeed entries create table arsse_categories( - article integer not null references arsse_articles(id) on delete cascade, - name text +-- author categories associated with newsfeed entries +-- these are not user-modifiable + article integer not null references arsse_articles(id) on delete cascade, -- article associated with the category + name text -- freeform name of the category ); -- set version marker diff --git a/sql/SQLite3/1.sql b/sql/SQLite3/1.sql index 8f273e60..1859ea89 100644 --- a/sql/SQLite3/1.sql +++ b/sql/SQLite3/1.sql @@ -2,16 +2,21 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details --- Sessions for Tiny Tiny RSS (and possibly others) +-- drop any existing tables, just in case +drop table if exists arsse_sessions; +drop table if exists arsse_labels; +drop table if exists arsse_label_members; + create table arsse_sessions ( +-- sessions for Tiny Tiny RSS (and possibly others) id text primary key, -- UUID of session created text not null default CURRENT_TIMESTAMP, -- Session start timestamp expires text not null, -- Time at which session is no longer valid user text not null references arsse_users(id) on delete cascade on update cascade -- user associated with the session ) without rowid; --- User-defined article labels for Tiny Tiny RSS create table arsse_labels ( +-- user-defined article labels for Tiny Tiny RSS id integer primary key, -- numeric ID owner text not null references arsse_users(id) on delete cascade on update cascade, -- owning user name text not null, -- label text @@ -19,30 +24,33 @@ create table arsse_labels ( unique(owner,name) ); --- Labels assignments for articles create table arsse_label_members ( - label integer not null references arsse_labels(id) on delete cascade, - article integer not null references arsse_articles(id) on delete cascade, +-- uabels assignments for articles + label integer not null references arsse_labels(id) on delete cascade, -- label ID associated to an article; label IDs belong to a user + article integer not null references arsse_articles(id) on delete cascade, -- article associated to a label subscription integer not null references arsse_subscriptions(id) on delete cascade, -- Subscription is included so that records are deleted when a subscription is removed - assigned boolean not null default 1, - modified text not null default CURRENT_TIMESTAMP, - primary key(label,article) + assigned boolean not null default 1, -- whether the association is current, to support soft deletion + modified text not null default CURRENT_TIMESTAMP, -- time at which the association was last made or unmade + primary key(label,article) -- only one association of a given label to a given article ) without rowid; -- alter marks table to add Tiny Tiny RSS' notes +-- SQLite has limited ALTER TABLE support, so the table must be re-created +-- and its data re-entered; other database systems have a much simpler prodecure alter table arsse_marks rename to arsse_marks_old; create table arsse_marks( - article integer not null references arsse_articles(id) on delete cascade, - subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, - read boolean not null default 0, - starred boolean not null default 0, - modified text not null default CURRENT_TIMESTAMP, - note text not null default '', - primary key(article,subscription) +-- users' actions on newsfeed entries + article integer not null references arsse_articles(id) on delete cascade, -- article associated with the marks + subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- subscription associated with the marks; the subscription in turn belongs to a user + read boolean not null default 0, -- whether the article has been read + starred boolean not null default 0, -- whether the article is starred + modified text not null default CURRENT_TIMESTAMP, -- time at which an article was last modified by a given user + note text not null default '', -- Tiny Tiny RSS freeform user note + primary key(article,subscription) -- no more than one mark-set per article per user ); insert into arsse_marks(article,subscription,read,starred,modified) select article,subscription,read,starred,modified from arsse_marks_old; drop table arsse_marks_old; -- set version marker pragma user_version = 2; -update arsse_meta set value = '2' where key = 'schema_version'; \ No newline at end of file +update arsse_meta set value = '2' where key = 'schema_version'; diff --git a/sql/SQLite3/2.sql b/sql/SQLite3/2.sql index 87f21efe..73402909 100644 --- a/sql/SQLite3/2.sql +++ b/sql/SQLite3/2.sql @@ -2,94 +2,106 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details --- Correct collation sequences +-- Correct collation sequences in order for various things to sort case-insensitively +-- SQLite has limited ALTER TABLE support, so the tables must be re-created +-- and their data re-entered; other database systems have a much simpler prodecure alter table arsse_users rename to arsse_users_old; create table arsse_users( - id text primary key not null collate nocase, - password text, - name text collate nocase, - avatar_type text, - avatar_data blob, - admin boolean default 0, - rights integer not null default 0 +-- users + id text primary key not null collate nocase, -- user id + password text, -- password, salted and hashed; if using external authentication this would be blank + name text collate nocase, -- display name + avatar_type text, -- internal avatar image's MIME content type + avatar_data blob, -- internal avatar image's binary data + admin boolean default 0, -- whether the user is a member of the special "admin" group + rights integer not null default 0 -- temporary admin-rights marker FIXME: remove reliance on this ); insert into arsse_users(id,password,name,avatar_type,avatar_data,admin,rights) select id,password,name,avatar_type,avatar_data,admin,rights from arsse_users_old; drop table arsse_users_old; alter table arsse_folders rename to arsse_folders_old; create table arsse_folders( - id integer primary key, - owner text not null references arsse_users(id) on delete cascade on update cascade, - parent integer references arsse_folders(id) on delete cascade, - name text not null collate nocase, - modified text not null default CURRENT_TIMESTAMP, -- - unique(owner,name,parent) +-- folders, used by NextCloud News and Tiny Tiny RSS +-- feed subscriptions may belong to at most one folder; +-- in Tiny Tiny RSS folders may nest + id integer primary key, -- sequence number + owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of folder + parent integer references arsse_folders(id) on delete cascade, -- parent folder id + name text not null collate nocase, -- folder name + modified text not null default CURRENT_TIMESTAMP, -- time at which the folder itself (not its contents) was changed; not currently used + unique(owner,name,parent) -- cannot have multiple folders with the same name under the same parent for the same owner ); insert into arsse_folders select * from arsse_folders_old; drop table arsse_folders_old; alter table arsse_feeds rename to arsse_feeds_old; create table arsse_feeds( - id integer primary key, - url text not null, - title text collate nocase, - favicon text, - source text, - updated text, - modified text, - next_fetch text, - orphaned text, - etag text not null default '', - err_count integer not null default 0, - err_msg text, - username text not null default '', - password text not null default '', - size integer not null default 0, - scrape boolean not null default 0, - unique(url,username,password) +-- newsfeeds, deduplicated +-- users have subscriptions to these feeds in another table + id integer primary key, -- sequence number + url text not null, -- URL of feed + title text collate nocase, -- default title of feed (users can set the title of their subscription to the feed) + favicon text, -- URL of favicon + source text, -- URL of site to which the feed belongs + updated text, -- time at which the feed was last fetched + modified text, -- time at which the feed last actually changed + next_fetch text, -- time at which the feed should next be fetched + orphaned text, -- time at which the feed last had no subscriptions + etag text not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes + err_count integer not null default 0, -- count of successive times update resulted in error since last successful update + err_msg text, -- last error message + username text not null default '', -- HTTP authentication username + password text not null default '', -- HTTP authentication password (this is stored in plain text) + size integer not null default 0, -- number of articles in the feed at last fetch + scrape boolean not null default 0, -- whether to use picoFeed's content scraper with this feed + unique(url,username,password) -- a URL with particular credentials should only appear once ); insert into arsse_feeds select * from arsse_feeds_old; drop table arsse_feeds_old; alter table arsse_subscriptions rename to arsse_subscriptions_old; create table arsse_subscriptions( - id integer primary key, - owner text not null references arsse_users(id) on delete cascade on update cascade, - feed integer not null references arsse_feeds(id) on delete cascade, - added text not null default CURRENT_TIMESTAMP, - modified text not null default CURRENT_TIMESTAMP, - title text collate nocase, - order_type int not null default 0, - pinned boolean not null default 0, - folder integer references arsse_folders(id) on delete cascade, - unique(owner,feed) +-- users' subscriptions to newsfeeds, with settings + id integer primary key, -- sequence number + owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of subscription + feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription + added text not null default CURRENT_TIMESTAMP, -- time at which feed was added + modified text not null default CURRENT_TIMESTAMP, -- time at which subscription properties were last modified + title text collate nocase, -- user-supplied title + order_type int not null default 0, -- NextCloud sort order + pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) + folder integer references arsse_folders(id) on delete cascade, -- TT-RSS category (nestable); the first-level category (which acts as NextCloud folder) is joined in when needed + unique(owner,feed) -- a given feed should only appear once for a given owner ); insert into arsse_subscriptions select * from arsse_subscriptions_old; drop table arsse_subscriptions_old; alter table arsse_articles rename to arsse_articles_old; create table arsse_articles( - id integer primary key, - feed integer not null references arsse_feeds(id) on delete cascade, - url text, - title text collate nocase, - author text collate nocase, - published text, - edited text, - modified text not null default CURRENT_TIMESTAMP, - content text, - guid text, - url_title_hash text not null, - url_content_hash text not null, - title_content_hash text not null +-- entries in newsfeeds + id integer primary key, -- sequence number + feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription + url text, -- URL of article + title text collate nocase, -- article title + author text collate nocase, -- author's name + published text, -- time of original publication + edited text, -- time of last edit by author + modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database + content text, -- content, as (X)HTML + guid text, -- GUID + url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid. + url_content_hash text not null, -- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. + title_content_hash text not null -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. ); insert into arsse_articles select * from arsse_articles_old; drop table arsse_articles_old; alter table arsse_categories rename to arsse_categories_old; create table arsse_categories( - article integer not null references arsse_articles(id) on delete cascade, - name text collate nocase +-- author categories associated with newsfeed entries +-- these are not user-modifiable + article integer not null references arsse_articles(id) on delete cascade, -- article associated with the category + name text collate nocase -- freeform name of the category ); insert into arsse_categories select * from arsse_categories_old; drop table arsse_categories_old; @@ -97,10 +109,11 @@ drop table arsse_categories_old; alter table arsse_labels rename to arsse_labels_old; create table arsse_labels ( - id integer primary key, - owner text not null references arsse_users(id) on delete cascade on update cascade, - name text not null collate nocase, - modified text not null default CURRENT_TIMESTAMP, +-- user-defined article labels for Tiny Tiny RSS + id integer primary key, -- numeric ID + owner text not null references arsse_users(id) on delete cascade on update cascade, -- owning user + name text not null collate nocase, -- label text + modified text not null default CURRENT_TIMESTAMP, -- time at which the label was last modified unique(owner,name) ); insert into arsse_labels select * from arsse_labels_old; @@ -108,4 +121,4 @@ drop table arsse_labels_old; -- set version marker pragma user_version = 3; -update arsse_meta set value = '3' where key = 'schema_version'; \ No newline at end of file +update arsse_meta set value = '3' where key = 'schema_version'; From dccd4caedefbb9fd5d38d108d53aab983e7b433c Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sat, 24 Nov 2018 23:18:17 -0500 Subject: [PATCH 19/58] Convert one database function test series (articles) to a common harness Also revert the dropping of tables in the schema files. This was for the convenience of tests, but the risk of data loss is too great --- sql/PostgreSQL/0.sql | 12 - sql/PostgreSQL/1.sql | 4 - sql/PostgreSQL/2.sql | 1 - sql/SQLite3/0.sql | 13 - sql/SQLite3/1.sql | 5 - tests/cases/Database/Base.php | 95 ++- tests/cases/Database/SeriesArticle.php | 825 +++++++++++---------- tests/cases/Db/SQLite3/TestDatabase.php | 19 + tests/cases/Db/SQLite3PDO/TestDatabase.php | 19 + tests/lib/DatabaseInformation.php | 82 +- 10 files changed, 603 insertions(+), 472 deletions(-) create mode 100644 tests/cases/Db/SQLite3/TestDatabase.php create mode 100644 tests/cases/Db/SQLite3PDO/TestDatabase.php diff --git a/sql/PostgreSQL/0.sql b/sql/PostgreSQL/0.sql index c76f6143..6e6b2f19 100644 --- a/sql/PostgreSQL/0.sql +++ b/sql/PostgreSQL/0.sql @@ -4,18 +4,6 @@ -- Please consult the SQLite 3 schemata for commented version -drop table if exists arsse_meta cascade; -drop table if exists arsse_users cascade; -drop table if exists arsse_users_meta cascade; -drop table if exists arsse_folders cascade; -drop table if exists arsse_feeds cascade; -drop table if exists arsse_subscriptions cascade; -drop table if exists arsse_articles cascade; -drop table if exists arsse_enclosures cascade; -drop table if exists arsse_marks cascade; -drop table if exists arsse_editions cascade; -drop table if exists arsse_categories cascade; - create table arsse_meta( key text primary key, value text diff --git a/sql/PostgreSQL/1.sql b/sql/PostgreSQL/1.sql index 5c35d6b2..086c7e35 100644 --- a/sql/PostgreSQL/1.sql +++ b/sql/PostgreSQL/1.sql @@ -4,10 +4,6 @@ -- Please consult the SQLite 3 schemata for commented version -drop table if exists arsse_sessions cascade; -drop table if exists arsse_labels cascade; -drop table if exists arsse_label_members cascade; - create table arsse_sessions ( id text primary key, created timestamp(0) with time zone not null default CURRENT_TIMESTAMP, diff --git a/sql/PostgreSQL/2.sql b/sql/PostgreSQL/2.sql index cd1fbf65..021d3cd6 100644 --- a/sql/PostgreSQL/2.sql +++ b/sql/PostgreSQL/2.sql @@ -7,7 +7,6 @@ -- create a case-insensitive generic collation sequence -- this collation is Unicode-aware, whereas SQLite's built-in nocase -- collation is ASCII-only -drop collation if exists nocase cascade; create collation nocase( provider = icu, locale = '@kf=false' diff --git a/sql/SQLite3/0.sql b/sql/SQLite3/0.sql index c8ae67f9..7a9dea6a 100644 --- a/sql/SQLite3/0.sql +++ b/sql/SQLite3/0.sql @@ -5,19 +5,6 @@ -- Make the database WAL-journalled; this is persitent PRAGMA journal_mode = wal; --- drop any existing tables, just in case -drop table if exists arsse_meta; -drop table if exists arsse_users; -drop table if exists arsse_users_meta; -drop table if exists arsse_folders; -drop table if exists arsse_feeds; -drop table if exists arsse_subscriptions; -drop table if exists arsse_articles; -drop table if exists arsse_enclosures; -drop table if exists arsse_marks; -drop table if exists arsse_editions; -drop table if exists arsse_categories; - create table arsse_meta( -- application metadata key text primary key not null, -- metadata key diff --git a/sql/SQLite3/1.sql b/sql/SQLite3/1.sql index 1859ea89..b96bd79f 100644 --- a/sql/SQLite3/1.sql +++ b/sql/SQLite3/1.sql @@ -2,11 +2,6 @@ -- Copyright 2017 J. King, Dustin Wilson et al. -- See LICENSE and AUTHORS files for details --- drop any existing tables, just in case -drop table if exists arsse_sessions; -drop table if exists arsse_labels; -drop table if exists arsse_label_members; - create table arsse_sessions ( -- sessions for Tiny Tiny RSS (and possibly others) id text primary key, -- UUID of session diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php index 8f6f7b67..8efcf3bc 100644 --- a/tests/cases/Database/Base.php +++ b/tests/cases/Database/Base.php @@ -6,37 +6,72 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Database; -use JKingWeb\Arsse\User\Driver as UserDriver; +use JKingWeb\Arsse\Test\Database; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Conf; use JKingWeb\Arsse\User; use JKingWeb\Arsse\Misc\ValueInfo; -use JKingWeb\Arsse\Database; use JKingWeb\Arsse\Db\Result; +use JKingWeb\Arsse\Test\DatabaseInformation; use Phake; -abstract class Base { - protected $drv; +abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ + use SeriesArticle; + + /** @var \JKingWeb\Arsse\Test\DatabaseInformation */ + protected static $dbInfo; + /** @var \JKingWeb\Arsse\Db\Driver */ + protected static $drv; + protected static $failureReason = ""; protected $primed = false; protected abstract function nextID(string $table): int; - public function setUp() { + protected function findTraitOfTest(string $test): string { + $class = new \ReflectionClass(self::class); + foreach ($class->getTraits() as $trait) { + if ($trait->hasMethod($test)) { + return $trait->getShortName(); + } + } + return $class->getShortName(); + } + + public static function setUpBeforeClass() { // establish a clean baseline - self::clearData(); - self::setConf(); - // configure and create the relevant database driver - $this->setUpDriver(); - // create the database interface with the suitable driver - Arsse::$db = new Database; + static::clearData(); + // perform an initial connection to the database to reset its version to zero + // in the case of SQLite this will always be the case (we use a memory database), + // but other engines should clean up from potentially interrupted prior tests + static::$dbInfo = new DatabaseInformation(static::$implementation); + static::setConf(); + try { + static::$drv = new static::$dbInfo->driverClass; + } catch (\JKingWeb\Arsse\Db\Exception $e) { + static::$failureReason = $e->getMessage(); + return; + } + // wipe the database absolutely clean + (static::$dbInfo->razeFunction)(static::$drv); + // create the database interface with the suitable driver and apply the latest schema + Arsse::$db = new Database(static::$drv); Arsse::$db->driverSchemaUpdate(); + } + + public function setUp() { + // get the name of the test's test series + $this->series = $this->findTraitofTest($this->getName()); + static::clearData(); + if (strlen(static::$failureReason)) { + $this->markTestSkipped(static::$failureReason); + } + Arsse::$db = new Database(static::$drv); // create a mock user manager Arsse::$user = Phake::mock(User::class); Phake::when(Arsse::$user)->authorize->thenReturn(true); - // call the additional setup method if it exists - if (method_exists($this, "setUpSeries")) { - $this->setUpSeries(); - } + // call the series-specific setup method + $setUp = "setUp".$this->series; + $this->$setUp(); // prime the database with series data if it hasn't already been done if (!$this->primed && isset($this->data)) { $this->primeDatabase($this->data); @@ -44,18 +79,30 @@ abstract class Base { } public function tearDown() { - // call the additional teardiwn method if it exists - if (method_exists($this, "tearDownSeries")) { - $this->tearDownSeries(); - } + // call the series-specific teardown method + $this->series = $this->findTraitofTest($this->getName()); + $tearDown = "tearDown".$this->series; + $this->$tearDown(); // clean up $this->primed = false; - $this->drv = null; - self::clearData(); + // call the database-specific table cleanup function + (static::$dbInfo->truncateFunction)(static::$drv); + // clear state + static::clearData(); } - public function primeDatabase(array $data, \JKingWeb\Arsse\Db\Driver $drv = null): bool { - $drv = $drv ?? $this->drv; + public static function tearDownAfterClass() { + // wipe the database absolutely clean + (static::$dbInfo->razeFunction)(static::$drv); + // clean up + static::$drv = null; + static::$dbInfo = null; + static::$failureReason = ""; + static::clearData(); + } + + public function primeDatabase(array $data): bool { + $drv = static::$drv; $tr = $drv->begin(); foreach ($data as $table => $info) { $cols = implode(",", array_keys($info['columns'])); @@ -75,7 +122,7 @@ abstract class Base { foreach ($expected as $table => $info) { $cols = implode(",", array_keys($info['columns'])); $types = $info['columns']; - $data = $this->drv->prepare("SELECT $cols from $table")->run()->getAll(); + $data = static::$drv->prepare("SELECT $cols from $table")->run()->getAll(); $cols = array_keys($info['columns']); foreach ($info['rows'] as $index => $row) { $this->assertCount(sizeof($cols), $row, "The number of values for array index $index does not match the number of fields"); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 695fe3cb..79d1461b 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -13,463 +13,464 @@ use JKingWeb\Arsse\Misc\Date; use Phake; trait SeriesArticle { - protected $data = [ - 'arsse_users' => [ - 'columns' => [ - 'id' => 'str', - 'password' => 'str', - 'name' => 'str', + protected function setUpSeriesArticle() { + $this->data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ["john.doe@example.org", "", "John Doe"], + ["john.doe@example.net", "", "John Doe"], + ], ], - 'rows' => [ - ["jane.doe@example.com", "", "Jane Doe"], - ["john.doe@example.com", "", "John Doe"], - ["john.doe@example.org", "", "John Doe"], - ["john.doe@example.net", "", "John Doe"], + 'arsse_folders' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'parent' => "int", + 'name' => "str", + ], + 'rows' => [ + [1, "john.doe@example.com", null, "Technology"], + [2, "john.doe@example.com", 1, "Software"], + [3, "john.doe@example.com", 1, "Rocketry"], + [4, "jane.doe@example.com", null, "Politics"], + [5, "john.doe@example.com", null, "Politics"], + [6, "john.doe@example.com", 2, "Politics"], + [7, "john.doe@example.net", null, "Technology"], + [8, "john.doe@example.net", 7, "Software"], + [9, "john.doe@example.net", null, "Politics"], + ] ], - ], - 'arsse_folders' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'parent' => "int", - 'name' => "str", + 'arsse_feeds' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + 'title' => "str", + ], + 'rows' => [ + [1,"http://example.com/1", "Feed 1"], + [2,"http://example.com/2", "Feed 2"], + [3,"http://example.com/3", "Feed 3"], + [4,"http://example.com/4", "Feed 4"], + [5,"http://example.com/5", "Feed 5"], + [6,"http://example.com/6", "Feed 6"], + [7,"http://example.com/7", "Feed 7"], + [8,"http://example.com/8", "Feed 8"], + [9,"http://example.com/9", "Feed 9"], + [10,"http://example.com/10", "Feed 10"], + [11,"http://example.com/11", "Feed 11"], + [12,"http://example.com/12", "Feed 12"], + [13,"http://example.com/13", "Feed 13"], + ] ], - 'rows' => [ - [1, "john.doe@example.com", null, "Technology"], - [2, "john.doe@example.com", 1, "Software"], - [3, "john.doe@example.com", 1, "Rocketry"], - [4, "jane.doe@example.com", null, "Politics"], - [5, "john.doe@example.com", null, "Politics"], - [6, "john.doe@example.com", 2, "Politics"], - [7, "john.doe@example.net", null, "Technology"], - [8, "john.doe@example.net", 7, "Software"], - [9, "john.doe@example.net", null, "Politics"], - ] - ], - 'arsse_feeds' => [ - 'columns' => [ - 'id' => "int", - 'url' => "str", - 'title' => "str", + 'arsse_subscriptions' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'feed' => "int", + 'folder' => "int", + 'title' => "str", + ], + 'rows' => [ + [1, "john.doe@example.com",1, null,"Subscription 1"], + [2, "john.doe@example.com",2, null,null], + [3, "john.doe@example.com",3, 1,"Subscription 3"], + [4, "john.doe@example.com",4, 6,null], + [5, "john.doe@example.com",10, 5,"Subscription 5"], + [6, "jane.doe@example.com",1, null,null], + [7, "jane.doe@example.com",10,null,"Subscription 7"], + [8, "john.doe@example.org",11,null,null], + [9, "john.doe@example.org",12,null,"Subscription 9"], + [10,"john.doe@example.org",13,null,null], + [11,"john.doe@example.net",10,null,"Subscription 11"], + [12,"john.doe@example.net",2, 9,null], + [13,"john.doe@example.net",3, 8,"Subscription 13"], + [14,"john.doe@example.net",4, 7,null], + ] ], - 'rows' => [ - [1,"http://example.com/1", "Feed 1"], - [2,"http://example.com/2", "Feed 2"], - [3,"http://example.com/3", "Feed 3"], - [4,"http://example.com/4", "Feed 4"], - [5,"http://example.com/5", "Feed 5"], - [6,"http://example.com/6", "Feed 6"], - [7,"http://example.com/7", "Feed 7"], - [8,"http://example.com/8", "Feed 8"], - [9,"http://example.com/9", "Feed 9"], - [10,"http://example.com/10", "Feed 10"], - [11,"http://example.com/11", "Feed 11"], - [12,"http://example.com/12", "Feed 12"], - [13,"http://example.com/13", "Feed 13"], - ] - ], - 'arsse_subscriptions' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'feed' => "int", - 'folder' => "int", - 'title' => "str", + 'arsse_articles' => [ + 'columns' => [ + 'id' => "int", + 'feed' => "int", + 'url' => "str", + 'title' => "str", + 'author' => "str", + 'published' => "datetime", + 'edited' => "datetime", + 'content' => "str", + 'guid' => "str", + 'url_title_hash' => "str", + 'url_content_hash' => "str", + 'title_content_hash' => "str", + 'modified' => "datetime", + ], + 'rows' => [ + [1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','

Article content 1

','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'], + [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','

Article content 2

','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'], + [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'], + [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','

Article content 4

','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'], + [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','

Article content 5

','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'], + ] ], - 'rows' => [ - [1, "john.doe@example.com",1, null,"Subscription 1"], - [2, "john.doe@example.com",2, null,null], - [3, "john.doe@example.com",3, 1,"Subscription 3"], - [4, "john.doe@example.com",4, 6,null], - [5, "john.doe@example.com",10, 5,"Subscription 5"], - [6, "jane.doe@example.com",1, null,null], - [7, "jane.doe@example.com",10,null,"Subscription 7"], - [8, "john.doe@example.org",11,null,null], - [9, "john.doe@example.org",12,null,"Subscription 9"], - [10,"john.doe@example.org",13,null,null], - [11,"john.doe@example.net",10,null,"Subscription 11"], - [12,"john.doe@example.net",2, 9,null], - [13,"john.doe@example.net",3, 8,"Subscription 13"], - [14,"john.doe@example.net",4, 7,null], - ] - ], - 'arsse_articles' => [ - 'columns' => [ - 'id' => "int", - 'feed' => "int", - 'url' => "str", - 'title' => "str", - 'author' => "str", - 'published' => "datetime", - 'edited' => "datetime", - 'content' => "str", - 'guid' => "str", - 'url_title_hash' => "str", - 'url_content_hash' => "str", - 'title_content_hash' => "str", - 'modified' => "datetime", - ], - 'rows' => [ - [1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','

Article content 1

','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'], - [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','

Article content 2

','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'], - [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'], - [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','

Article content 4

','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'], - [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','

Article content 5

','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'], - ] - ], - 'arsse_enclosures' => [ - 'columns' => [ - 'article' => "int", - 'url' => "str", - 'type' => "str", - ], - 'rows' => [ - [102,"http://example.com/text","text/plain"], - [103,"http://example.com/video","video/webm"], - [104,"http://example.com/image","image/svg+xml"], - [105,"http://example.com/audio","audio/ogg"], + 'arsse_enclosures' => [ + 'columns' => [ + 'article' => "int", + 'url' => "str", + 'type' => "str", + ], + 'rows' => [ + [102,"http://example.com/text","text/plain"], + [103,"http://example.com/video","video/webm"], + [104,"http://example.com/image","image/svg+xml"], + [105,"http://example.com/audio","audio/ogg"], - ] - ], - 'arsse_editions' => [ - 'columns' => [ - 'id' => "int", - 'article' => "int", + ] ], - 'rows' => [ - [1,1], - [2,2], - [3,3], - [4,4], - [5,5], - [6,6], - [7,7], - [8,8], - [9,9], - [10,10], - [11,11], - [12,12], - [13,13], - [14,14], - [15,15], - [16,16], - [17,17], - [18,18], - [19,19], - [20,20], - [101,101], - [102,102], - [103,103], - [104,104], - [105,105], - [202,102], - [203,103], - [204,104], - [205,105], - [305,105], - [1001,20], - ] - ], - 'arsse_marks' => [ - 'columns' => [ - 'subscription' => "int", - 'article' => "int", - 'read' => "bool", - 'starred' => "bool", - 'modified' => "datetime", - 'note' => "str", + 'arsse_editions' => [ + 'columns' => [ + 'id' => "int", + 'article' => "int", + ], + 'rows' => [ + [1,1], + [2,2], + [3,3], + [4,4], + [5,5], + [6,6], + [7,7], + [8,8], + [9,9], + [10,10], + [11,11], + [12,12], + [13,13], + [14,14], + [15,15], + [16,16], + [17,17], + [18,18], + [19,19], + [20,20], + [101,101], + [102,102], + [103,103], + [104,104], + [105,105], + [202,102], + [203,103], + [204,104], + [205,105], + [305,105], + [1001,20], + ] ], - 'rows' => [ - [1, 1,1,1,'2000-01-01 00:00:00',''], - [5, 19,1,0,'2016-01-01 00:00:00',''], - [5, 20,0,1,'2005-01-01 00:00:00',''], - [7, 20,1,0,'2010-01-01 00:00:00',''], - [8, 102,1,0,'2000-01-02 02:00:00','Note 2'], - [9, 103,0,1,'2000-01-03 03:00:00','Note 3'], - [9, 104,1,1,'2000-01-04 04:00:00','Note 4'], - [10,105,0,0,'2000-01-05 05:00:00',''], - [11, 19,0,0,'2017-01-01 00:00:00','ook'], - [11, 20,1,0,'2017-01-01 00:00:00','eek'], - [12, 3,0,1,'2017-01-01 00:00:00','ack'], - [12, 4,1,1,'2017-01-01 00:00:00','ach'], - [1, 2,0,0,'2010-01-01 00:00:00','Some Note'], - ] - ], - 'arsse_categories' => [ // author-supplied categories - 'columns' => [ - 'article' => "int", - 'name' => "str", + 'arsse_marks' => [ + 'columns' => [ + 'subscription' => "int", + 'article' => "int", + 'read' => "bool", + 'starred' => "bool", + 'modified' => "datetime", + 'note' => "str", + ], + 'rows' => [ + [1, 1,1,1,'2000-01-01 00:00:00',''], + [5, 19,1,0,'2016-01-01 00:00:00',''], + [5, 20,0,1,'2005-01-01 00:00:00',''], + [7, 20,1,0,'2010-01-01 00:00:00',''], + [8, 102,1,0,'2000-01-02 02:00:00','Note 2'], + [9, 103,0,1,'2000-01-03 03:00:00','Note 3'], + [9, 104,1,1,'2000-01-04 04:00:00','Note 4'], + [10,105,0,0,'2000-01-05 05:00:00',''], + [11, 19,0,0,'2017-01-01 00:00:00','ook'], + [11, 20,1,0,'2017-01-01 00:00:00','eek'], + [12, 3,0,1,'2017-01-01 00:00:00','ack'], + [12, 4,1,1,'2017-01-01 00:00:00','ach'], + [1, 2,0,0,'2010-01-01 00:00:00','Some Note'], + ] ], - 'rows' => [ - [19,"Fascinating"], - [19,"Logical"], - [20,"Interesting"], - [20,"Logical"], + 'arsse_categories' => [ // author-supplied categories + 'columns' => [ + 'article' => "int", + 'name' => "str", + ], + 'rows' => [ + [19,"Fascinating"], + [19,"Logical"], + [20,"Interesting"], + [20,"Logical"], + ], ], - ], - 'arsse_labels' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'name' => "str", + 'arsse_labels' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'name' => "str", + ], + 'rows' => [ + [1,"john.doe@example.com","Interesting"], + [2,"john.doe@example.com","Fascinating"], + [3,"jane.doe@example.com","Boring"], + [4,"john.doe@example.com","Lonely"], + ], ], - 'rows' => [ - [1,"john.doe@example.com","Interesting"], - [2,"john.doe@example.com","Fascinating"], - [3,"jane.doe@example.com","Boring"], - [4,"john.doe@example.com","Lonely"], + 'arsse_label_members' => [ + 'columns' => [ + 'label' => "int", + 'article' => "int", + 'subscription' => "int", + 'assigned' => "bool", + 'modified' => "datetime", + ], + 'rows' => [ + [1, 1,1,1,'2000-01-01 00:00:00'], + [2, 1,1,1,'2000-01-01 00:00:00'], + [1,19,5,1,'2000-01-01 00:00:00'], + [2,20,5,1,'2000-01-01 00:00:00'], + [1, 5,3,0,'2000-01-01 00:00:00'], + [2, 5,3,1,'2000-01-01 00:00:00'], + [4, 7,4,0,'2000-01-01 00:00:00'], + [4, 8,4,1,'2015-01-01 00:00:00'], + ], ], - ], - 'arsse_label_members' => [ - 'columns' => [ - 'label' => "int", - 'article' => "int", - 'subscription' => "int", - 'assigned' => "bool", - 'modified' => "datetime", + ]; + $this->matches = [ + [ + 'id' => 101, + 'url' => 'http://example.com/1', + 'title' => 'Article title 1', + 'subscription_title' => "Feed 11", + 'author' => '', + 'content' => '

Article content 1

', + 'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda', + 'published_date' => '2000-01-01 00:00:00', + 'edited_date' => '2000-01-01 00:00:01', + 'modified_date' => '2000-01-01 01:00:00', + 'unread' => 1, + 'starred' => 0, + 'edition' => 101, + 'subscription' => 8, + 'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207', + 'media_url' => null, + 'media_type' => null, + 'note' => "", ], - 'rows' => [ - [1, 1,1,1,'2000-01-01 00:00:00'], - [2, 1,1,1,'2000-01-01 00:00:00'], - [1,19,5,1,'2000-01-01 00:00:00'], - [2,20,5,1,'2000-01-01 00:00:00'], - [1, 5,3,0,'2000-01-01 00:00:00'], - [2, 5,3,1,'2000-01-01 00:00:00'], - [4, 7,4,0,'2000-01-01 00:00:00'], - [4, 8,4,1,'2015-01-01 00:00:00'], + [ + 'id' => 102, + 'url' => 'http://example.com/2', + 'title' => 'Article title 2', + 'subscription_title' => "Feed 11", + 'author' => '', + 'content' => '

Article content 2

', + 'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7', + 'published_date' => '2000-01-02 00:00:00', + 'edited_date' => '2000-01-02 00:00:02', + 'modified_date' => '2000-01-02 02:00:00', + 'unread' => 0, + 'starred' => 0, + 'edition' => 202, + 'subscription' => 8, + 'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e', + 'media_url' => "http://example.com/text", + 'media_type' => "text/plain", + 'note' => "Note 2", ], - ], - ]; - protected $matches = [ - [ - 'id' => 101, - 'url' => 'http://example.com/1', - 'title' => 'Article title 1', - 'subscription_title' => "Feed 11", - 'author' => '', - 'content' => '

Article content 1

', - 'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda', - 'published_date' => '2000-01-01 00:00:00', - 'edited_date' => '2000-01-01 00:00:01', - 'modified_date' => '2000-01-01 01:00:00', - 'unread' => 1, - 'starred' => 0, - 'edition' => 101, - 'subscription' => 8, - 'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207', - 'media_url' => null, - 'media_type' => null, - 'note' => "", - ], - [ - 'id' => 102, - 'url' => 'http://example.com/2', - 'title' => 'Article title 2', - 'subscription_title' => "Feed 11", - 'author' => '', - 'content' => '

Article content 2

', - 'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7', - 'published_date' => '2000-01-02 00:00:00', - 'edited_date' => '2000-01-02 00:00:02', - 'modified_date' => '2000-01-02 02:00:00', - 'unread' => 0, - 'starred' => 0, - 'edition' => 202, - 'subscription' => 8, - 'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e', - 'media_url' => "http://example.com/text", - 'media_type' => "text/plain", - 'note' => "Note 2", - ], - [ - 'id' => 103, - 'url' => 'http://example.com/3', - 'title' => 'Article title 3', - 'subscription_title' => "Subscription 9", - 'author' => '', - 'content' => '

Article content 3

', - 'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92', - 'published_date' => '2000-01-03 00:00:00', - 'edited_date' => '2000-01-03 00:00:03', - 'modified_date' => '2000-01-03 03:00:00', - 'unread' => 1, - 'starred' => 1, - 'edition' => 203, - 'subscription' => 9, - 'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b', - 'media_url' => "http://example.com/video", - 'media_type' => "video/webm", - 'note' => "Note 3", - ], - [ - 'id' => 104, - 'url' => 'http://example.com/4', - 'title' => 'Article title 4', - 'subscription_title' => "Subscription 9", - 'author' => '', - 'content' => '

Article content 4

', - 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', - 'published_date' => '2000-01-04 00:00:00', - 'edited_date' => '2000-01-04 00:00:04', - 'modified_date' => '2000-01-04 04:00:00', - 'unread' => 0, - 'starred' => 1, - 'edition' => 204, - 'subscription' => 9, - 'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', - 'media_url' => "http://example.com/image", - 'media_type' => "image/svg+xml", - 'note' => "Note 4", - ], - [ - 'id' => 105, - 'url' => 'http://example.com/5', - 'title' => 'Article title 5', - 'subscription_title' => "Feed 13", - 'author' => '', - 'content' => '

Article content 5

', - 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', - 'published_date' => '2000-01-05 00:00:00', - 'edited_date' => '2000-01-05 00:00:05', - 'modified_date' => '2000-01-05 05:00:00', - 'unread' => 1, - 'starred' => 0, - 'edition' => 305, - 'subscription' => 10, - 'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', - 'media_url' => "http://example.com/audio", - 'media_type' => "audio/ogg", - 'note' => "", - ], - ]; - protected $fields = [ - Database::LIST_MINIMAL => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - ], - Database::LIST_CONSERVATIVE => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", - ], - Database::LIST_TYPICAL => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", - "content", "media_url", "media_type", - ], - Database::LIST_FULL => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", - "content", "media_url", "media_type", - "note", - ], - ]; - - public function setUpSeries() { + [ + 'id' => 103, + 'url' => 'http://example.com/3', + 'title' => 'Article title 3', + 'subscription_title' => "Subscription 9", + 'author' => '', + 'content' => '

Article content 3

', + 'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92', + 'published_date' => '2000-01-03 00:00:00', + 'edited_date' => '2000-01-03 00:00:03', + 'modified_date' => '2000-01-03 03:00:00', + 'unread' => 1, + 'starred' => 1, + 'edition' => 203, + 'subscription' => 9, + 'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b', + 'media_url' => "http://example.com/video", + 'media_type' => "video/webm", + 'note' => "Note 3", + ], + [ + 'id' => 104, + 'url' => 'http://example.com/4', + 'title' => 'Article title 4', + 'subscription_title' => "Subscription 9", + 'author' => '', + 'content' => '

Article content 4

', + 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', + 'published_date' => '2000-01-04 00:00:00', + 'edited_date' => '2000-01-04 00:00:04', + 'modified_date' => '2000-01-04 04:00:00', + 'unread' => 0, + 'starred' => 1, + 'edition' => 204, + 'subscription' => 9, + 'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', + 'media_url' => "http://example.com/image", + 'media_type' => "image/svg+xml", + 'note' => "Note 4", + ], + [ + 'id' => 105, + 'url' => 'http://example.com/5', + 'title' => 'Article title 5', + 'subscription_title' => "Feed 13", + 'author' => '', + 'content' => '

Article content 5

', + 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', + 'published_date' => '2000-01-05 00:00:00', + 'edited_date' => '2000-01-05 00:00:05', + 'modified_date' => '2000-01-05 05:00:00', + 'unread' => 1, + 'starred' => 0, + 'edition' => 305, + 'subscription' => 10, + 'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', + 'media_url' => "http://example.com/audio", + 'media_type' => "audio/ogg", + 'note' => "", + ], + ]; + $this->fields = [ + Database::LIST_MINIMAL => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + ], + Database::LIST_CONSERVATIVE => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + ], + Database::LIST_TYPICAL => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + "content", "media_url", "media_type", + ], + Database::LIST_FULL => [ + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + "content", "media_url", "media_type", + "note", + ], + ]; $this->checkTables = ['arsse_marks' => ["subscription","article","read","starred","modified","note"],]; $this->user = "john.doe@example.net"; } - protected function compareIds(array $exp, Context $c) { - $ids = array_column($ids = Arsse::$db->articleList($this->user, $c)->getAll(), "id"); - sort($ids); - sort($exp); - $this->assertEquals($exp, $ids); + protected function tearDownSeriesArticle() { + unset($this->data, $this->matches, $this->fields, $this->checkTables, $this->user); } public function testListArticlesCheckingContext() { - $this->user = "john.doe@example.com"; + $compareIds = function(array $exp, Context $c) { + $ids = array_column($ids = Arsse::$db->articleList("john.doe@example.com", $c)->getAll(), "id"); + sort($ids); + sort($exp); + $this->assertEquals($exp, $ids); + }; // get all items for user $exp = [1,2,3,4,5,6,7,8,19,20]; - $this->compareIds($exp, new Context); - $this->compareIds($exp, (new Context)->articles(range(1, Database::LIMIT_ARTICLES * 3))); + $compareIds($exp, new Context); + $compareIds($exp, (new Context)->articles(range(1, Database::LIMIT_ARTICLES * 3))); // get items from a folder tree - $this->compareIds([5,6,7,8], (new Context)->folder(1)); + $compareIds([5,6,7,8], (new Context)->folder(1)); // get items from a leaf folder - $this->compareIds([7,8], (new Context)->folder(6)); + $compareIds([7,8], (new Context)->folder(6)); // get items from a non-leaf folder without descending - $this->compareIds([1,2,3,4], (new Context)->folderShallow(0)); - $this->compareIds([5,6], (new Context)->folderShallow(1)); + $compareIds([1,2,3,4], (new Context)->folderShallow(0)); + $compareIds([5,6], (new Context)->folderShallow(1)); // get items from a single subscription $exp = [19,20]; - $this->compareIds($exp, (new Context)->subscription(5)); + $compareIds($exp, (new Context)->subscription(5)); // get un/read items from a single subscription - $this->compareIds([20], (new Context)->subscription(5)->unread(true)); - $this->compareIds([19], (new Context)->subscription(5)->unread(false)); + $compareIds([20], (new Context)->subscription(5)->unread(true)); + $compareIds([19], (new Context)->subscription(5)->unread(false)); // get starred articles - $this->compareIds([1,20], (new Context)->starred(true)); - $this->compareIds([2,3,4,5,6,7,8,19], (new Context)->starred(false)); - $this->compareIds([1], (new Context)->starred(true)->unread(false)); - $this->compareIds([], (new Context)->starred(true)->unread(false)->subscription(5)); + $compareIds([1,20], (new Context)->starred(true)); + $compareIds([2,3,4,5,6,7,8,19], (new Context)->starred(false)); + $compareIds([1], (new Context)->starred(true)->unread(false)); + $compareIds([], (new Context)->starred(true)->unread(false)->subscription(5)); // get items relative to edition - $this->compareIds([19], (new Context)->subscription(5)->latestEdition(999)); - $this->compareIds([19], (new Context)->subscription(5)->latestEdition(19)); - $this->compareIds([20], (new Context)->subscription(5)->oldestEdition(999)); - $this->compareIds([20], (new Context)->subscription(5)->oldestEdition(1001)); + $compareIds([19], (new Context)->subscription(5)->latestEdition(999)); + $compareIds([19], (new Context)->subscription(5)->latestEdition(19)); + $compareIds([20], (new Context)->subscription(5)->oldestEdition(999)); + $compareIds([20], (new Context)->subscription(5)->oldestEdition(1001)); // get items relative to article ID - $this->compareIds([1,2,3], (new Context)->latestArticle(3)); - $this->compareIds([19,20], (new Context)->oldestArticle(19)); + $compareIds([1,2,3], (new Context)->latestArticle(3)); + $compareIds([19,20], (new Context)->oldestArticle(19)); // get items relative to (feed) modification date $exp = [2,4,6,8,20]; - $this->compareIds($exp, (new Context)->modifiedSince("2005-01-01T00:00:00Z")); - $this->compareIds($exp, (new Context)->modifiedSince("2010-01-01T00:00:00Z")); + $compareIds($exp, (new Context)->modifiedSince("2005-01-01T00:00:00Z")); + $compareIds($exp, (new Context)->modifiedSince("2010-01-01T00:00:00Z")); $exp = [1,3,5,7,19]; - $this->compareIds($exp, (new Context)->notModifiedSince("2005-01-01T00:00:00Z")); - $this->compareIds($exp, (new Context)->notModifiedSince("2000-01-01T00:00:00Z")); + $compareIds($exp, (new Context)->notModifiedSince("2005-01-01T00:00:00Z")); + $compareIds($exp, (new Context)->notModifiedSince("2000-01-01T00:00:00Z")); // get items relative to (user) modification date (both marks and labels apply) - $this->compareIds([8,19], (new Context)->markedSince("2014-01-01T00:00:00Z")); - $this->compareIds([2,4,6,8,19,20], (new Context)->markedSince("2010-01-01T00:00:00Z")); - $this->compareIds([1,2,3,4,5,6,7,20], (new Context)->notMarkedSince("2014-01-01T00:00:00Z")); - $this->compareIds([1,3,5,7], (new Context)->notMarkedSince("2005-01-01T00:00:00Z")); + $compareIds([8,19], (new Context)->markedSince("2014-01-01T00:00:00Z")); + $compareIds([2,4,6,8,19,20], (new Context)->markedSince("2010-01-01T00:00:00Z")); + $compareIds([1,2,3,4,5,6,7,20], (new Context)->notMarkedSince("2014-01-01T00:00:00Z")); + $compareIds([1,3,5,7], (new Context)->notMarkedSince("2005-01-01T00:00:00Z")); // paged results - $this->compareIds([1], (new Context)->limit(1)); - $this->compareIds([2], (new Context)->limit(1)->oldestEdition(1+1)); - $this->compareIds([3], (new Context)->limit(1)->oldestEdition(2+1)); - $this->compareIds([4,5], (new Context)->limit(2)->oldestEdition(3+1)); + $compareIds([1], (new Context)->limit(1)); + $compareIds([2], (new Context)->limit(1)->oldestEdition(1+1)); + $compareIds([3], (new Context)->limit(1)->oldestEdition(2+1)); + $compareIds([4,5], (new Context)->limit(2)->oldestEdition(3+1)); // reversed results - $this->compareIds([20], (new Context)->reverse(true)->limit(1)); - $this->compareIds([19], (new Context)->reverse(true)->limit(1)->latestEdition(1001-1)); - $this->compareIds([8], (new Context)->reverse(true)->limit(1)->latestEdition(19-1)); - $this->compareIds([7,6], (new Context)->reverse(true)->limit(2)->latestEdition(8-1)); + $compareIds([20], (new Context)->reverse(true)->limit(1)); + $compareIds([19], (new Context)->reverse(true)->limit(1)->latestEdition(1001-1)); + $compareIds([8], (new Context)->reverse(true)->limit(1)->latestEdition(19-1)); + $compareIds([7,6], (new Context)->reverse(true)->limit(2)->latestEdition(8-1)); // get articles by label ID - $this->compareIds([1,19], (new Context)->label(1)); - $this->compareIds([1,5,20], (new Context)->label(2)); + $compareIds([1,19], (new Context)->label(1)); + $compareIds([1,5,20], (new Context)->label(2)); // get articles by label name - $this->compareIds([1,19], (new Context)->labelName("Interesting")); - $this->compareIds([1,5,20], (new Context)->labelName("Fascinating")); + $compareIds([1,19], (new Context)->labelName("Interesting")); + $compareIds([1,5,20], (new Context)->labelName("Fascinating")); // get articles with any or no label - $this->compareIds([1,5,8,19,20], (new Context)->labelled(true)); - $this->compareIds([2,3,4,6,7], (new Context)->labelled(false)); + $compareIds([1,5,8,19,20], (new Context)->labelled(true)); + $compareIds([2,3,4,6,7], (new Context)->labelled(false)); // get a specific article or edition - $this->compareIds([20], (new Context)->article(20)); - $this->compareIds([20], (new Context)->edition(1001)); + $compareIds([20], (new Context)->article(20)); + $compareIds([20], (new Context)->edition(1001)); // get multiple specific articles or editions - $this->compareIds([1,20], (new Context)->articles([1,20,50])); - $this->compareIds([1,20], (new Context)->editions([1,1001,50])); + $compareIds([1,20], (new Context)->articles([1,20,50])); + $compareIds([1,20], (new Context)->editions([1,1001,50])); // get articles base on whether or not they have notes - $this->compareIds([1,3,4,5,6,7,8,19,20], (new Context)->annotated(false)); - $this->compareIds([2], (new Context)->annotated(true)); + $compareIds([1,3,4,5,6,7,8,19,20], (new Context)->annotated(false)); + $compareIds([2], (new Context)->annotated(true)); // get specific starred articles - $this->compareIds([1], (new Context)->articles([1,2,3])->starred(true)); - $this->compareIds([2,3], (new Context)->articles([1,2,3])->starred(false)); + $compareIds([1], (new Context)->articles([1,2,3])->starred(true)); + $compareIds([2,3], (new Context)->articles([1,2,3])->starred(false)); } public function testListArticlesOfAMissingFolder() { diff --git a/tests/cases/Db/SQLite3/TestDatabase.php b/tests/cases/Db/SQLite3/TestDatabase.php new file mode 100644 index 00000000..d40dd880 --- /dev/null +++ b/tests/cases/Db/SQLite3/TestDatabase.php @@ -0,0 +1,19 @@ + + * @covers \JKingWeb\Arsse\Misc\Query + */ +class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { + protected static $implementation = "SQLite 3"; + + protected function nextID(string $table): int { + return static::$drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue(); + } +} diff --git a/tests/cases/Db/SQLite3PDO/TestDatabase.php b/tests/cases/Db/SQLite3PDO/TestDatabase.php new file mode 100644 index 00000000..1dca8b79 --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestDatabase.php @@ -0,0 +1,19 @@ + + * @covers \JKingWeb\Arsse\Misc\Query + */ +class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { + protected static $implementation = "PDO SQLite 3"; + + protected function nextID(string $table): int { + return (int) static::$drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue(); + } +} diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php index d22a6185..4b1d950e 100644 --- a/tests/lib/DatabaseInformation.php +++ b/tests/lib/DatabaseInformation.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Test; use JKingWeb\Arsse\Arsse; +use JKingWeb\Arsse\Db\Driver; class DatabaseInformation { public $name; @@ -17,6 +18,8 @@ class DatabaseInformation { public $driverClass; public $stringOutput; public $interfaceConstructor; + public $truncateFunction; + public $razeFunction; protected static $data; @@ -50,6 +53,57 @@ class DatabaseInformation { } protected static function getData() { + $sqlite3TableList = function($db): array { + $listTables = "SELECT name from sqlite_master where type = 'table' and name like 'arsse_%'"; + if ($db instanceof Driver) { + $tables = $db->query($listTables)->getAll(); + $tables = sizeof($tables) ? array_column($tables, "name") : []; + } elseif ($db instanceof \PDO) { + $tables = $db->query($listTables)->fetchAll(\PDO::FETCH_ASSOC); + $tables = sizeof($tables) ? array_column($tables, "name") : []; + } else { + $tables = []; + $result = $db->query($listTables); + while ($r = $result->fetchArray(\SQLITE3_ASSOC)) { + $tables[] = $r['name']; + } + $result->finalize(); + } + return $tables; + }; + $sqlite3TruncateFunction = function($db, array $afterStatements = []) use ($sqlite3TableList) { + foreach ($sqlite3TableList($db) as $table) { + if ($table == "arsse_meta") { + $db->exec("DELETE FROM $table where key <> 'schema_version'"); + } else { + $db->exec("DELETE FROM $table"); + } + } + foreach ($afterStatements as $st) { + $db->exec($st); + } + }; + $sqlite3RazeFunction = function($db, array $afterStatements = []) use ($sqlite3TableList) { + $db->exec("PRAGMA foreign_keys=0"); + foreach ($sqlite3TableList($db) as $table) { + $db->exec("DROP TABLE IF EXISTS $table"); + } + $db->exec("PRAGMA user_version=0"); + $db->exec("PRAGMA foreign_keys=1"); + foreach ($afterStatements as $st) { + $db->exec($st); + } + }; + $pgObjectList = function($db): array { + $listObjects = "SELECT table_name as name, 'TABLE' as type from information_schema.tables where table_schema = current_schema() and table_name like 'arsse_%' union SELECT collation_name as name, 'COLLATION' as type from information_schema.collations where collation_schema = current_schema()"; + if ($db instanceof Driver) { + return $db->query($listObjects)->getAll(); + } elseif ($db instanceof \PDO) { + return $db->query($listObjects)->fetchAll(\PDO::FETCH_ASSOC); + } else { + throw \Exception("Native PostgreSQL interface not implemented"); + } + }; return [ 'SQLite 3' => [ 'pdo' => false, @@ -67,7 +121,8 @@ class DatabaseInformation { $d->enableExceptions(true); return $d; }, - + 'truncateFunction' => $sqlite3TruncateFunction, + 'razeFunction' => $sqlite3RazeFunction, ], 'PDO SQLite 3' => [ 'pdo' => true, @@ -85,6 +140,8 @@ class DatabaseInformation { return; } }, + 'truncateFunction' => $sqlite3TruncateFunction, + 'razeFunction' => $sqlite3RazeFunction, ], 'PDO PostgreSQL' => [ 'pdo' => true, @@ -105,6 +162,29 @@ class DatabaseInformation { } return $d; }, + 'truncateFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { + foreach ($objectList($db) as $obj) { + if ($obj['type'] != "TABLE") { + continue; + } elseif ($obj['name'] == "arsse_meta") { + $db->exec("DELETE FROM {$obj['name']} where key <> 'schema_version'"); + } else { + $db->exec("TRUNCATE TABLE {$obj['name']} restart identity cascade"); + } + } + foreach ($afterStatements as $st) { + $db->exec($st); + } + }, + 'razeFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { + foreach ($objectList($db) as $obj) { + $db->exec("DROP {$obj['type']} {$obj['name']} IF EXISTS cascade"); + + } + foreach ($afterStatements as $st) { + $db->exec($st); + } + }, ], ]; } From a75fad53cad588a8f3234ba7ea32ff08a377f2b9 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 25 Nov 2018 00:03:56 -0500 Subject: [PATCH 20/58] Adapt the rest of the test series --- tests/cases/Database/Base.php | 10 + tests/cases/Database/SeriesCleanup.php | 6 +- tests/cases/Database/SeriesFeed.php | 43 +- tests/cases/Database/SeriesFolder.php | 80 ++-- tests/cases/Database/SeriesLabel.php | 441 ++++++++++---------- tests/cases/Database/SeriesMeta.php | 33 +- tests/cases/Database/SeriesMiscellany.php | 9 + tests/cases/Database/SeriesSession.php | 8 +- tests/cases/Database/SeriesSubscription.php | 196 ++++----- tests/cases/Database/SeriesUser.php | 34 +- 10 files changed, 456 insertions(+), 404 deletions(-) diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php index 8efcf3bc..85582e74 100644 --- a/tests/cases/Database/Base.php +++ b/tests/cases/Database/Base.php @@ -16,7 +16,16 @@ use JKingWeb\Arsse\Test\DatabaseInformation; use Phake; abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ + use SeriesMiscellany; + use SeriesMeta; + use SeriesUser; + use SeriesSession; + use SeriesFolder; + use SeriesFeed; + use SeriesSubscription; use SeriesArticle; + use SeriesLabel; + use SeriesCleanup; /** @var \JKingWeb\Arsse\Test\DatabaseInformation */ protected static $dbInfo; @@ -62,6 +71,7 @@ abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ // get the name of the test's test series $this->series = $this->findTraitofTest($this->getName()); static::clearData(); + static::setConf(); if (strlen(static::$failureReason)) { $this->markTestSkipped(static::$failureReason); } diff --git a/tests/cases/Database/SeriesCleanup.php b/tests/cases/Database/SeriesCleanup.php index fcb2393d..b888686c 100644 --- a/tests/cases/Database/SeriesCleanup.php +++ b/tests/cases/Database/SeriesCleanup.php @@ -10,7 +10,7 @@ use JKingWeb\Arsse\Arsse; use Phake; trait SeriesCleanup { - public function setUpSeries() { + protected function setUpSeriesCleanup() { // set up the configuration Arsse::$conf->import([ 'userSessionTimeout' => "PT1H", @@ -135,6 +135,10 @@ trait SeriesCleanup { ]; } + protected function tearDownSeriesCleanup() { + unset($this->data); + } + public function testCleanUpOrphanedFeeds() { Arsse::$db->feedCleanup(); $now = gmdate("Y-m-d H:i:s"); diff --git a/tests/cases/Database/SeriesFeed.php b/tests/cases/Database/SeriesFeed.php index 00475aef..c7cd2a4d 100644 --- a/tests/cases/Database/SeriesFeed.php +++ b/tests/cases/Database/SeriesFeed.php @@ -12,26 +12,7 @@ use JKingWeb\Arsse\Feed\Exception as FeedException; use Phake; trait SeriesFeed { - protected $matches = [ - [ - 'id' => 4, - 'edited' => '2000-01-04 00:00:00', - 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', - 'url_title_hash' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8', - 'url_content_hash' => 'f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3', - 'title_content_hash' => 'ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', - ], - [ - 'id' => 5, - 'edited' => '2000-01-05 00:00:00', - 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', - 'url_title_hash' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022', - 'url_content_hash' => '834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900', - 'title_content_hash' => '43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', - ], - ]; - - public function setUpSeries() { + protected function setUpSeriesFeed() { // set up the test data $past = gmdate("Y-m-d H:i:s", strtotime("now - 1 minute")); $future = gmdate("Y-m-d H:i:s", strtotime("now + 1 minute")); @@ -163,6 +144,28 @@ trait SeriesFeed { ] ], ]; + $this->matches = [ + [ + 'id' => 4, + 'edited' => '2000-01-04 00:00:00', + 'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180', + 'url_title_hash' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8', + 'url_content_hash' => 'f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3', + 'title_content_hash' => 'ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9', + ], + [ + 'id' => 5, + 'edited' => '2000-01-05 00:00:00', + 'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41', + 'url_title_hash' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022', + 'url_content_hash' => '834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900', + 'title_content_hash' => '43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba', + ], + ]; + } + + protected function tearDownSeriesFeed() { + unset($this->data, $this->matches); } public function testListLatestItems() { diff --git a/tests/cases/Database/SeriesFolder.php b/tests/cases/Database/SeriesFolder.php index 6199d232..7265f07a 100644 --- a/tests/cases/Database/SeriesFolder.php +++ b/tests/cases/Database/SeriesFolder.php @@ -10,45 +10,51 @@ use JKingWeb\Arsse\Arsse; use Phake; trait SeriesFolder { - protected $data = [ - 'arsse_users' => [ - 'columns' => [ - 'id' => 'str', - 'password' => 'str', - 'name' => 'str', + protected function setUpSeriesFolder() { + $this->data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ], ], - 'rows' => [ - ["jane.doe@example.com", "", "Jane Doe"], - ["john.doe@example.com", "", "John Doe"], + 'arsse_folders' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'parent' => "int", + 'name' => "str", + ], + /* Layout translates to: + Jane + Politics + John + Technology + Software + Politics + Rocketry + Politics + */ + 'rows' => [ + [1, "john.doe@example.com", null, "Technology"], + [2, "john.doe@example.com", 1, "Software"], + [3, "john.doe@example.com", 1, "Rocketry"], + [4, "jane.doe@example.com", null, "Politics"], + [5, "john.doe@example.com", null, "Politics"], + [6, "john.doe@example.com", 2, "Politics"], + ] ], - ], - 'arsse_folders' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'parent' => "int", - 'name' => "str", - ], - /* Layout translates to: - Jane - Politics - John - Technology - Software - Politics - Rocketry - Politics - */ - 'rows' => [ - [1, "john.doe@example.com", null, "Technology"], - [2, "john.doe@example.com", 1, "Software"], - [3, "john.doe@example.com", 1, "Rocketry"], - [4, "jane.doe@example.com", null, "Politics"], - [5, "john.doe@example.com", null, "Politics"], - [6, "john.doe@example.com", 2, "Politics"], - ] - ], - ]; + ]; + } + + protected function tearDownSeriesFolder() { + unset($this->data); + } public function testAddARootFolder() { $user = "john.doe@example.com"; diff --git a/tests/cases/Database/SeriesLabel.php b/tests/cases/Database/SeriesLabel.php index 062d23e9..8347ce53 100644 --- a/tests/cases/Database/SeriesLabel.php +++ b/tests/cases/Database/SeriesLabel.php @@ -12,241 +12,244 @@ use JKingWeb\Arsse\Misc\Date; use Phake; trait SeriesLabel { - protected $data = [ - 'arsse_users' => [ - 'columns' => [ - 'id' => 'str', - 'password' => 'str', - 'name' => 'str', + protected function setUpSeriesLabel() { + $this->data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ["john.doe@example.org", "", "John Doe"], + ["john.doe@example.net", "", "John Doe"], + ], ], - 'rows' => [ - ["jane.doe@example.com", "", "Jane Doe"], - ["john.doe@example.com", "", "John Doe"], - ["john.doe@example.org", "", "John Doe"], - ["john.doe@example.net", "", "John Doe"], + 'arsse_folders' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'parent' => "int", + 'name' => "str", + ], + 'rows' => [ + [1, "john.doe@example.com", null, "Technology"], + [2, "john.doe@example.com", 1, "Software"], + [3, "john.doe@example.com", 1, "Rocketry"], + [4, "jane.doe@example.com", null, "Politics"], + [5, "john.doe@example.com", null, "Politics"], + [6, "john.doe@example.com", 2, "Politics"], + [7, "john.doe@example.net", null, "Technology"], + [8, "john.doe@example.net", 7, "Software"], + [9, "john.doe@example.net", null, "Politics"], + ] ], - ], - 'arsse_folders' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'parent' => "int", - 'name' => "str", + 'arsse_feeds' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + ], + 'rows' => [ + [1,"http://example.com/1"], + [2,"http://example.com/2"], + [3,"http://example.com/3"], + [4,"http://example.com/4"], + [5,"http://example.com/5"], + [6,"http://example.com/6"], + [7,"http://example.com/7"], + [8,"http://example.com/8"], + [9,"http://example.com/9"], + [10,"http://example.com/10"], + [11,"http://example.com/11"], + [12,"http://example.com/12"], + [13,"http://example.com/13"], + ] ], - 'rows' => [ - [1, "john.doe@example.com", null, "Technology"], - [2, "john.doe@example.com", 1, "Software"], - [3, "john.doe@example.com", 1, "Rocketry"], - [4, "jane.doe@example.com", null, "Politics"], - [5, "john.doe@example.com", null, "Politics"], - [6, "john.doe@example.com", 2, "Politics"], - [7, "john.doe@example.net", null, "Technology"], - [8, "john.doe@example.net", 7, "Software"], - [9, "john.doe@example.net", null, "Politics"], - ] - ], - 'arsse_feeds' => [ - 'columns' => [ - 'id' => "int", - 'url' => "str", + 'arsse_subscriptions' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'feed' => "int", + 'folder' => "int", + ], + 'rows' => [ + [1,"john.doe@example.com",1,null], + [2,"john.doe@example.com",2,null], + [3,"john.doe@example.com",3,1], + [4,"john.doe@example.com",4,6], + [5,"john.doe@example.com",10,5], + [6,"jane.doe@example.com",1,null], + [7,"jane.doe@example.com",10,null], + [8,"john.doe@example.org",11,null], + [9,"john.doe@example.org",12,null], + [10,"john.doe@example.org",13,null], + [11,"john.doe@example.net",10,null], + [12,"john.doe@example.net",2,9], + [13,"john.doe@example.net",3,8], + [14,"john.doe@example.net",4,7], + ] ], - 'rows' => [ - [1,"http://example.com/1"], - [2,"http://example.com/2"], - [3,"http://example.com/3"], - [4,"http://example.com/4"], - [5,"http://example.com/5"], - [6,"http://example.com/6"], - [7,"http://example.com/7"], - [8,"http://example.com/8"], - [9,"http://example.com/9"], - [10,"http://example.com/10"], - [11,"http://example.com/11"], - [12,"http://example.com/12"], - [13,"http://example.com/13"], - ] - ], - 'arsse_subscriptions' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'feed' => "int", - 'folder' => "int", + 'arsse_articles' => [ + 'columns' => [ + 'id' => "int", + 'feed' => "int", + 'url' => "str", + 'title' => "str", + 'author' => "str", + 'published' => "datetime", + 'edited' => "datetime", + 'content' => "str", + 'guid' => "str", + 'url_title_hash' => "str", + 'url_content_hash' => "str", + 'title_content_hash' => "str", + 'modified' => "datetime", + ], + 'rows' => [ + [1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], + [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], + [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','

Article content 1

','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'], + [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','

Article content 2

','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'], + [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'], + [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','

Article content 4

','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'], + [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','

Article content 5

','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'], + ] ], - 'rows' => [ - [1,"john.doe@example.com",1,null], - [2,"john.doe@example.com",2,null], - [3,"john.doe@example.com",3,1], - [4,"john.doe@example.com",4,6], - [5,"john.doe@example.com",10,5], - [6,"jane.doe@example.com",1,null], - [7,"jane.doe@example.com",10,null], - [8,"john.doe@example.org",11,null], - [9,"john.doe@example.org",12,null], - [10,"john.doe@example.org",13,null], - [11,"john.doe@example.net",10,null], - [12,"john.doe@example.net",2,9], - [13,"john.doe@example.net",3,8], - [14,"john.doe@example.net",4,7], - ] - ], - 'arsse_articles' => [ - 'columns' => [ - 'id' => "int", - 'feed' => "int", - 'url' => "str", - 'title' => "str", - 'author' => "str", - 'published' => "datetime", - 'edited' => "datetime", - 'content' => "str", - 'guid' => "str", - 'url_title_hash' => "str", - 'url_content_hash' => "str", - 'title_content_hash' => "str", - 'modified' => "datetime", - ], - 'rows' => [ - [1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"], - [20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], - [101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','

Article content 1

','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'], - [102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','

Article content 2

','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'], - [103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','

Article content 3

','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'], - [104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','

Article content 4

','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'], - [105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','

Article content 5

','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'], - ] - ], - 'arsse_enclosures' => [ - 'columns' => [ - 'article' => "int", - 'url' => "str", - 'type' => "str", - ], - 'rows' => [ - [102,"http://example.com/text","text/plain"], - [103,"http://example.com/video","video/webm"], - [104,"http://example.com/image","image/svg+xml"], - [105,"http://example.com/audio","audio/ogg"], + 'arsse_enclosures' => [ + 'columns' => [ + 'article' => "int", + 'url' => "str", + 'type' => "str", + ], + 'rows' => [ + [102,"http://example.com/text","text/plain"], + [103,"http://example.com/video","video/webm"], + [104,"http://example.com/image","image/svg+xml"], + [105,"http://example.com/audio","audio/ogg"], - ] - ], - 'arsse_editions' => [ - 'columns' => [ - 'id' => "int", - 'article' => "int", + ] ], - 'rows' => [ - [1,1], - [2,2], - [3,3], - [4,4], - [5,5], - [6,6], - [7,7], - [8,8], - [9,9], - [10,10], - [11,11], - [12,12], - [13,13], - [14,14], - [15,15], - [16,16], - [17,17], - [18,18], - [19,19], - [20,20], - [101,101], - [102,102], - [103,103], - [104,104], - [105,105], - [202,102], - [203,103], - [204,104], - [205,105], - [305,105], - [1001,20], - ] - ], - 'arsse_marks' => [ - 'columns' => [ - 'subscription' => "int", - 'article' => "int", - 'read' => "bool", - 'starred' => "bool", - 'modified' => "datetime" + 'arsse_editions' => [ + 'columns' => [ + 'id' => "int", + 'article' => "int", + ], + 'rows' => [ + [1,1], + [2,2], + [3,3], + [4,4], + [5,5], + [6,6], + [7,7], + [8,8], + [9,9], + [10,10], + [11,11], + [12,12], + [13,13], + [14,14], + [15,15], + [16,16], + [17,17], + [18,18], + [19,19], + [20,20], + [101,101], + [102,102], + [103,103], + [104,104], + [105,105], + [202,102], + [203,103], + [204,104], + [205,105], + [305,105], + [1001,20], + ] ], - 'rows' => [ - [1, 1,1,1,'2000-01-01 00:00:00'], - [5, 19,1,0,'2000-01-01 00:00:00'], - [5, 20,0,1,'2010-01-01 00:00:00'], - [7, 20,1,0,'2010-01-01 00:00:00'], - [8, 102,1,0,'2000-01-02 02:00:00'], - [9, 103,0,1,'2000-01-03 03:00:00'], - [9, 104,1,1,'2000-01-04 04:00:00'], - [10,105,0,0,'2000-01-05 05:00:00'], - [11, 19,0,0,'2017-01-01 00:00:00'], - [11, 20,1,0,'2017-01-01 00:00:00'], - [12, 3,0,1,'2017-01-01 00:00:00'], - [12, 4,1,1,'2017-01-01 00:00:00'], - ] - ], - 'arsse_labels' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'name' => "str", + 'arsse_marks' => [ + 'columns' => [ + 'subscription' => "int", + 'article' => "int", + 'read' => "bool", + 'starred' => "bool", + 'modified' => "datetime" + ], + 'rows' => [ + [1, 1,1,1,'2000-01-01 00:00:00'], + [5, 19,1,0,'2000-01-01 00:00:00'], + [5, 20,0,1,'2010-01-01 00:00:00'], + [7, 20,1,0,'2010-01-01 00:00:00'], + [8, 102,1,0,'2000-01-02 02:00:00'], + [9, 103,0,1,'2000-01-03 03:00:00'], + [9, 104,1,1,'2000-01-04 04:00:00'], + [10,105,0,0,'2000-01-05 05:00:00'], + [11, 19,0,0,'2017-01-01 00:00:00'], + [11, 20,1,0,'2017-01-01 00:00:00'], + [12, 3,0,1,'2017-01-01 00:00:00'], + [12, 4,1,1,'2017-01-01 00:00:00'], + ] ], - 'rows' => [ - [1,"john.doe@example.com","Interesting"], - [2,"john.doe@example.com","Fascinating"], - [3,"jane.doe@example.com","Boring"], - [4,"john.doe@example.com","Lonely"], + 'arsse_labels' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'name' => "str", + ], + 'rows' => [ + [1,"john.doe@example.com","Interesting"], + [2,"john.doe@example.com","Fascinating"], + [3,"jane.doe@example.com","Boring"], + [4,"john.doe@example.com","Lonely"], + ], ], - ], - 'arsse_label_members' => [ - 'columns' => [ - 'label' => "int", - 'article' => "int", - 'subscription' => "int", - 'assigned' => "bool", + 'arsse_label_members' => [ + 'columns' => [ + 'label' => "int", + 'article' => "int", + 'subscription' => "int", + 'assigned' => "bool", + ], + 'rows' => [ + [1, 1,1,1], + [2, 1,1,1], + [1,19,5,1], + [2,20,5,1], + [1, 5,3,0], + [2, 5,3,1], + ], ], - 'rows' => [ - [1, 1,1,1], - [2, 1,1,1], - [1,19,5,1], - [2,20,5,1], - [1, 5,3,0], - [2, 5,3,1], - ], - ], - ]; - - public function setUpSeries() { + ]; $this->checkLabels = ['arsse_labels' => ["id","owner","name"]]; $this->checkMembers = ['arsse_label_members' => ["label","article","subscription","assigned"]]; $this->user = "john.doe@example.com"; } + protected function tearDownSeriesLabel() { + unset($this->data, $this->checkLabels, $this->checkMembers, $this->user); + } + public function testAddALabel() { $user = "john.doe@example.com"; $labelID = $this->nextID("arsse_labels"); diff --git a/tests/cases/Database/SeriesMeta.php b/tests/cases/Database/SeriesMeta.php index 467c8e0b..538700a3 100644 --- a/tests/cases/Database/SeriesMeta.php +++ b/tests/cases/Database/SeriesMeta.php @@ -10,26 +10,29 @@ use JKingWeb\Arsse\Test\Database; use JKingWeb\Arsse\Arsse; trait SeriesMeta { - protected $dataBare = [ - 'arsse_meta' => [ - 'columns' => [ - 'key' => 'str', - 'value' => 'str', + protected function setUpSeriesMeta() { + $dataBare = [ + 'arsse_meta' => [ + 'columns' => [ + 'key' => 'str', + 'value' => 'str', + ], + 'rows' => [ + //['schema_version', "".\JKingWeb\Arsse\Database::SCHEMA_VERSION], + ['album',"A Farewell to Kings"], + ], ], - 'rows' => [ - //['schema_version', "".\JKingWeb\Arsse\Database::SCHEMA_VERSION], - ['album',"A Farewell to Kings"], - ], - ], - ]; - - public function setUpSeries() { + ]; // the schema_version key is a special case, and to avoid jumping through hoops for every test we deal with it now - $this->data = $this->dataBare; + $this->data = $dataBare; // as far as tests are concerned the schema version is part of the expectations primed into the database array_unshift($this->data['arsse_meta']['rows'], ['schema_version', "".Database::SCHEMA_VERSION]); // but it's already been inserted by the driver, so we prime without it - $this->primeDatabase($this->dataBare); + $this->primeDatabase($dataBare); + } + + protected function tearDownSeriesMeta() { + unset($this->data); } public function testAddANewValue() { diff --git a/tests/cases/Database/SeriesMiscellany.php b/tests/cases/Database/SeriesMiscellany.php index c5e0eb97..50e4ed59 100644 --- a/tests/cases/Database/SeriesMiscellany.php +++ b/tests/cases/Database/SeriesMiscellany.php @@ -10,6 +10,15 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Database; trait SeriesMiscellany { + protected function setUpSeriesMiscellany() { + static::setConf([ + 'dbDriver' => static::$dbInfo->driverClass, + ]); + } + + protected function tearDownSeriesMiscellany() { + } + public function testListDrivers() { $exp = [ 'JKingWeb\\Arsse\\Db\\SQLite3\\Driver' => Arsse::$lang->msg("Driver.Db.SQLite3.Name"), diff --git a/tests/cases/Database/SeriesSession.php b/tests/cases/Database/SeriesSession.php index e605b868..c9867420 100644 --- a/tests/cases/Database/SeriesSession.php +++ b/tests/cases/Database/SeriesSession.php @@ -11,9 +11,9 @@ use JKingWeb\Arsse\Misc\Date; use Phake; trait SeriesSession { - public function setUpSeries() { + protected function setUpSeriesSession() { // set up the configuration - Arsse::$conf->import([ + static::setConf([ 'userSessionTimeout' => "PT1H", 'userSessionLifetime' => "PT24H", ]); @@ -51,6 +51,10 @@ trait SeriesSession { ]; } + protected function tearDownSeriesSession() { + unset($this->data); + } + public function testResumeAValidSession() { $exp1 = [ 'id' => "80fa94c1a11f11e78667001e673b2560", diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index 7e3a3941..ebaeea3e 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -12,112 +12,116 @@ use JKingWeb\Arsse\Feed\Exception as FeedException; use Phake; trait SeriesSubscription { - protected $data = [ - 'arsse_users' => [ - 'columns' => [ - 'id' => 'str', - 'password' => 'str', - 'name' => 'str', - ], - 'rows' => [ - ["jane.doe@example.com", "", "Jane Doe"], - ["john.doe@example.com", "", "John Doe"], - ], - ], - 'arsse_folders' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'parent' => "int", - 'name' => "str", - ], - 'rows' => [ - [1, "john.doe@example.com", null, "Technology"], - [2, "john.doe@example.com", 1, "Software"], - [3, "john.doe@example.com", 1, "Rocketry"], - [4, "jane.doe@example.com", null, "Politics"], - [5, "john.doe@example.com", null, "Politics"], - [6, "john.doe@example.com", 2, "Politics"], - ] - ], - 'arsse_feeds' => [ - 'columns' => [ - 'id' => "int", - 'url' => "str", - 'title' => "str", - 'username' => "str", - 'password' => "str", - 'next_fetch' => "datetime", - 'favicon' => "str", - ], - 'rows' => [] // filled in the series setup - ], - 'arsse_subscriptions' => [ - 'columns' => [ - 'id' => "int", - 'owner' => "str", - 'feed' => "int", - 'title' => "str", - 'folder' => "int", - 'pinned' => "bool", - 'order_type' => "int", - ], - 'rows' => [ - [1,"john.doe@example.com",2,null,null,1,2], - [2,"jane.doe@example.com",2,null,null,0,0], - [3,"john.doe@example.com",3,"Ook",2,0,1], - ] - ], - 'arsse_articles' => [ - 'columns' => [ - 'id' => "int", - 'feed' => "int", - 'url_title_hash' => "str", - 'url_content_hash' => "str", - 'title_content_hash' => "str", - ], - 'rows' => [ - [1,2,"","",""], - [2,2,"","",""], - [3,2,"","",""], - [4,2,"","",""], - [5,2,"","",""], - [6,3,"","",""], - [7,3,"","",""], - [8,3,"","",""], - ] - ], - 'arsse_marks' => [ - 'columns' => [ - 'article' => "int", - 'subscription' => "int", - 'read' => "bool", - 'starred' => "bool", - ], - 'rows' => [ - [1,2,1,0], - [2,2,1,0], - [3,2,1,0], - [4,2,1,0], - [5,2,1,0], - [1,1,1,0], - [7,3,1,0], - [8,3,0,0], - ] - ], - ]; - public function setUpSeries() { + public function setUpSeriesSubscription() { + $this->data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + ], + 'rows' => [ + ["jane.doe@example.com", "", "Jane Doe"], + ["john.doe@example.com", "", "John Doe"], + ], + ], + 'arsse_folders' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'parent' => "int", + 'name' => "str", + ], + 'rows' => [ + [1, "john.doe@example.com", null, "Technology"], + [2, "john.doe@example.com", 1, "Software"], + [3, "john.doe@example.com", 1, "Rocketry"], + [4, "jane.doe@example.com", null, "Politics"], + [5, "john.doe@example.com", null, "Politics"], + [6, "john.doe@example.com", 2, "Politics"], + ] + ], + 'arsse_feeds' => [ + 'columns' => [ + 'id' => "int", + 'url' => "str", + 'title' => "str", + 'username' => "str", + 'password' => "str", + 'next_fetch' => "datetime", + 'favicon' => "str", + ], + 'rows' => [] // filled in the series setup + ], + 'arsse_subscriptions' => [ + 'columns' => [ + 'id' => "int", + 'owner' => "str", + 'feed' => "int", + 'title' => "str", + 'folder' => "int", + 'pinned' => "bool", + 'order_type' => "int", + ], + 'rows' => [ + [1,"john.doe@example.com",2,null,null,1,2], + [2,"jane.doe@example.com",2,null,null,0,0], + [3,"john.doe@example.com",3,"Ook",2,0,1], + ] + ], + 'arsse_articles' => [ + 'columns' => [ + 'id' => "int", + 'feed' => "int", + 'url_title_hash' => "str", + 'url_content_hash' => "str", + 'title_content_hash' => "str", + ], + 'rows' => [ + [1,2,"","",""], + [2,2,"","",""], + [3,2,"","",""], + [4,2,"","",""], + [5,2,"","",""], + [6,3,"","",""], + [7,3,"","",""], + [8,3,"","",""], + ] + ], + 'arsse_marks' => [ + 'columns' => [ + 'article' => "int", + 'subscription' => "int", + 'read' => "bool", + 'starred' => "bool", + ], + 'rows' => [ + [1,2,1,0], + [2,2,1,0], + [3,2,1,0], + [4,2,1,0], + [5,2,1,0], + [1,1,1,0], + [7,3,1,0], + [8,3,0,0], + ] + ], + ]; $this->data['arsse_feeds']['rows'] = [ [1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),''], [2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),'http://example.com/favicon.ico'], [3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),''], ]; // initialize a partial mock of the Database object to later manipulate the feedUpdate method - Arsse::$db = Phake::partialMock(Database::class, $this->drv); + Arsse::$db = Phake::partialMock(Database::class, static::$drv); $this->user = "john.doe@example.com"; } + protected function tearDownSeriesSubscription() { + unset($this->data, $this->user); + } + public function testAddASubscriptionToAnExistingFeed() { $url = "http://example.com/feed1"; $subID = $this->nextID("arsse_subscriptions"); diff --git a/tests/cases/Database/SeriesUser.php b/tests/cases/Database/SeriesUser.php index 402dec6f..49c324b9 100644 --- a/tests/cases/Database/SeriesUser.php +++ b/tests/cases/Database/SeriesUser.php @@ -11,21 +11,27 @@ use JKingWeb\Arsse\User\Driver as UserDriver; use Phake; trait SeriesUser { - protected $data = [ - 'arsse_users' => [ - 'columns' => [ - 'id' => 'str', - 'password' => 'str', - 'name' => 'str', - 'rights' => 'int', + protected function setUpSeriesUser() { + $this->data = [ + 'arsse_users' => [ + 'columns' => [ + 'id' => 'str', + 'password' => 'str', + 'name' => 'str', + 'rights' => 'int', + ], + 'rows' => [ + ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', "Hard Lip Herbert", 100], // password is hash of "secret" + ["jane.doe@example.com", "", "Jane Doe", 0], + ["john.doe@example.com", "", "John Doe", 0], + ], ], - 'rows' => [ - ["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', "Hard Lip Herbert", 100], // password is hash of "secret" - ["jane.doe@example.com", "", "Jane Doe", 0], - ["john.doe@example.com", "", "John Doe", 0], - ], - ], - ]; + ]; + } + + protected function tearDownSeriesUser() { + unset($this->data); + } public function testCheckThatAUserExists() { $this->assertTrue(Arsse::$db->userExists("jane.doe@example.com")); From 925560d4bab2b01dfc6c694bb377a6d285ef25e3 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Sun, 25 Nov 2018 00:06:20 -0500 Subject: [PATCH 21/58] Cleanup --- tests/lib/Database/DriverSQLite3.php | 24 ---- tests/lib/Database/DriverSQLite3PDO.php | 24 ---- tests/lib/Database/Setup.php | 175 ------------------------ 3 files changed, 223 deletions(-) delete mode 100644 tests/lib/Database/DriverSQLite3.php delete mode 100644 tests/lib/Database/DriverSQLite3PDO.php delete mode 100644 tests/lib/Database/Setup.php diff --git a/tests/lib/Database/DriverSQLite3.php b/tests/lib/Database/DriverSQLite3.php deleted file mode 100644 index 1b76eea7..00000000 --- a/tests/lib/Database/DriverSQLite3.php +++ /dev/null @@ -1,24 +0,0 @@ -markTestSkipped("SQLite extension not loaded"); - } - Arsse::$conf->dbSQLite3File = ":memory:"; - $this->drv = new Driver(); - } - - public function nextID(string $table): int { - return $this->drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue(); - } -} diff --git a/tests/lib/Database/DriverSQLite3PDO.php b/tests/lib/Database/DriverSQLite3PDO.php deleted file mode 100644 index 9c52bd87..00000000 --- a/tests/lib/Database/DriverSQLite3PDO.php +++ /dev/null @@ -1,24 +0,0 @@ -markTestSkipped("PDO-SQLite extension not loaded"); - } - Arsse::$conf->dbSQLite3File = ":memory:"; - $this->drv = new PDODriver(); - } - - public function nextID(string $table): int { - return (int) $this->drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue(); - } -} diff --git a/tests/lib/Database/Setup.php b/tests/lib/Database/Setup.php deleted file mode 100644 index b3c749d1..00000000 --- a/tests/lib/Database/Setup.php +++ /dev/null @@ -1,175 +0,0 @@ -setUpDriver(); - // create the database interface with the suitable driver - Arsse::$db = new Database($this->drv); - Arsse::$db->driverSchemaUpdate(); - // create a mock user manager - Arsse::$user = Phake::mock(User::class); - Phake::when(Arsse::$user)->authorize->thenReturn(true); - // call the additional setup method if it exists - if (method_exists($this, "setUpSeries")) { - $this->setUpSeries(); - } - // prime the database with series data if it hasn't already been done - if (!$this->primed && isset($this->data)) { - $this->primeDatabase($this->data); - } - } - - public function tearDown() { - // call the additional teardiwn method if it exists - if (method_exists($this, "tearDownSeries")) { - $this->tearDownSeries(); - } - // clean up - $this->primed = false; - $this->drv = null; - self::clearData(); - } - - public function primeDatabase(array $data, \JKingWeb\Arsse\Db\Driver $drv = null): bool { - $drv = $drv ?? $this->drv; - $tr = $drv->begin(); - foreach ($data as $table => $info) { - $cols = implode(",", array_keys($info['columns'])); - $bindings = array_values($info['columns']); - $params = implode(",", array_fill(0, sizeof($info['columns']), "?")); - $s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings); - foreach ($info['rows'] as $row) { - $s->runArray($row); - } - } - $tr->commit(); - $this->primed = true; - return true; - } - - public function primeFile(string $file, array $data = null): bool { - $data = $data ?? $this->data; - $primed = $this->primed; - $drv = new \JKingWeb\Arsse\Db\SQLite3\Driver($file); - $drv->schemaUpdate(\JKingWeb\Arsse\Database::SCHEMA_VERSION); - $this->primeDatabase($data, $drv); - $this->primed = $primed; - return true; - } - - public function compareExpectations(array $expected): bool { - foreach ($expected as $table => $info) { - $cols = implode(",", array_keys($info['columns'])); - $types = $info['columns']; - $data = $this->drv->prepare("SELECT $cols from $table")->run()->getAll(); - $cols = array_keys($info['columns']); - foreach ($info['rows'] as $index => $row) { - $this->assertCount(sizeof($cols), $row, "The number of values for array index $index does not match the number of fields"); - $row = array_combine($cols, $row); - foreach ($data as $index => $test) { - foreach ($test as $col => $value) { - switch ($types[$col]) { - case "datetime": - $test[$col] = $this->approximateTime($row[$col], $value); - break; - case "int": - $test[$col] = ValueInfo::normalize($value, ValueInfo::T_INT | ValueInfo::M_DROP | valueInfo::M_NULL); - break; - case "float": - $test[$col] = ValueInfo::normalize($value, ValueInfo::T_FLOAT | ValueInfo::M_DROP | valueInfo::M_NULL); - break; - case "bool": - $test[$col] = (int) ValueInfo::normalize($value, ValueInfo::T_BOOL | ValueInfo::M_DROP | valueInfo::M_NULL); - break; - } - } - if ($row===$test) { - $data[$index] = $test; - break; - } - } - $this->assertContains($row, $data, "Table $table does not contain record at array index $index."); - $found = array_search($row, $data, true); - unset($data[$found]); - } - $this->assertSame([], $data); - } - return true; - } - - public function primeExpectations(array $source, array $tableSpecs = null): array { - $out = []; - foreach ($tableSpecs as $table => $columns) { - // make sure the source has the table we want - $this->assertArrayHasKey($table, $source, "Source for expectations does not contain requested table $table."); - $out[$table] = [ - 'columns' => [], - 'rows' => array_fill(0, sizeof($source[$table]['rows']), []), - ]; - // make sure the source has all the columns we want for the table - $cols = array_flip($columns); - $cols = array_intersect_key($cols, $source[$table]['columns']); - $this->assertSame(array_keys($cols), $columns, "Source for table $table does not contain all requested columns"); - // get a map of source value offsets and keys - $targets = array_flip(array_keys($source[$table]['columns'])); - foreach ($cols as $key => $order) { - // fill the column-spec - $out[$table]['columns'][$key] = $source[$table]['columns'][$key]; - foreach ($source[$table]['rows'] as $index => $row) { - // fill each row column-wise with re-ordered values - $out[$table]['rows'][$index][$order] = $row[$targets[$key]]; - } - } - } - return $out; - } - - public function assertResult(array $expected, Result $data) { - $data = $data->getAll(); - $this->assertCount(sizeof($expected), $data, "Number of result rows (".sizeof($data).") differs from number of expected rows (".sizeof($expected).")"); - if (sizeof($expected)) { - // make sure the expectations are consistent - foreach ($expected as $exp) { - if (!isset($keys)) { - $keys = $exp; - continue; - } - $this->assertSame(array_keys($keys), array_keys($exp), "Result set expectations are irregular"); - } - // filter the result set to contain just the desired keys (we don't care if the result has extra keys) - $rows = []; - foreach ($data as $row) { - $rows[] = array_intersect_key($row, $keys); - } - // compare the result set to the expectations - foreach ($rows as $row) { - $this->assertContains($row, $expected, "Result set contains unexpected record."); - $found = array_search($row, $expected); - unset($expected[$found]); - } - $this->assertArraySubset($expected, [], "Expectations not in result set."); - } - } -} From 8a4920203610e96b595c2b31e50d90dc4ab7c969 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 27 Nov 2018 14:26:33 -0500 Subject: [PATCH 22/58] Use common cleanup code for all database-related tests --- tests/cases/Db/BaseDriver.php | 78 ++++++++++++------ tests/cases/Db/BaseResult.php | 36 +++++--- tests/cases/Db/BaseStatement.php | 38 ++++++--- tests/cases/Db/PostgreSQL/TestDatabase.php | 19 +++++ tests/cases/Db/PostgreSQL/TestDriver.php | 9 +- tests/cases/Db/PostgreSQL/TestStatement.php | 13 +-- tests/cases/Db/SQLite3/TestDriver.php | 91 +++++---------------- tests/cases/Db/SQLite3/TestResult.php | 21 ++--- tests/cases/Db/SQLite3/TestStatement.php | 15 ++-- tests/cases/Db/SQLite3PDO/TestDriver.php | 21 ++--- tests/cases/Db/SQLite3PDO/TestStatement.php | 13 +-- tests/cases/Db/TestResultPDO.php | 27 ++---- tests/lib/DatabaseInformation.php | 28 ++++++- 13 files changed, 203 insertions(+), 206 deletions(-) create mode 100644 tests/cases/Db/PostgreSQL/TestDatabase.php diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index 46244080..e918fa99 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -11,51 +11,69 @@ use JKingWeb\Arsse\Db\Result; use JKingWeb\Arsse\Test\DatabaseInformation; abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { + protected static $dbInfo; + protected static $interface; protected $drv; - protected $interface; protected $create; protected $lock; protected $setVersion; - protected $conf = [ + protected static $conf = [ 'dbTimeoutExec' => 0.5, 'dbSQLite3Timeout' => 0, + //'dbSQLite3File' => "(temporary file)", ]; + + public static function setUpBeforeClass() { + // establish a clean baseline + static::clearData(); + static::$dbInfo = new DatabaseInformation(static::$implementation); + static::setConf(static::$conf); + static::$interface = (static::$dbInfo->interfaceConstructor)(); + } public function setUp() { self::clearData(); - self::setConf($this->conf); - $info = new DatabaseInformation($this->implementation); - $this->interface = ($info->interfaceConstructor)(); - if (!$this->interface) { - $this->markTestSkipped("$this->implementation database driver not available"); + self::setConf(static::$conf); + if (!static::$interface) { + $this->markTestSkipped(static::$implementation." database driver not available"); } - $this->drv = new $info->driverClass; - $this->exec("DROP TABLE IF EXISTS arsse_test"); - $this->exec("DROP TABLE IF EXISTS arsse_meta"); - $this->exec("CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)"); - $this->exec("INSERT INTO arsse_meta(key,value) values('schema_version','0')"); + // 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')", + ]); + // construct a fresh driver for each test + $this->drv = new static::$dbInfo->driverClass; } public function tearDown() { - self::clearData(); + // deconstruct the driver unset($this->drv); - try { - $this->exec("ROLLBACK"); - } catch(\Throwable $e) { + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); } - $this->exec("DROP TABLE IF EXISTS arsse_meta"); - $this->exec("DROP TABLE IF EXISTS arsse_test"); + self::clearData(); } - protected function exec(string $q): bool { + public static function tearDownAfterClass() { + static::$implementation = null; + static::$dbInfo = null; + self::clearData(); + } + + protected function exec($q): bool { // PDO implementation - $this->interface->exec($q); + $q = (!is_array($q)) ? [$q] : $q; + foreach ($q as $query) { + static::$interface->exec((string) $query); + } return true; } protected function query(string $q) { // PDO implementation - return $this->interface->query($q)->fetchColumn(); + return static::$interface->query($q)->fetchColumn(); } # TESTS @@ -87,7 +105,8 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->exec($this->create); $this->exec($this->lock); $this->assertException("general", "Db", "ExceptionTimeout"); - $this->drv->exec($this->lock); + $lock = is_array($this->lock) ? implode("; ",$this->lock) : $this->lock; + $this->drv->exec($lock); } public function testExecConstraintViolation() { @@ -115,7 +134,8 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->exec($this->create); $this->exec($this->lock); $this->assertException("general", "Db", "ExceptionTimeout"); - $this->drv->exec($this->lock); + $lock = is_array($this->lock) ? implode("; ",$this->lock) : $this->lock; + $this->drv->exec($lock); } public function testQueryConstraintViolation() { @@ -342,12 +362,20 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame(1, $this->drv->schemaVersion()); $this->drv->exec(str_replace("#", "2", $this->setVersion)); $this->assertSame(2, $this->drv->schemaVersion()); + // SQLite is unaffected by the removal of the metadata table; other backends are + // in neither case should a query for the schema version produce an error, however + $this->exec("DROP TABLE IF EXISTS arsse_meta"); + $exp = (static::$dbInfo->backend == "SQLite 3") ? 2 : 0; + $this->assertSame($exp, $this->drv->schemaVersion()); } public function testLockTheDatabase() { + // PostgreSQL doesn't actually lock the whole database, only the metadata table + // normally the application will first query this table to ensure the schema version is correct, + // so the effect is usually the same $this->drv->savepointCreate(true); $this->assertException(); - $this->exec($this->create); + $this->exec(str_replace("#", "3", $this->setVersion)); } public function testUnlockTheDatabase() { @@ -355,6 +383,6 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->drv->savepointRelease(); $this->drv->savepointCreate(true); $this->drv->savepointUndo(); - $this->assertTrue($this->exec($this->create)); + $this->assertTrue($this->exec(str_replace("#", "3", $this->setVersion))); } } diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index 96dae042..de1ddf22 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -10,29 +10,45 @@ use JKingWeb\Arsse\Db\Result; use JKingWeb\Arsse\Test\DatabaseInformation; abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { + protected static $dbInfo; + protected static $interface; protected $resultClass; protected $stringOutput; - protected $interface; - abstract protected function exec(string $q); abstract protected function makeResult(string $q): array; + public static function setUpBeforeClass() { + // establish a clean baseline + static::clearData(); + static::$dbInfo = new DatabaseInformation(static::$implementation); + static::setConf(); + static::$interface = (static::$dbInfo->interfaceConstructor)(); + } + public function setUp() { self::clearData(); self::setConf(); - $info = new DatabaseInformation($this->implementation); - $this->interface = ($info->interfaceConstructor)(); - if (!$this->interface) { - $this->markTestSkipped("$this->implementation database driver not available"); + if (!static::$interface) { + $this->markTestSkipped(static::$implementation." database driver not available"); } - $this->resultClass = $info->resultClass; - $this->stringOutput = $info->stringOutput; - $this->exec("DROP TABLE IF EXISTS arsse_meta"); + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + $this->resultClass = static::$dbInfo->resultClass; + $this->stringOutput = static::$dbInfo->stringOutput; } public function tearDown() { + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } + self::clearData(); + } + + public static function tearDownAfterClass() { + static::$implementation = null; + static::$dbInfo = null; self::clearData(); - $this->exec("DROP TABLE IF EXISTS arsse_meta"); } public function testConstructResult() { diff --git a/tests/cases/Db/BaseStatement.php b/tests/cases/Db/BaseStatement.php index 4369f7ac..c7da01ee 100644 --- a/tests/cases/Db/BaseStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -10,29 +10,45 @@ use JKingWeb\Arsse\Db\Statement; use JKingWeb\Arsse\Test\DatabaseInformation; abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { + protected static $dbInfo; + protected static $interface; protected $statementClass; protected $stringOutput; - protected $interface; - abstract protected function exec(string $q); abstract protected function makeStatement(string $q, array $types = []): array; abstract protected function decorateTypeSyntax(string $value, string $type): string; + public static function setUpBeforeClass() { + // establish a clean baseline + static::clearData(); + static::$dbInfo = new DatabaseInformation(static::$implementation); + static::setConf(); + static::$interface = (static::$dbInfo->interfaceConstructor)(); + } + public function setUp() { self::clearData(); self::setConf(); - $info = new DatabaseInformation($this->implementation); - $this->interface = ($info->interfaceConstructor)(); - if (!$this->interface) { - $this->markTestSkipped("$this->implementation database driver not available"); + if (!static::$interface) { + $this->markTestSkipped(static::$implementation." database driver not available"); } - $this->statementClass = $info->statementClass; - $this->stringOutput = $info->stringOutput; - $this->exec("DROP TABLE IF EXISTS arsse_meta"); + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + $this->statementClass = static::$dbInfo->statementClass; + $this->stringOutput = static::$dbInfo->stringOutput; } public function tearDown() { - $this->exec("DROP TABLE IF EXISTS arsse_meta"); + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } + self::clearData(); + } + + public static function tearDownAfterClass() { + static::$implementation = null; + static::$dbInfo = null; self::clearData(); } @@ -56,7 +72,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideBinaryBindings */ public function testHandleBinaryData($value, string $type, string $exp) { - if (in_array($this->implementation, ["PostgreSQL", "PDO PostgreSQL"])) { + if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) { $this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown"); } if ($exp=="null") { diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php new file mode 100644 index 00000000..5ba3d04d --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -0,0 +1,19 @@ + + * @covers \JKingWeb\Arsse\Misc\Query + */ +class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { + protected static $implementation = "PDO PostgreSQL"; + + protected function nextID(string $table): int { + return static::$drv->query("SELECT select cast(last_value as bigint) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue(); + } +} diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php index 497488f7..63e73c6b 100644 --- a/tests/cases/Db/PostgreSQL/TestDriver.php +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -11,13 +11,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; * @covers \JKingWeb\Arsse\Db\PDODriver * @covers \JKingWeb\Arsse\Db\PDOError */ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { - protected $implementation = "PDO PostgreSQL"; + protected static $implementation = "PDO PostgreSQL"; protected $create = "CREATE TABLE arsse_test(id bigserial primary key)"; - protected $lock = "BEGIN; LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT"; + protected $lock = ["BEGIN", "LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT"]; protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'"; - - public function tearDown() { - parent::tearDown(); - unset($this->interface); - } } diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php index 2c8586fc..cfa42b47 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -10,19 +10,10 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; * @covers \JKingWeb\Arsse\Db\PDOStatement * @covers \JKingWeb\Arsse\Db\PDOError */ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { - protected $implementation = "PDO PostgreSQL"; - - public function tearDown() { - parent::tearDown(); - unset($this->interface); - } - - protected function exec(string $q) { - $this->interface->exec($q); - } + protected static $implementation = "PDO PostgreSQL"; protected function makeStatement(string $q, array $types = []): array { - return [$this->interface, $this->interface->prepare($q), $types]; + return [static::$interface, static::$interface->prepare($q), $types]; } protected function decorateTypeSyntax(string $value, string $type): string { diff --git a/tests/cases/Db/SQLite3/TestDriver.php b/tests/cases/Db/SQLite3/TestDriver.php index df802106..9b2348a6 100644 --- a/tests/cases/Db/SQLite3/TestDriver.php +++ b/tests/cases/Db/SQLite3/TestDriver.php @@ -10,92 +10,39 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3; * @covers \JKingWeb\Arsse\Db\SQLite3\Driver * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { - protected $implementation = "SQLite 3"; + protected static $implementation = "SQLite 3"; protected $create = "CREATE TABLE arsse_test(id integer primary key)"; protected $lock = "BEGIN EXCLUSIVE TRANSACTION"; protected $setVersion = "PRAGMA user_version=#"; protected static $file; public static function setUpBeforeClass() { - self::$file = tempnam(sys_get_temp_dir(), 'ook'); + // create a temporary database file rather than using a memory database + // some tests require one connection to block another, so a memory database is not suitable + static::$file = tempnam(sys_get_temp_dir(), 'ook'); + static::$conf['dbSQLite3File'] = static::$file; + parent::setUpBeforeclass(); } public static function tearDownAfterClass() { - @unlink(self::$file); - self::$file = null; - } - - public function setUp() { - $this->conf['dbSQLite3File'] = self::$file; - parent::setUp(); - $this->exec("PRAGMA user_version=0"); + if (static::$interface) { + static::$interface->close(); + } + parent::tearDownAfterClass(); + @unlink(static::$file); + static::$file = null; } - public function tearDown() { - parent::tearDown(); - $this->exec("PRAGMA user_version=0"); - $this->interface->close(); - unset($this->interface); - } - - protected function exec(string $q): bool { - $this->interface->exec($q); + protected function exec($q): bool { + // SQLite's implementation coincidentally matches PDO's, but we reproduce it here for correctness' sake + $q = (!is_array($q)) ? [$q] : $q; + foreach ($q as $query) { + static::$interface->exec((string) $query); + } return true; } protected function query(string $q) { - return $this->interface->querySingle($q); - } - - public function provideDrivers() { - self::clearData(); - self::setConf([ - 'dbTimeoutExec' => 0.5, - 'dbSQLite3Timeout' => 0, - 'dbSQLite3File' => tempnam(sys_get_temp_dir(), 'ook'), - ]); - $i = $this->provideDbInterfaces(); - $d = $this->provideDbDrivers(); - $pdoExec = function (string $q) { - $this->interface->exec($q); - return true; - }; - $pdoQuery = function (string $q) { - return $this->interface->query($q)->fetchColumn(); - }; - return [ - 'SQLite 3' => [ - $i['SQLite 3']['interface'], - $d['SQLite 3'], - "CREATE TABLE arsse_test(id integer primary key)", - "BEGIN EXCLUSIVE TRANSACTION", - "PRAGMA user_version=#", - function (string $q) { - $this->interface->exec($q); - return true; - }, - function (string $q) { - return $this->interface->querySingle($q); - }, - ], - 'PDO SQLite 3' => [ - $i['PDO SQLite 3']['interface'], - $d['PDO SQLite 3'], - "CREATE TABLE arsse_test(id integer primary key)", - "BEGIN EXCLUSIVE TRANSACTION", - "PRAGMA user_version=#", - $pdoExec, - $pdoQuery, - ], - 'PDO PostgreSQL' => [ - $i['PDO PostgreSQL']['interface'], - $d['PDO PostgreSQL'], - "CREATE TABLE arsse_test(id bigserial primary key)", - "BEGIN; LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT", - "UPDATE arsse_meta set value = '#' where key = 'schema_version'", - $pdoExec, - $pdoQuery, - ], - ]; + return static::$interface->querySingle($q); } } diff --git a/tests/cases/Db/SQLite3/TestResult.php b/tests/cases/Db/SQLite3/TestResult.php index 2f655e7e..4109b8f1 100644 --- a/tests/cases/Db/SQLite3/TestResult.php +++ b/tests/cases/Db/SQLite3/TestResult.php @@ -12,22 +12,19 @@ use JKingWeb\Arsse\Test\DatabaseInformation; * @covers \JKingWeb\Arsse\Db\SQLite3\Result */ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { - protected $implementation = "SQLite 3"; + protected static $implementation = "SQLite 3"; - public function tearDown() { - parent::tearDown(); - $this->interface->close(); - unset($this->interface); - } - - protected function exec(string $q) { - $this->interface->exec($q); + public static function tearDownAfterClass() { + if (static::$interface) { + static::$interface->close(); + } + parent::tearDownAfterClass(); } protected function makeResult(string $q): array { - $set = $this->interface->query($q); - $rows = $this->interface->changes(); - $id = $this->interface->lastInsertRowID(); + $set = static::$interface->query($q); + $rows = static::$interface->changes(); + $id = static::$interface->lastInsertRowID(); return [$set, [$rows, $id]]; } } diff --git a/tests/cases/Db/SQLite3/TestStatement.php b/tests/cases/Db/SQLite3/TestStatement.php index fc06dbd0..1684e832 100644 --- a/tests/cases/Db/SQLite3/TestStatement.php +++ b/tests/cases/Db/SQLite3/TestStatement.php @@ -10,20 +10,15 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3; * @covers \JKingWeb\Arsse\Db\SQLite3\Statement * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { - protected $implementation = "SQLite 3"; + protected static $implementation = "SQLite 3"; - public function tearDown() { - parent::tearDown(); - $this->interface->close(); - unset($this->interface); - } - - protected function exec(string $q) { - $this->interface->exec($q); + public static function tearDownAfterClass() { + static::$interface->close(); + parent::tearDownAfterClass(); } protected function makeStatement(string $q, array $types = []): array { - return [$this->interface, $this->interface->prepare($q), $types]; + return [static::$interface, static::$interface->prepare($q), $types]; } protected function decorateTypeSyntax(string $value, string $type): string { diff --git a/tests/cases/Db/SQLite3PDO/TestDriver.php b/tests/cases/Db/SQLite3PDO/TestDriver.php index 73ae773e..475d5756 100644 --- a/tests/cases/Db/SQLite3PDO/TestDriver.php +++ b/tests/cases/Db/SQLite3PDO/TestDriver.php @@ -11,30 +11,23 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO; * @covers \JKingWeb\Arsse\Db\PDODriver * @covers \JKingWeb\Arsse\Db\PDOError */ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { - protected $implementation = "PDO SQLite 3"; + protected static $implementation = "PDO SQLite 3"; protected $create = "CREATE TABLE arsse_test(id integer primary key)"; protected $lock = "BEGIN EXCLUSIVE TRANSACTION"; protected $setVersion = "PRAGMA user_version=#"; protected static $file; public static function setUpBeforeClass() { - self::$file = tempnam(sys_get_temp_dir(), 'ook'); + // create a temporary database file rather than using a memory database + // some tests require one connection to block another, so a memory database is not suitable + static::$file = tempnam(sys_get_temp_dir(), 'ook'); + static::$conf['dbSQLite3File'] = static::$file; + parent::setUpBeforeclass(); } public static function tearDownAfterClass() { + parent::tearDownAfterClass(); @unlink(self::$file); self::$file = null; } - - public function setUp() { - $this->conf['dbSQLite3File'] = self::$file; - parent::setUp(); - $this->exec("PRAGMA user_version=0"); - } - - public function tearDown() { - parent::tearDown(); - $this->exec("PRAGMA user_version=0"); - unset($this->interface); - } } diff --git a/tests/cases/Db/SQLite3PDO/TestStatement.php b/tests/cases/Db/SQLite3PDO/TestStatement.php index 74f05f19..9e2e06c6 100644 --- a/tests/cases/Db/SQLite3PDO/TestStatement.php +++ b/tests/cases/Db/SQLite3PDO/TestStatement.php @@ -10,19 +10,10 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO; * @covers \JKingWeb\Arsse\Db\PDOStatement * @covers \JKingWeb\Arsse\Db\PDOError */ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { - protected $implementation = "PDO SQLite 3"; - - public function tearDown() { - parent::tearDown(); - unset($this->interface); - } - - protected function exec(string $q) { - $this->interface->exec($q); - } + protected static $implementation = "PDO SQLite 3"; protected function makeStatement(string $q, array $types = []): array { - return [$this->interface, $this->interface->prepare($q), $types]; + return [static::$interface, static::$interface->prepare($q), $types]; } protected function decorateTypeSyntax(string $value, string $type): string { diff --git a/tests/cases/Db/TestResultPDO.php b/tests/cases/Db/TestResultPDO.php index f4d2eec7..f1792f84 100644 --- a/tests/cases/Db/TestResultPDO.php +++ b/tests/cases/Db/TestResultPDO.php @@ -12,41 +12,30 @@ use JKingWeb\Arsse\Test\DatabaseInformation; * @covers \JKingWeb\Arsse\Db\PDOResult */ class TestResultPDO extends \JKingWeb\Arsse\TestCase\Db\BaseResult { - protected static $firstAvailableDriver; + protected static $implementation; public static function setUpBeforeClass() { self::setConf(); // we only need to test one PDO implementation (they all use the same result class), so we find the first usable one $drivers = DatabaseInformation::listPDO(); - self::$firstAvailableDriver = $drivers[0]; + self::$implementation = $drivers[0]; foreach ($drivers as $driver) { $info = new DatabaseInformation($driver); $interface = ($info->interfaceConstructor)(); if ($interface) { - self::$firstAvailableDriver = $driver; + self::$implementation = $driver; break; } } - } - - public function setUp() { - $this->implementation = self::$firstAvailableDriver; - parent::setUp(); - } - - public function tearDown() { - parent::tearDown(); - unset($this->interface); - } - - protected function exec(string $q) { - $this->interface->exec($q); + unset($interface); + unset($info); + parent::setUpBeforeClass(); } protected function makeResult(string $q): array { - $set = $this->interface->query($q); + $set = static::$interface->query($q); $rows = $set->rowCount(); - $id = $this->interface->lastInsertID(); + $id = static::$interface->lastInsertID(); return [$set, [$rows, $id]]; } } diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php index 4b1d950e..0ec4d1d2 100644 --- a/tests/lib/DatabaseInformation.php +++ b/tests/lib/DatabaseInformation.php @@ -59,7 +59,12 @@ class DatabaseInformation { $tables = $db->query($listTables)->getAll(); $tables = sizeof($tables) ? array_column($tables, "name") : []; } elseif ($db instanceof \PDO) { - $tables = $db->query($listTables)->fetchAll(\PDO::FETCH_ASSOC); + retry: + try { + $tables = $db->query($listTables)->fetchAll(\PDO::FETCH_ASSOC); + } catch (\PDOException $e) { + goto retry; + } $tables = sizeof($tables) ? array_column($tables, "name") : []; } else { $tables = []; @@ -72,6 +77,11 @@ class DatabaseInformation { return $tables; }; $sqlite3TruncateFunction = function($db, array $afterStatements = []) use ($sqlite3TableList) { + // rollback any pending transaction + try { + $db->exec("ROLLBACK"); + } catch(\Throwable $e) { + } foreach ($sqlite3TableList($db) as $table) { if ($table == "arsse_meta") { $db->exec("DELETE FROM $table where key <> 'schema_version'"); @@ -84,6 +94,11 @@ class DatabaseInformation { } }; $sqlite3RazeFunction = function($db, array $afterStatements = []) use ($sqlite3TableList) { + // rollback any pending transaction + try { + $db->exec("ROLLBACK"); + } catch(\Throwable $e) { + } $db->exec("PRAGMA foreign_keys=0"); foreach ($sqlite3TableList($db) as $table) { $db->exec("DROP TABLE IF EXISTS $table"); @@ -163,7 +178,12 @@ class DatabaseInformation { return $d; }, 'truncateFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { - foreach ($objectList($db) as $obj) { + // rollback any pending transaction + try { + $db->exec("ROLLBACK"); + } catch(\Throwable $e) { + } + foreach ($pgObjectList($db) as $obj) { if ($obj['type'] != "TABLE") { continue; } elseif ($obj['name'] == "arsse_meta") { @@ -177,8 +197,8 @@ class DatabaseInformation { } }, 'razeFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { - foreach ($objectList($db) as $obj) { - $db->exec("DROP {$obj['type']} {$obj['name']} IF EXISTS cascade"); + foreach ($pgObjectList($db) as $obj) { + $db->exec("DROP {$obj['type']} IF EXISTS {$obj['name']} cascade"); } foreach ($afterStatements as $st) { From 1414f8979caef0eb817b3295864ca6d0a2cbc196 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 27 Nov 2018 17:16:00 -0500 Subject: [PATCH 23/58] Fix savepoint handling and locking in PostgreSQL driver --- lib/Db/AbstractDriver.php | 13 ++++++++++++ lib/Db/PostgreSQL/Driver.php | 27 ++++++++++++++++-------- tests/cases/Db/BaseDriver.php | 2 +- tests/cases/Db/PostgreSQL/TestDriver.php | 10 ++++++++- tests/lib/DatabaseInformation.php | 6 +++++- tests/phpunit.xml | 2 +- 6 files changed, 47 insertions(+), 13 deletions(-) diff --git a/lib/Db/AbstractDriver.php b/lib/Db/AbstractDriver.php index 45b9476b..814159e4 100644 --- a/lib/Db/AbstractDriver.php +++ b/lib/Db/AbstractDriver.php @@ -78,50 +78,63 @@ abstract class AbstractDriver implements Driver { } public function savepointCreate(bool $lock = false): int { + // if no transaction is active and a lock was requested, lock the database using a backend-specific routine if ($lock && !$this->transDepth) { $this->lock(); $this->locked = true; } + // 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 return $this->transDepth; } public function savepointRelease(int $index = null): bool { + // assume the most recent savepoint if none was specified $index = $index ?? $this->transDepth; 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); $this->transStatus[$index] = self::TR_COMMIT; + // for any later pending savepoints, set their state to implicitly committed $a = $index; while (++$a && $a <= $this->transDepth) { if ($this->transStatus[$a] <= self::TR_PEND) { $this->transStatus[$a] = self::TR_PEND_COMMIT; } } + // return success $out = true; break; case self::TR_PEND_COMMIT: + // set the state to explicitly committed $this->transStatus[$index] = self::TR_COMMIT; $out = true; break; case self::TR_PEND_ROLLBACK: + // set the state to explicitly committed $this->transStatus[$index] = self::TR_COMMIT; $out = false; break; case self::TR_COMMIT: case self::TR_ROLLBACK: //@codeCoverageIgnore + // savepoint has already been released or rolled back; this is an error throw new Exception("savepointStale", ['action' => "commit", 'index' => $index]); default: throw new Exception("savepointStatusUnknown", $this->transStatus[$index]); // @codeCoverageIgnore } if ($index==$this->transDepth) { + // if we've released the topmost savepoint, clean up all prior savepoints which have already been explicitly committed (or rolled back), if any while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) { array_pop($this->transStatus); $this->transDepth--; } } + // if no savepoints are pending and the database was locked, unlock it if (!$this->transDepth && $this->locked) { $this->unlock(); $this->locked = false; diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 5e9f6372..be8e40e4 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -13,6 +13,8 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionTimeout; class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { + protected $transStart = 0; + public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) { // check to make sure required extension is loaded if (!static::requirementsMet()) { @@ -104,37 +106,44 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } public function savepointCreate(bool $lock = false): int { - if (!$this->transDepth) { + if (!$this->transStart) { $this->exec("BEGIN TRANSACTION"); + $this->transStart = parent::savepointCreate($lock); + return $this->transStart; + } else { + return parent::savepointCreate($lock); } - return parent::savepointCreate($lock); } public function savepointRelease(int $index = null): bool { - $out = parent::savepointUndo($index); - if ($out && !$this->transDepth) { - $this->exec("COMMIT TRANSACTION"); + $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 ($out && !$this->transDepth) { - $this->exec("ROLLBACK TRANSACTION"); + if ($index == $this->transStart) { + $this->exec("ROLLBACK"); + $this->transStart = 0; } return $out; } protected function lock(): bool { - if ($this->schemaVersion()) { + if ($this->query("SELECT count(*) from information_schema.tables where table_schema = current_schema() and table_name = 'arsse_meta'")->getValue()) { $this->exec("LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"); } return true; } protected function unlock(bool $rollback = false): bool { - $this->exec((!$rollback) ? "COMMIT" : "ROLLBACK"); + // do nothing; transaction is committed or rolled back later return true; } diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index e918fa99..87f2d173 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -375,7 +375,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { // so the effect is usually the same $this->drv->savepointCreate(true); $this->assertException(); - $this->exec(str_replace("#", "3", $this->setVersion)); + $this->exec($this->lock); } public function testUnlockTheDatabase() { diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php index 63e73c6b..ae4c4b2d 100644 --- a/tests/cases/Db/PostgreSQL/TestDriver.php +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -13,6 +13,14 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { protected static $implementation = "PDO PostgreSQL"; protected $create = "CREATE TABLE arsse_test(id bigserial primary key)"; - protected $lock = ["BEGIN", "LOCK TABLE arsse_test IN EXCLUSIVE MODE NOWAIT"]; + protected $lock = ["BEGIN", "LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"]; protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'"; + + public function tearDown() { + try { + $this->drv->exec("ROLLBACK"); + } catch (\Throwable $e) { + } + parent::tearDown(); + } } diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php index 0ec4d1d2..01cc444f 100644 --- a/tests/lib/DatabaseInformation.php +++ b/tests/lib/DatabaseInformation.php @@ -197,9 +197,13 @@ class DatabaseInformation { } }, 'razeFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { + // rollback any pending transaction + try { + $db->exec("ROLLBACK"); + } catch(\Throwable $e) { + } foreach ($pgObjectList($db) as $obj) { $db->exec("DROP {$obj['type']} IF EXISTS {$obj['name']} cascade"); - } foreach ($afterStatements as $st) { $db->exec($st); diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 343f381f..06b3d6ad 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -60,7 +60,7 @@ cases/Db/PostgreSQL/TestStatement.php cases/Db/PostgreSQL/TestCreation.php - + cases/Db/PostgreSQL/TestDriver.php
From 93af38143685bc1662e5e23b1cb6ab77eb4baecf Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 27 Nov 2018 17:39:39 -0500 Subject: [PATCH 24/58] Test setting of schema name --- lib/Db/PostgreSQL/Driver.php | 15 +++++++++------ tests/lib/AbstractTest.php | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index be8e40e4..d70801e8 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -76,7 +76,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { "SET statement_timeout = '$timeout'", ]; if (strlen($schema) > 0) { - $out[] = 'SET search_path = \'"'.str_replace('"', '""', $schema).'", "$user", public\''; + $schema = '"'.str_replace('"', '""', $schema).'"'; + $out[] = "SET search_path = $schema, public"; } return $out; } @@ -92,11 +93,6 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } } - - public static function driverName(): string { - return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name"); - } - public static function schemaID(): string { return "PostgreSQL"; } @@ -150,11 +146,18 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function __destruct() { } + /** @codeCoverageIgnore */ + public static function driverName(): string { + return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name"); + } + + /** @codeCoverageIgnore */ public static function requirementsMet(): bool { // stub: native interface is not yet supported return false; } + /** @codeCoverageIgnore */ protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { // stub: native interface is not yet supported throw new \Exception; diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index addcc79c..3e9af64f 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -47,6 +47,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { 'dbPostgreSQLUser' => "arsse_test", 'dbPostgreSQLPass' => "arsse_test", 'dbPostgreSQLDb' => "arsse_test", + 'dbPostgreSQLSchema' => "arsse_test", ]; Arsse::$conf = ($force ? null : Arsse::$conf) ?? (new Conf)->import($defaults)->import($conf); } From d0db784b2216eba4d2a14f720606d3c1fab3346e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 27 Nov 2018 17:50:38 -0500 Subject: [PATCH 25/58] PostgreSQL schema tweak --- sql/PostgreSQL/1.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql/PostgreSQL/1.sql b/sql/PostgreSQL/1.sql index 086c7e35..73a0c1ff 100644 --- a/sql/PostgreSQL/1.sql +++ b/sql/PostgreSQL/1.sql @@ -8,7 +8,7 @@ create table arsse_sessions ( id text primary key, created timestamp(0) with time zone not null default CURRENT_TIMESTAMP, expires timestamp(0) with time zone not null, - user text not null references arsse_users(id) on delete cascade on update cascade + "user" text not null references arsse_users(id) on delete cascade on update cascade ); create table arsse_labels ( From 8dfedd30ef5d8ae444288a4bc5e320b68ae0552e Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 28 Nov 2018 10:46:23 -0500 Subject: [PATCH 26/58] Test PostgreSQL schema upgrade This was in fact buggy due to the schema version check causing an error --- lib/Db/AbstractDriver.php | 8 -- lib/Db/PostgreSQL/Driver.php | 8 ++ tests/cases/Db/BaseDriver.php | 5 + tests/cases/Db/BaseUpdate.php | 136 +++++++++++++++++++++++ tests/cases/Db/PostgreSQL/TestUpdate.php | 16 +++ tests/cases/Db/SQLite3/TestUpdate.php | 110 +----------------- tests/cases/Db/SQLite3PDO/TestUpdate.php | 111 +----------------- tests/phpunit.xml | 2 +- 8 files changed, 174 insertions(+), 222 deletions(-) create mode 100644 tests/cases/Db/BaseUpdate.php create mode 100644 tests/cases/Db/PostgreSQL/TestUpdate.php diff --git a/lib/Db/AbstractDriver.php b/lib/Db/AbstractDriver.php index 814159e4..6dbfb0a5 100644 --- a/lib/Db/AbstractDriver.php +++ b/lib/Db/AbstractDriver.php @@ -17,14 +17,6 @@ abstract class AbstractDriver implements Driver { abstract protected function unlock(bool $rollback = false): bool; abstract protected function getError(): string; - public function schemaVersion(): int { - try { - return (int) $this->query("SELECT value from arsse_meta where key = 'schema_version'")->getValue(); - } catch (Exception $e) { - return 0; - } - } - public function schemaUpdate(int $to, string $basePath = null): bool { $ver = $this->schemaVersion(); if (!Arsse::$conf->dbAutoUpdate) { diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index d70801e8..5b243857 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -101,6 +101,14 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return $this->query("SELECT pg_encoding_to_char(encoding) from pg_database where datname = current_database()")->getValue() == "UTF8"; } + public function schemaVersion(): int { + if ($this->query("SELECT count(*) from information_schema.tables where table_name = 'arsse_meta' and table_schema = current_schema()")->getValue()) { + return (int) $this->query("SELECT value from arsse_meta where key = 'schema_version'")->getValue(); + } else { + return 0; + } + } + public function savepointCreate(bool $lock = false): int { if (!$this->transStart) { $this->exec("BEGIN TRANSACTION"); diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index 87f2d173..ad04409f 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -82,6 +82,11 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $class = get_class($this->drv); $this->assertTrue(strlen($class::driverName()) > 0); } + + public function testFetchSchemaId() { + $class = get_class($this->drv); + $this->assertTrue(strlen($class::schemaID()) > 0); + } public function testCheckCharacterSetAcceptability() { $this->assertTrue($this->drv->charsetAcceptable()); diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php new file mode 100644 index 00000000..b4de0ea3 --- /dev/null +++ b/tests/cases/Db/BaseUpdate.php @@ -0,0 +1,136 @@ +interfaceConstructor)(); + } + + public function setUp() { + if (!static::$interface) { + $this->markTestSkipped(static::$implementation." database driver not available"); + } + self::clearData(); + self::setConf(); + // construct a fresh driver for each test + $this->drv = new static::$dbInfo->driverClass; + $schemaId = (get_class($this->drv))::schemaID(); + // set up a virtual filesystem for schema files + $this->vfs = vfsStream::setup("schemata", null, [$schemaId => []]); + $this->base = $this->vfs->url(); + $this->path = $this->base."/$schemaId/"; + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } + + public function tearDown() { + // deconstruct the driver + unset($this->drv); + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } + unset($this->path, $this->base, $this->vfs); + self::clearData(); + } + + public static function tearDownAfterClass() { + static::$implementation = null; + static::$dbInfo = null; + self::clearData(); + } + + public function testLoadMissingFile() { + $this->assertException("updateFileMissing", "Db"); + $this->drv->schemaUpdate(1, $this->base); + } + + public function testLoadUnreadableFile() { + touch($this->path."0.sql"); + chmod($this->path."0.sql", 0000); + $this->assertException("updateFileUnreadable", "Db"); + $this->drv->schemaUpdate(1, $this->base); + } + + public function testLoadCorruptFile() { + file_put_contents($this->path."0.sql", "This is a corrupt file"); + $this->assertException("updateFileError", "Db"); + $this->drv->schemaUpdate(1, $this->base); + } + + public function testLoadIncompleteFile() { + file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);"); + $this->assertException("updateFileIncomplete", "Db"); + $this->drv->schemaUpdate(1, $this->base); + } + + public function testLoadEmptyFile() { + file_put_contents($this->path."0.sql", ""); + $this->assertException("updateFileIncomplete", "Db"); + $this->drv->schemaUpdate(1, $this->base); + } + + public function testLoadCorrectFile() { + file_put_contents($this->path."0.sql", static::$minimal1); + $this->drv->schemaUpdate(1, $this->base); + $this->assertEquals(1, $this->drv->schemaVersion()); + } + + 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'"); + $this->assertException("updateFileIncomplete", "Db"); + try { + $this->drv->schemaUpdate(2, $this->base); + } catch (Exception $e) { + $this->assertEquals(1, $this->drv->schemaVersion()); + throw $e; + } + } + + public function testPerformSequentialUpdate() { + file_put_contents($this->path."0.sql", static::$minimal1); + file_put_contents($this->path."1.sql", static::$minimal2); + $this->drv->schemaUpdate(2, $this->base); + $this->assertEquals(2, $this->drv->schemaVersion()); + } + + public function testPerformActualUpdate() { + $this->drv->schemaUpdate(Database::SCHEMA_VERSION); + $this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion()); + } + + public function testDeclineManualUpdate() { + // turn auto-updating off + Arsse::$conf->dbAutoUpdate = false; + $this->assertException("updateManual", "Db"); + $this->drv->schemaUpdate(Database::SCHEMA_VERSION); + } + + public function testDeclineDowngrade() { + $this->assertException("updateTooNew", "Db"); + $this->drv->schemaUpdate(-1, $this->base); + } +} diff --git a/tests/cases/Db/PostgreSQL/TestUpdate.php b/tests/cases/Db/PostgreSQL/TestUpdate.php new file mode 100644 index 00000000..62f9140b --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestUpdate.php @@ -0,0 +1,16 @@ + + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { + protected static $implementation = "PDO PostgreSQL"; + protected static $minimal1 = "CREATE TABLE arsse_meta(key text 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/cases/Db/SQLite3/TestUpdate.php b/tests/cases/Db/SQLite3/TestUpdate.php index 1c219a1d..ecea58b9 100644 --- a/tests/cases/Db/SQLite3/TestUpdate.php +++ b/tests/cases/Db/SQLite3/TestUpdate.php @@ -6,113 +6,11 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\SQLite3; -use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Conf; -use JKingWeb\Arsse\Database; -use JKingWeb\Arsse\Db\Exception; -use JKingWeb\Arsse\Db\SQLite3\Driver; -use org\bovigo\vfs\vfsStream; - /** * @covers \JKingWeb\Arsse\Db\SQLite3\Driver * @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ -class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { - protected $data; - protected $drv; - protected $vfs; - protected $base; - - const MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; - const MINIMAL2 = "pragma user_version=2"; - - public function setUp(array $conf = []) { - if (!Driver::requirementsMet()) { - $this->markTestSkipped("SQLite extension not loaded"); - } - self::clearData(); - $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); - self::setConf($conf); - $this->base = $this->vfs->url(); - $this->path = $this->base."/SQLite3/"; - $this->drv = new Driver(); - } - - public function tearDown() { - unset($this->drv); - unset($this->data); - unset($this->vfs); - self::clearData(); - } - - public function testLoadMissingFile() { - $this->assertException("updateFileMissing", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadUnreadableFile() { - touch($this->path."0.sql"); - chmod($this->path."0.sql", 0000); - $this->assertException("updateFileUnreadable", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadCorruptFile() { - file_put_contents($this->path."0.sql", "This is a corrupt file"); - $this->assertException("updateFileError", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadIncompleteFile() { - file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);"); - $this->assertException("updateFileIncomplete", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadEmptyFile() { - file_put_contents($this->path."0.sql", ""); - $this->assertException("updateFileIncomplete", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadCorrectFile() { - file_put_contents($this->path."0.sql", self::MINIMAL1); - $this->drv->schemaUpdate(1, $this->base); - $this->assertEquals(1, $this->drv->schemaVersion()); - } - - public function testPerformPartialUpdate() { - file_put_contents($this->path."0.sql", self::MINIMAL1); - file_put_contents($this->path."1.sql", " "); - $this->assertException("updateFileIncomplete", "Db"); - try { - $this->drv->schemaUpdate(2, $this->base); - } catch (Exception $e) { - $this->assertEquals(1, $this->drv->schemaVersion()); - throw $e; - } - } - - public function testPerformSequentialUpdate() { - file_put_contents($this->path."0.sql", self::MINIMAL1); - file_put_contents($this->path."1.sql", self::MINIMAL2); - $this->drv->schemaUpdate(2, $this->base); - $this->assertEquals(2, $this->drv->schemaVersion()); - } - - public function testPerformActualUpdate() { - $this->drv->schemaUpdate(Database::SCHEMA_VERSION); - $this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion()); - } - - public function testDeclineManualUpdate() { - // turn auto-updating off - $this->setUp(['dbAutoUpdate' => false]); - $this->assertException("updateManual", "Db"); - $this->drv->schemaUpdate(Database::SCHEMA_VERSION); - } - - public function testDeclineDowngrade() { - $this->assertException("updateTooNew", "Db"); - $this->drv->schemaUpdate(-1, $this->base); - } +class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { + protected static $implementation = "SQLite 3"; + protected static $minimal1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; + protected static $minimal2 = "pragma user_version=2"; } diff --git a/tests/cases/Db/SQLite3PDO/TestUpdate.php b/tests/cases/Db/SQLite3PDO/TestUpdate.php index 58caca37..4a23595f 100644 --- a/tests/cases/Db/SQLite3PDO/TestUpdate.php +++ b/tests/cases/Db/SQLite3PDO/TestUpdate.php @@ -6,114 +6,11 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO; -use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Conf; -use JKingWeb\Arsse\Database; -use JKingWeb\Arsse\Db\Exception; -use JKingWeb\Arsse\Db\SQLite3\PDODriver; -use org\bovigo\vfs\vfsStream; - /** * @covers \JKingWeb\Arsse\Db\SQLite3\PDODriver * @covers \JKingWeb\Arsse\Db\PDOError */ -class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest { - protected $data; - protected $drv; - protected $vfs; - protected $base; - - const MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; - const MINIMAL2 = "pragma user_version=2"; - - public function setUp(array $conf = []) { - if (!PDODriver::requirementsMet()) { - $this->markTestSkipped("PDO-SQLite extension not loaded"); - } - self::clearData(); - $this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]); - $conf['dbDriver'] = PDODriver::class; - self::setConf($conf); - $this->base = $this->vfs->url(); - $this->path = $this->base."/SQLite3/"; - $this->drv = new PDODriver(); - } - - public function tearDown() { - unset($this->drv); - unset($this->data); - unset($this->vfs); - self::clearData(); - } - - public function testLoadMissingFile() { - $this->assertException("updateFileMissing", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadUnreadableFile() { - touch($this->path."0.sql"); - chmod($this->path."0.sql", 0000); - $this->assertException("updateFileUnreadable", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadCorruptFile() { - file_put_contents($this->path."0.sql", "This is a corrupt file"); - $this->assertException("updateFileError", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadIncompleteFile() { - file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);"); - $this->assertException("updateFileIncomplete", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadEmptyFile() { - file_put_contents($this->path."0.sql", ""); - $this->assertException("updateFileIncomplete", "Db"); - $this->drv->schemaUpdate(1, $this->base); - } - - public function testLoadCorrectFile() { - file_put_contents($this->path."0.sql", self::MINIMAL1); - $this->drv->schemaUpdate(1, $this->base); - $this->assertEquals(1, $this->drv->schemaVersion()); - } - - public function testPerformPartialUpdate() { - file_put_contents($this->path."0.sql", self::MINIMAL1); - file_put_contents($this->path."1.sql", " "); - $this->assertException("updateFileIncomplete", "Db"); - try { - $this->drv->schemaUpdate(2, $this->base); - } catch (Exception $e) { - $this->assertEquals(1, $this->drv->schemaVersion()); - throw $e; - } - } - - public function testPerformSequentialUpdate() { - file_put_contents($this->path."0.sql", self::MINIMAL1); - file_put_contents($this->path."1.sql", self::MINIMAL2); - $this->drv->schemaUpdate(2, $this->base); - $this->assertEquals(2, $this->drv->schemaVersion()); - } - - public function testPerformActualUpdate() { - $this->drv->schemaUpdate(Database::SCHEMA_VERSION); - $this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion()); - } - - public function testDeclineManualUpdate() { - // turn auto-updating off - $this->setUp(['dbAutoUpdate' => false]); - $this->assertException("updateManual", "Db"); - $this->drv->schemaUpdate(Database::SCHEMA_VERSION); - } - - public function testDeclineDowngrade() { - $this->assertException("updateTooNew", "Db"); - $this->drv->schemaUpdate(-1, $this->base); - } +class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { + protected static $implementation = "PDO SQLite 3"; + protected static $minimal1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; + protected static $minimal2 = "pragma user_version=2"; } diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 06b3d6ad..d4964100 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -61,7 +61,7 @@ cases/Db/PostgreSQL/TestStatement.php cases/Db/PostgreSQL/TestCreation.php cases/Db/PostgreSQL/TestDriver.php - + cases/Db/PostgreSQL/TestUpdate.php cases/Db/SQLite3/TestDatabase.php From 10b228224d6af560033b11c2d3573ba2e505922f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 28 Nov 2018 12:12:49 -0500 Subject: [PATCH 27/58] Correct PostgreSQL data format and other tweaks --- sql/PostgreSQL/0.sql | 24 +++++++++++------------ sql/PostgreSQL/1.sql | 8 ++++---- tests/cases/Database/Base.php | 6 +++++- tests/cases/Database/SeriesMiscellany.php | 7 +++---- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/sql/PostgreSQL/0.sql b/sql/PostgreSQL/0.sql index 6e6b2f19..3d940f5a 100644 --- a/sql/PostgreSQL/0.sql +++ b/sql/PostgreSQL/0.sql @@ -31,7 +31,7 @@ create table arsse_folders( owner text not null references arsse_users(id) on delete cascade on update cascade, parent bigint references arsse_folders(id) on delete cascade, name text not null, - modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, -- + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, -- unique(owner,name,parent) ); @@ -41,10 +41,10 @@ create table arsse_feeds( title text, favicon text, source text, - updated timestamp(0) with time zone, - modified timestamp(0) with time zone, - next_fetch timestamp(0) with time zone, - orphaned timestamp(0) with time zone, + updated timestamp(0) without time zone, + modified timestamp(0) without time zone, + next_fetch timestamp(0) without time zone, + orphaned timestamp(0) without time zone, etag text not null default '', err_count bigint not null default 0, err_msg text, @@ -59,8 +59,8 @@ create table arsse_subscriptions( id bigserial primary key, owner text not null references arsse_users(id) on delete cascade on update cascade, feed bigint not null references arsse_feeds(id) on delete cascade, - added timestamp(0) with time zone not null default CURRENT_TIMESTAMP, - modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + added timestamp(0) without time zone not null default CURRENT_TIMESTAMP, + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, title text, order_type smallint not null default 0, pinned smallint not null default 0, @@ -74,9 +74,9 @@ create table arsse_articles( url text, title text, author text, - published timestamp(0) with time zone, - edited timestamp(0) with time zone, - modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + published timestamp(0) without time zone, + edited timestamp(0) without time zone, + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, content text, guid text, url_title_hash text not null, @@ -95,14 +95,14 @@ create table arsse_marks( subscription bigint not null references arsse_subscriptions(id) on delete cascade on update cascade, read smallint not null default 0, starred smallint not null default 0, - modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, primary key(article,subscription) ); create table arsse_editions( id bigserial primary key, article bigint not null references arsse_articles(id) on delete cascade, - modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP ); create table arsse_categories( diff --git a/sql/PostgreSQL/1.sql b/sql/PostgreSQL/1.sql index 73a0c1ff..1549fd5f 100644 --- a/sql/PostgreSQL/1.sql +++ b/sql/PostgreSQL/1.sql @@ -6,8 +6,8 @@ create table arsse_sessions ( id text primary key, - created timestamp(0) with time zone not null default CURRENT_TIMESTAMP, - expires timestamp(0) with time zone not null, + created timestamp(0) without time zone not null default CURRENT_TIMESTAMP, + expires timestamp(0) without time zone not null, "user" text not null references arsse_users(id) on delete cascade on update cascade ); @@ -15,7 +15,7 @@ create table arsse_labels ( id bigserial primary key, owner text not null references arsse_users(id) on delete cascade on update cascade, name text not null, - modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, unique(owner,name) ); @@ -24,7 +24,7 @@ create table arsse_label_members ( article bigint not null references arsse_articles(id) on delete cascade, subscription bigint not null references arsse_subscriptions(id) on delete cascade, assigned smallint not null default 1, - modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP, + modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, primary key(label,article) ); diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php index 85582e74..46ce64e1 100644 --- a/tests/cases/Database/Base.php +++ b/tests/cases/Database/Base.php @@ -76,6 +76,7 @@ abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ $this->markTestSkipped(static::$failureReason); } Arsse::$db = new Database(static::$drv); + Arsse::$db->driverSchemaUpdate(); // create a mock user manager Arsse::$user = Phake::mock(User::class); Phake::when(Arsse::$user)->authorize->thenReturn(true); @@ -115,7 +116,10 @@ abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ $drv = static::$drv; $tr = $drv->begin(); foreach ($data as $table => $info) { - $cols = implode(",", array_keys($info['columns'])); + $cols = array_map(function($v) { + return '"'.str_replace('"', '""', $v).'"'; + }, array_keys($info['columns'])); + $cols = implode(",", $cols); $bindings = array_values($info['columns']); $params = implode(",", array_fill(0, sizeof($info['columns']), "?")); $s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings); diff --git a/tests/cases/Database/SeriesMiscellany.php b/tests/cases/Database/SeriesMiscellany.php index 50e4ed59..0777e4e6 100644 --- a/tests/cases/Database/SeriesMiscellany.php +++ b/tests/cases/Database/SeriesMiscellany.php @@ -27,11 +27,11 @@ trait SeriesMiscellany { } public function testInitializeDatabase() { - $d = new Database(); - $this->assertSame(Database::SCHEMA_VERSION, $d->driverSchemaVersion()); + $this->assertSame(Database::SCHEMA_VERSION, Arsse::$db->driverSchemaVersion()); } public function testManuallyInitializeDatabase() { + (static::$dbInfo->razeFunction)(static::$drv); $d = new Database(false); $this->assertSame(0, $d->driverSchemaVersion()); $this->assertTrue($d->driverSchemaUpdate()); @@ -40,7 +40,6 @@ trait SeriesMiscellany { } public function testCheckCharacterSetAcceptability() { - $d = new Database(); - $this->assertInternalType("bool", $d->driverCharsetAcceptable()); + $this->assertInternalType("bool", Arsse::$db->driverCharsetAcceptable()); } } From dd4f22e04ebf99c8e65c62181d9f14d2ab83e929 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 28 Nov 2018 14:21:36 -0500 Subject: [PATCH 28/58] Avoid use of reserved SQL word "user" --- lib/Database.php | 34 +++++++++++----------- tests/cases/Db/PostgreSQL/TestDatabase.php | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 7182c6f7..21b930c7 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -182,7 +182,7 @@ class Database { $id = UUID::mint()->hex; $expires = Date::add(Arsse::$conf->userSessionTimeout); // save the session to the database - $this->db->prepare("INSERT INTO arsse_sessions(id,expires,user) values(?,?,?)", "str", "datetime", "str")->run($id, $expires, $user); + $this->db->prepare("INSERT INTO arsse_sessions(id,expires,\"user\") values(?,?,?)", "str", "datetime", "str")->run($id, $expires, $user); // return the ID return $id; } @@ -193,12 +193,12 @@ class Database { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } // delete the session and report success. - return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id = ? and user = ?", "str", "str")->run($id, $user)->changes(); + return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id = ? and \"user\" = ?", "str", "str")->run($id, $user)->changes(); } public function sessionResume(string $id): array { $maxAge = Date::sub(Arsse::$conf->userSessionLifetime); - $out = $this->db->prepare("SELECT id,created,expires,user from arsse_sessions where id = ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow(); + $out = $this->db->prepare("SELECT id,created,expires,\"user\" from arsse_sessions where id = ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow(); // if the session does not exist or is expired, throw an exception if (!$out) { throw new User\ExceptionSession("invalid", $id); @@ -371,11 +371,11 @@ class Database { // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves $p = $this->db->prepare( "WITH RECURSIVE - target as (select ? as user, ? as source, ? as dest, ? as rename), - folders as (SELECT id from arsse_folders join target on owner = user and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id) + target as (select ? as userid, ? as source, ? as dest, ? as rename), + folders as (SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id) ". "SELECT - ((select dest from target) is null or exists(select id from arsse_folders join target on owner = user and coalesce(id,0) = coalesce(dest,0))) as extant, + ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) as extant, not exists(select id from folders where id = coalesce((select dest from target),0)) as valid, not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) as available ", @@ -462,15 +462,15 @@ class Database { coalesce(arsse_subscriptions.title, arsse_feeds.title) as title, (SELECT count(*) from arsse_articles where feed = arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks where subscription = arsse_subscriptions.id and read = 1) as unread from arsse_subscriptions - join user on user = owner + join userdata on userid = owner join arsse_feeds on feed = arsse_feeds.id left join topmost on folder=f_id" ); $q->setOrder("pinned desc, title collate nocase"); // define common table expressions - $q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once + $q->setCTE("userdata(userid)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once // topmost folders belonging to the user - $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner = user where parent is null union select id,top from arsse_folders join topmost on parent=f_id"); + $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join userdata on owner = userid where parent is null union select id,top from arsse_folders join topmost on parent=f_id"); if ($id) { // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder // if an ID is specified, add a suitable WHERE condition and bindings @@ -818,7 +818,7 @@ class Database { FROM arsse_articles" ); $q->setLimit($context->limit, $context->offset); - $q->setCTE("user(user)", "SELECT ?", "str", $user); + $q->setCTE("userdata(userid)", "SELECT ?", "str", $user); if ($context->subscription()) { // if a subscription is specified, make sure it exists $id = $this->subscriptionValidateId($user, $context->subscription)['feed']; @@ -830,15 +830,15 @@ class Database { // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $context->folder); // add another CTE for the subscriptions within the folder - $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner join folders on arsse_subscriptions.folder = folders.folder", [], [], "join subscribed_feeds on feed = subscribed_feeds.id"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join userdata on userid = owner join folders on arsse_subscriptions.folder = folders.folder", [], [], "join subscribed_feeds on feed = subscribed_feeds.id"); } elseif ($context->folderShallow()) { // if a shallow folder is specified, make sure it exists $this->folderValidateId($user, $context->folderShallow); // if it does exist, add a CTE with only its subscriptions (and not those of its descendents) - $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner and coalesce(folder,0) = ?", "strict int", $context->folderShallow, "join subscribed_feeds on feed = subscribed_feeds.id"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join userdata on userid = owner and coalesce(folder,0) = ?", "strict int", $context->folderShallow, "join subscribed_feeds on feed = subscribed_feeds.id"); } else { // otherwise add a CTE for all the user's subscriptions - $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner", [], [], "join subscribed_feeds on feed = subscribed_feeds.id"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join userdata on userid = owner", [], [], "join subscribed_feeds on feed = subscribed_feeds.id"); } if ($context->edition()) { // if an edition is specified, filter for its previously identified article @@ -1075,7 +1075,7 @@ class Database { and article in (select id from target_articles where to_insert = 0 and (honour_read = 1 or honour_star = 1 or (select note from target_values) is not null))", "INSERT INTO arsse_marks(subscription,article,read,starred,note) select - (select id from arsse_subscriptions join user on user = owner where arsse_subscriptions.feed = target_articles.feed), + (select id from arsse_subscriptions join userdata on userid = owner where arsse_subscriptions.feed = target_articles.feed), id, coalesce((select read from target_values) * honour_read,0), coalesce((select starred from target_values),0), @@ -1262,8 +1262,8 @@ class Database { // a simple WHERE clause is required here $q->setWhere("arsse_feeds.id = ?", "int", $id); } else { - $q->setCTE("user(user)", "SELECT ?", "str", $user); - $q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join user on user = owner", [], [], "join feeds on arsse_articles.feed = feeds.feed"); + $q->setCTE("userdata(userid)", "SELECT ?", "str", $user); + $q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join userdata on userid = owner", [], [], "join feeds on arsse_articles.feed = feeds.feed"); } return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } @@ -1415,7 +1415,7 @@ class Database { arsse_label_members(label,article,subscription) SELECT ?,id, - (select id from arsse_subscriptions join user on user = owner where arsse_subscriptions.feed = target_articles.feed) + (select id from arsse_subscriptions join userdata on userid = owner where arsse_subscriptions.feed = target_articles.feed) FROM target_articles", "int", $id diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index 5ba3d04d..372a8b7f 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -14,6 +14,6 @@ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { protected static $implementation = "PDO PostgreSQL"; protected function nextID(string $table): int { - return static::$drv->query("SELECT select cast(last_value as bigint) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue(); + return (int) static::$drv->query("SELECT cast(last_value as bigint) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue(); } } From 4a2efd998730caafddbb6c5e8514af412ecd8b19 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 28 Nov 2018 16:24:12 -0500 Subject: [PATCH 29/58] Correct the state of PostgreSQL serial sequence during tests --- tests/cases/Db/PostgreSQL/TestDatabase.php | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index 372a8b7f..7d3d8c86 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -14,6 +14,27 @@ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { protected static $implementation = "PDO PostgreSQL"; protected function nextID(string $table): int { - return (int) static::$drv->query("SELECT cast(last_value as bigint) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue(); + return ((int) static::$drv->query("SELECT last_value from pg_sequences where sequencename = '{$table}_id_seq'")->getValue()) + 1; + } + + public function setUp() { + parent::setUp(); + $seqList = + "select + replace(substring(column_default, 10), right(column_default, 12), '') as seq, + table_name as table, + column_name as col + from information_schema.columns + where table_name like 'arsse_%' + and column_default like 'nextval(%' + "; + foreach(static::$drv->query($seqList) as $r) { + $num = static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue(); + if (!$num) { + continue; + } + $num++; + static::$drv->exec("ALTER SEQUENCE {$r['seq']} RESTART WITH $num"); + } } } From e68fcc0afa6a05a10e46d15aa091a3fae0aa17c5 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 28 Nov 2018 17:16:03 -0500 Subject: [PATCH 30/58] Manipulate only those sequences in the current PostgreSQL schema --- tests/cases/Db/PostgreSQL/TestDatabase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index 7d3d8c86..2b6e7fb3 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -24,8 +24,9 @@ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { replace(substring(column_default, 10), right(column_default, 12), '') as seq, table_name as table, column_name as col - from information_schema.columns - where table_name like 'arsse_%' + from information_schema.columns + where table_schema = current_schema() + and table_name like 'arsse_%' and column_default like 'nextval(%' "; foreach(static::$drv->query($seqList) as $r) { From 4c8d8f1a52ec6fe9b9a000c97e3045ab2e778a96 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 28 Nov 2018 17:18:33 -0500 Subject: [PATCH 31/58] Provide PostgreSQL with an empty-set query for IN() clauses Also satisfy PostgreSQL with some explicit casts --- lib/Database.php | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 21b930c7..115ae092 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; use JKingWeb\DrUUID\UUID; +use JKingWeb\Arsse\Db\Statement; use JKingWeb\Arsse\Misc\Query; use JKingWeb\Arsse\Misc\Context; use JKingWeb\Arsse\Misc\Date; @@ -84,13 +85,26 @@ class Database { protected function generateIn(array $values, string $type): array { $out = [ - [], // query clause + "", // query clause [], // binding types ]; - // the query clause is just a series of question marks separated by commas - $out[0] = implode(",", array_fill(0, sizeof($values), "?")); - // the binding types are just a repetition of the supplied type - $out[1] = array_fill(0, sizeof($values), $type); + if (sizeof($values)) { + // the query clause is just a series of question marks separated by commas + $out[0] = implode(",", array_fill(0, sizeof($values), "?")); + // the binding types are just a repetition of the supplied type + $out[1] = array_fill(0, sizeof($values), $type); + } else { + // if the set is empty, some databases require a query which returns an empty set + $standin = [ + 'string' => "''", + 'binary' => "''", + 'datetime' => "''", + 'integer' => "1", + 'boolean' => "1", + 'float' => "1.0", + ][Statement::TYPES[$type] ?? "string"]; + $out[0] = "select $standin where 1 = 0"; + } return $out; } @@ -371,7 +385,7 @@ class Database { // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves $p = $this->db->prepare( "WITH RECURSIVE - target as (select ? as userid, ? as source, ? as dest, ? as rename), + target as (select ? as userid, cast(? as bigint) as source, cast(? as bigint) as dest, ? as rename), folders as (SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id) ". "SELECT From 4a1c23ba455358af0098ca377d5ccb7d6953ca10 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 29 Nov 2018 13:45:37 -0500 Subject: [PATCH 32/58] Munge PostgreSQL queries instead of adding explicit casts PDO does not adequately inform PostgreSQL of a parameter's type, so type casts are required. Rather than adding these to each query manually, the queries are instead processed to add type hints automatically. Unfortunately the queries are processed rather naively; question-mark characters in string constants, identifiers, regex patterns, or geometry operators will break things spectacularly. --- lib/Database.php | 4 +- lib/Db/PDODriver.php | 4 +- lib/Db/PDOError.php | 5 +- lib/Db/PDOStatement.php | 8 +-- lib/Db/PostgreSQL/PDODriver.php | 4 ++ lib/Db/PostgreSQL/PDOStatement.php | 77 +++++++++++++++++++++ tests/cases/Db/BaseStatement.php | 4 +- tests/cases/Db/PostgreSQL/TestStatement.php | 2 +- tests/lib/DatabaseInformation.php | 4 +- 9 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 lib/Db/PostgreSQL/PDOStatement.php diff --git a/lib/Database.php b/lib/Database.php index 115ae092..c3f91c22 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -385,7 +385,7 @@ class Database { // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves $p = $this->db->prepare( "WITH RECURSIVE - target as (select ? as userid, cast(? as bigint) as source, cast(? as bigint) as dest, ? as rename), + target as (select ? as userid, ? as source, ? as dest, ? as rename), folders as (SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id) ". "SELECT @@ -480,7 +480,7 @@ class Database { join arsse_feeds on feed = arsse_feeds.id left join topmost on folder=f_id" ); - $q->setOrder("pinned desc, title collate nocase"); + $q->setOrder("pinned desc, coalesce(arsse_subscriptions.title, arsse_feeds.title) collate nocase"); // define common table expressions $q->setCTE("userdata(userid)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once // topmost folders belonging to the user diff --git a/lib/Db/PDODriver.php b/lib/Db/PDODriver.php index c6ec0d4b..0eedd6bd 100644 --- a/lib/Db/PDODriver.php +++ b/lib/Db/PDODriver.php @@ -28,9 +28,9 @@ trait PDODriver { } $changes = $r->rowCount(); try { - $lastId = 0; - $lastId = $this->db->lastInsertId(); + $lastId = ($changes) ? $this->db->lastInsertId() : 0; } catch (\PDOException $e) { // @codeCoverageIgnore + $lastId = 0; } return new PDOResult($r, [$changes, $lastId]); } diff --git a/lib/Db/PDOError.php b/lib/Db/PDOError.php index d9ee7c86..5aee4dfb 100644 --- a/lib/Db/PDOError.php +++ b/lib/Db/PDOError.php @@ -7,14 +7,15 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db; trait PDOError { - public function exceptionBuild(): array { - if ($this instanceof Statement) { + public function exceptionBuild(bool $statementError = null): array { + if ($statementError ?? ($this instanceof Statement)) { $err = $this->st->errorInfo(); } else { $err = $this->db->errorInfo(); } switch ($err[0]) { case "22P02": + case "42804": return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; case "23000": case "23502": diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php index 95b95a7c..dca020fd 100644 --- a/lib/Db/PDOStatement.php +++ b/lib/Db/PDOStatement.php @@ -28,10 +28,10 @@ class PDOStatement extends AbstractStatement { } public function __destruct() { - unset($this->st); + unset($this->st, $this->db); } - public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { + public function runArray(array $values = []): Result { $this->st->closeCursor(); $this->bindValues($values); try { @@ -42,9 +42,9 @@ class PDOStatement extends AbstractStatement { } $changes = $this->st->rowCount(); try { - $lastId = 0; - $lastId = $this->db->lastInsertId(); + $lastId = ($changes) ? $this->db->lastInsertId() : 0; } catch (\PDOException $e) { // @codeCoverageIgnore + $lastId = 0; } return new PDOResult($this->st, [$changes, $lastId]); } diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php index af983381..d9716dad 100644 --- a/lib/Db/PostgreSQL/PDODriver.php +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -44,4 +44,8 @@ class PDODriver extends Driver { public static function driverName(): string { return Arsse::$lang->msg("Driver.Db.PostgreSQLPDO.Name"); } + + public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { + return new PDOStatement($this->db, $query, $paramTypes); + } } diff --git a/lib/Db/PostgreSQL/PDOStatement.php b/lib/Db/PostgreSQL/PDOStatement.php new file mode 100644 index 00000000..9d584d00 --- /dev/null +++ b/lib/Db/PostgreSQL/PDOStatement.php @@ -0,0 +1,77 @@ + "bigint", + "float" => "decimal", + "datetime" => "timestamp", + "binary" => "bytea", + "string" => "text", + "boolean" => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3 + ]; + + protected $db; + protected $st; + protected $qOriginal; + protected $qMunged; + protected $bindings; + + public function __construct(\PDO $db, string $query, array $bindings = []) { + $this->db = $db; // both db and st are the same object due to the logic of the PDOError handler + $this->qOriginal = $query; + $this->retypeArray($bindings); + } + + public function __destruct() { + unset($this->db, $this->st); + } + + public function retypeArray(array $bindings, bool $append = false): bool { + if ($append) { + return parent::retypeArray($bindings, $append); + } else { + $this->bindings = $bindings; + parent::retypeArray($bindings, $append); + $this->qMunged = self::mungeQuery($this->qOriginal, $this->types, false); + try { + $s = $this->db->prepare($this->qMunged); + $this->st = new \JKingWeb\Arsse\Db\PDOStatement($this->db, $s, $this->bindings); + } catch (\PDOException $e) { + list($excClass, $excMsg, $excData) = $this->exceptionBuild(true); + throw new $excClass($excMsg, $excData); + } + } + return true; + } + + public static function mungeQuery(string $q, array $types, bool $mungeParamMarkers = true): string { + $q = explode("?", $q); + $out = ""; + for ($b = 1; $b < sizeof($q); $b++) { + $a = $b - 1; + $mark = $mungeParamMarkers ? "\$$b" : "?"; + $type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a]] : ""; + $out .= $q[$a].$mark.$type; + } + $out .= array_pop($q); + return $out; + } + + public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { + return $this->st->runArray($values); + } + + /** @codeCoverageIgnore */ + protected function bindValue($value, string $type, int $position): bool { + // stub required by abstract parent, but never used + return $value; + } +} diff --git a/tests/cases/Db/BaseStatement.php b/tests/cases/Db/BaseStatement.php index c7da01ee..4ac71df9 100644 --- a/tests/cases/Db/BaseStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -59,7 +59,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideBindings */ public function testBindATypedValue($value, string $type, string $exp) { if ($exp=="null") { - $query = "SELECT (cast(? as text) is null) as pass"; + $query = "SELECT (? is null) as pass"; } else { $query = "SELECT ($exp = ?) as pass"; } @@ -76,7 +76,7 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { $this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown"); } if ($exp=="null") { - $query = "SELECT (cast(? as text) is null) as pass"; + $query = "SELECT (? is null) as pass"; } else { $query = "SELECT ($exp = ?) as pass"; } diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php index cfa42b47..5066b9a3 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -13,7 +13,7 @@ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { protected static $implementation = "PDO PostgreSQL"; protected function makeStatement(string $q, array $types = []): array { - return [static::$interface, static::$interface->prepare($q), $types]; + return [static::$interface, $q, $types]; } protected function decorateTypeSyntax(string $value, string $type): string { diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php index 01cc444f..1eefa383 100644 --- a/tests/lib/DatabaseInformation.php +++ b/tests/lib/DatabaseInformation.php @@ -161,10 +161,10 @@ class DatabaseInformation { 'PDO PostgreSQL' => [ 'pdo' => true, 'backend' => "PostgreSQL", - 'statementClass' => \JKingWeb\Arsse\Db\PDOStatement::class, + 'statementClass' => \JKingWeb\Arsse\Db\PostgreSQL\PDOStatement::class, 'resultClass' => \JKingWeb\Arsse\Db\PDOResult::class, 'driverClass' => \JKingWeb\Arsse\Db\PostgreSQL\PDODriver::class, - 'stringOutput' => true, + 'stringOutput' => false, 'interfaceConstructor' => function() { $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); try { From 527ecee3938778426d6524dfb7e65044d3c6250f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 29 Nov 2018 13:56:15 -0500 Subject: [PATCH 33/58] Code coverage fixes --- lib/Db/PDODriver.php | 2 +- lib/Db/PDOStatement.php | 2 +- lib/Db/PostgreSQL/PDOStatement.php | 9 +++++---- tests/cases/Db/PostgreSQL/TestStatement.php | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/Db/PDODriver.php b/lib/Db/PDODriver.php index 0eedd6bd..e418cdca 100644 --- a/lib/Db/PDODriver.php +++ b/lib/Db/PDODriver.php @@ -28,9 +28,9 @@ trait PDODriver { } $changes = $r->rowCount(); try { + $lastId = 0; $lastId = ($changes) ? $this->db->lastInsertId() : 0; } catch (\PDOException $e) { // @codeCoverageIgnore - $lastId = 0; } return new PDOResult($r, [$changes, $lastId]); } diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php index dca020fd..26a50458 100644 --- a/lib/Db/PDOStatement.php +++ b/lib/Db/PDOStatement.php @@ -42,9 +42,9 @@ class PDOStatement extends AbstractStatement { } $changes = $this->st->rowCount(); try { + $lastId = 0; $lastId = ($changes) ? $this->db->lastInsertId() : 0; } catch (\PDOException $e) { // @codeCoverageIgnore - $lastId = 0; } return new PDOResult($this->st, [$changes, $lastId]); } diff --git a/lib/Db/PostgreSQL/PDOStatement.php b/lib/Db/PostgreSQL/PDOStatement.php index 9d584d00..450ebab4 100644 --- a/lib/Db/PostgreSQL/PDOStatement.php +++ b/lib/Db/PostgreSQL/PDOStatement.php @@ -42,12 +42,13 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement { parent::retypeArray($bindings, $append); $this->qMunged = self::mungeQuery($this->qOriginal, $this->types, false); try { + // statement creation with PostgreSQL should never fail (it is not evaluated at creation time) $s = $this->db->prepare($this->qMunged); - $this->st = new \JKingWeb\Arsse\Db\PDOStatement($this->db, $s, $this->bindings); - } catch (\PDOException $e) { - list($excClass, $excMsg, $excData) = $this->exceptionBuild(true); - throw new $excClass($excMsg, $excData); + } catch (\PDOException $e) { // @codeCoverageIgnore + list($excClass, $excMsg, $excData) = $this->exceptionBuild(true); // @codeCoverageIgnore + throw new $excClass($excMsg, $excData); // @codeCoverageIgnore } + $this->st = new \JKingWeb\Arsse\Db\PDOStatement($this->db, $s, $this->bindings); } return true; } diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php index 5066b9a3..d5f8b9d8 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; /** - * @covers \JKingWeb\Arsse\Db\PDOStatement + * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDOStatement * @covers \JKingWeb\Arsse\Db\PDOError */ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { protected static $implementation = "PDO PostgreSQL"; From 5c5a5a4886721d1f74f552857e6c106c0db5bc98 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 29 Nov 2018 14:36:34 -0500 Subject: [PATCH 34/58] Appease PostgreSQL's max() aggregate --- lib/Database.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index c3f91c22..600377c9 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -820,10 +820,14 @@ class Database { arsse_articles.id as id, arsse_articles.feed as feed, arsse_articles.modified as modified_date, - max( - arsse_articles.modified, - coalesce((select modified from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),''), - coalesce((select modified from arsse_label_members where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),'') + ( + select + arsse_articles.modified as term + union select + coalesce((select modified from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),'0001-01-01 00:00:00') as term + union select + coalesce((select modified from arsse_label_members where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),'0001-01-01 00:00:00') as term + order by term desc limit 1 ) as marked_date, NOT (select count(*) from arsse_marks where article = arsse_articles.id and read = 1 and subscription in (select sub from subscribed_feeds)) as unread, (select count(*) from arsse_marks where article = arsse_articles.id and starred = 1 and subscription in (select sub from subscribed_feeds)) as starred, From 8fc31cfc40dce1473bd624d5e70b330c73f23f62 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 4 Dec 2018 20:41:21 -0500 Subject: [PATCH 35/58] Rewrite various queries to work in PostgreSQL This involved changes to the driver interface as well as the database schemata. The most significantly altered queries were for article selection and marking, which relied upon unusual features of SQLite. Overall query efficiency should not be adversely affected (it may have even imprved) in the common case, while very rare cases (not presently triggered by any REST handlers) require more queries. One notable benefit of these changes is that functions which query articles can now have complete control over which columns are returned. This has not, however, been implemented yet: symbolic column groups are still used for now. Note that PostgreSQL still fails many tests, but the test suite runs to completion. Note also that one line of the Database class is not covered; later changes will eventually make it easier to cover the line in question. --- lib/Database.php | 379 ++++++++++++++----------- lib/Db/Driver.php | 2 + lib/Db/PostgreSQL/Driver.php | 4 + lib/Db/SQLite3/Driver.php | 9 + lib/Misc/Context.php | 13 +- lib/Misc/Query.php | 13 + sql/PostgreSQL/3.sql | 11 + sql/SQLite3/3.sql | 24 ++ tests/cases/Database/SeriesArticle.php | 41 +++ tests/cases/Db/BaseDriver.php | 5 + 10 files changed, 334 insertions(+), 167 deletions(-) create mode 100644 sql/PostgreSQL/3.sql create mode 100644 sql/SQLite3/3.sql diff --git a/lib/Database.php b/lib/Database.php index 600377c9..812012f8 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -14,7 +14,7 @@ use JKingWeb\Arsse\Misc\Date; use JKingWeb\Arsse\Misc\ValueInfo; class Database { - const SCHEMA_VERSION = 3; + const SCHEMA_VERSION = 4; const LIMIT_ARTICLES = 50; // articleList verbosity levels const LIST_MINIMAL = 0; // only that metadata which is required for context matching @@ -809,77 +809,101 @@ class Database { )->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC); } - protected function articleQuery(string $user, Context $context, array $extraColumns = []): Query { - $extraColumns = implode(",", $extraColumns); - if (strlen($extraColumns)) { - $extraColumns .= ","; + protected function articleQuery(string $user, Context $context, array $cols = ["id"]): Query { + $greatest = $this->db->sqlToken("greatest"); + // prepare the output column list + $colDefs = [ + 'id' => "arsse_articles.id", + 'edition' => "latest_editions.edition", + 'url' => "arsse_articles.url", + 'title' => "arsse_articles.title", + 'author' => "arsse_articles.author", + 'content' => "arsse_articles.content", + 'guid' => "arsse_articles.guid", + 'fingerprint' => "arsse_articles.url_title_hash || ':' || arsse_articles.url_content_hash || ':' || arsse_articles.title_content_hash", + 'subscription' => "arsse_subscriptions.id", + 'feed' => "arsse_subscriptions.feed", + 'starred' => "coalesce(arsse_marks.starred,0)", + 'unread' => "abs(coalesce(arsse_marks.read,0) - 1)", + 'note' => "coalesce(arsse_marks.note,'')", + 'published_date' => "arsse_articles.published", + 'edited_date' => "arsse_articles.edited", + 'modified_date' => "arsse_articles.modified", + 'marked_date' => "$greatest(arsse_articles.modified, coalesce(arsse_marks.modified, '0001-01-01 00:00:00'), coalesce(arsse_label_members.modified, '0001-01-01 00:00:00'))", + 'subscription_title' => "coalesce(arsse_subscriptions.title, arsse_feeds.title)", + 'media_url' => "arsse_enclosures.url", + 'media_type' => "arsse_enclosures.type", + + ]; + if (!$cols) { + // if no columns are specified return a count + $columns = "count(distinct arsse_articles.id) as count"; + } else { + $columns = []; + foreach ($cols as $col) { + $col = trim(strtolower($col)); + if (!isset($colDefs[$col])) { + continue; + } + $columns[] = $colDefs[$col]." as ".$col; + } + $columns = implode(",", $columns); } + // define the basic query, to which we add lots of stuff where necessary $q = new Query( - "SELECT - $extraColumns - arsse_articles.id as id, - arsse_articles.feed as feed, - arsse_articles.modified as modified_date, - ( - select - arsse_articles.modified as term - union select - coalesce((select modified from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),'0001-01-01 00:00:00') as term - union select - coalesce((select modified from arsse_label_members where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),'0001-01-01 00:00:00') as term - order by term desc limit 1 - ) as marked_date, - NOT (select count(*) from arsse_marks where article = arsse_articles.id and read = 1 and subscription in (select sub from subscribed_feeds)) as unread, - (select count(*) from arsse_marks where article = arsse_articles.id and starred = 1 and subscription in (select sub from subscribed_feeds)) as starred, - (select max(id) from arsse_editions where article = arsse_articles.id) as edition, - subscribed_feeds.sub as subscription - FROM arsse_articles" + "SELECT + $columns + from arsse_articles + join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed and arsse_subscriptions.owner = ? + join arsse_feeds on arsse_subscriptions.feed = arsse_feeds.id + left join arsse_marks on arsse_marks.subscription = arsse_subscriptions.id and arsse_marks.article = arsse_articles.id + left join arsse_enclosures on arsse_enclosures.article = arsse_articles.id + left join arsse_label_members on arsse_label_members.subscription = arsse_subscriptions.id and arsse_label_members.article = arsse_articles.id and arsse_label_members.assigned = 1 + left join arsse_labels on arsse_labels.owner = arsse_subscriptions.owner and arsse_label_members.label = arsse_labels.id", + ["str"], [$user] ); + $q->setCTE("latest_editions(article,edition)", "SELECT article,max(id) from arsse_editions group by article", [], [], "join latest_editions on arsse_articles.id = latest_editions.article"); + if ($cols) { + // if there are no output columns requested we're getting a count and should not group, but otherwise we should + $q->setGroup("arsse_articles.id", "arsse_marks.note", "arsse_enclosures.url", "arsse_enclosures.type", "arsse_subscriptions.title", "arsse_feeds.title", "arsse_subscriptions.id", "arsse_marks.modified", "arsse_label_members.modified", "arsse_marks.read", "arsse_marks.starred", "latest_editions.edition"); + } $q->setLimit($context->limit, $context->offset); - $q->setCTE("userdata(userid)", "SELECT ?", "str", $user); if ($context->subscription()) { // if a subscription is specified, make sure it exists - $id = $this->subscriptionValidateId($user, $context->subscription)['feed']; - // add a basic CTE that will join in only the requested subscription - $q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed = subscribed_feeds.id"); + $this->subscriptionValidateId($user, $context->subscription); + // filter for the subscription + $q->setWhere("arsse_subscriptions.id = ?", "int", $context->subscription); } elseif ($context->folder()) { // if a folder is specified, make sure it exists $this->folderValidateId($user, $context->folder); // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $context->folder); - // add another CTE for the subscriptions within the folder - $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join userdata on userid = owner join folders on arsse_subscriptions.folder = folders.folder", [], [], "join subscribed_feeds on feed = subscribed_feeds.id"); + // limit subscriptions to the listed folders + $q->setWhere("arsse_subscriptions.folder in (select folder from folders)"); } elseif ($context->folderShallow()) { // if a shallow folder is specified, make sure it exists $this->folderValidateId($user, $context->folderShallow); - // if it does exist, add a CTE with only its subscriptions (and not those of its descendents) - $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join userdata on userid = owner and coalesce(folder,0) = ?", "strict int", $context->folderShallow, "join subscribed_feeds on feed = subscribed_feeds.id"); - } else { - // otherwise add a CTE for all the user's subscriptions - $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join userdata on userid = owner", [], [], "join subscribed_feeds on feed = subscribed_feeds.id"); + // if it does exist, filter for that folder only + $q->setWhere("coalesce(arsse_subscriptions.folder,0) = ?", "int", $context->folderShallow); } if ($context->edition()) { - // if an edition is specified, filter for its previously identified article - $q->setWhere("arsse_articles.id = (select article from arsse_editions where id = ?)", "int", $context->edition); + // if an edition is specified, first validate it, then filter for it + $this->articleValidateEdition($user, $context->edition); + $q->setWhere("latest_editions.edition = ?", "int", $context->edition); } elseif ($context->article()) { - // if an article is specified, filter for it (it has already been validated above) + // if an article is specified, first validate it, then filter for it + $this->articleValidateId($user, $context->article); $q->setWhere("arsse_articles.id = ?", "int", $context->article); } if ($context->editions()) { - // if multiple specific editions have been requested, prepare a CTE to list them and their articles + // if multiple specific editions have been requested, filter against the list if (!$context->editions) { throw new Db\ExceptionInput("tooShort", ['field' => "editions", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element } elseif (sizeof($context->editions) > self::LIMIT_ARTICLES) { throw new Db\ExceptionInput("tooLong", ['field' => "editions", 'action' => __FUNCTION__, 'max' => self::LIMIT_ARTICLES]); // @codeCoverageIgnore } list($inParams, $inTypes) = $this->generateIn($context->editions, "int"); - $q->setCTE( - "requested_articles(id,edition)", - "SELECT article,id as edition from arsse_editions where edition in ($inParams)", - $inTypes, - $context->editions - ); - $q->setWhere("arsse_articles.id in (select id from requested_articles)"); + $q->setWhere("latest_editions.edition in ($inParams)", $inTypes, $context->editions); } elseif ($context->articles()) { // if multiple specific articles have been requested, prepare a CTE to list them and their articles if (!$context->articles) { @@ -888,21 +912,13 @@ class Database { throw new Db\ExceptionInput("tooLong", ['field' => "articles", 'action' => __FUNCTION__, 'max' => self::LIMIT_ARTICLES]); // @codeCoverageIgnore } list($inParams, $inTypes) = $this->generateIn($context->articles, "int"); - $q->setCTE( - "requested_articles(id,edition)", - "SELECT id,(select max(id) from arsse_editions where article = arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams)", - $inTypes, - $context->articles - ); - $q->setWhere("arsse_articles.id in (select id from requested_articles)"); - } else { - // if neither list is specified, mock an empty table - $q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 = 0"); + $q->setWhere("arsse_articles.id in ($inParams)", $inTypes, $context->articles); } // filter based on label by ID or name if ($context->labelled()) { // any label (true) or no label (false) - $q->setWhere((!$context->labelled ? "not " : "")."exists(select article from arsse_label_members where assigned = 1 and article = arsse_articles.id and subscription in (select sub from subscribed_feeds))"); + $isOrIsNot = (!$context->labelled ? "is" : "is not"); + $q->setWhere("arsse_labels.id $isOrIsNot null"); } elseif ($context->label() || $context->labelName()) { // specific label ID or name if ($context->label()) { @@ -910,7 +926,7 @@ class Database { } else { $id = $this->labelValidateId($user, $context->labelName, true)['id']; } - $q->setWhere("exists(select article from arsse_label_members where assigned = 1 and article = arsse_articles.id and label = ?)", "int", $id); + $q->setWhere("arsse_labels.id = ?", "int", $id); } // filter based on article or edition offset if ($context->oldestArticle()) { @@ -920,40 +936,41 @@ class Database { $q->setWhere("arsse_articles.id <= ?", "int", $context->latestArticle); } if ($context->oldestEdition()) { - $q->setWhere("edition >= ?", "int", $context->oldestEdition); + $q->setWhere("latest_editions.edition >= ?", "int", $context->oldestEdition); } if ($context->latestEdition()) { - $q->setWhere("edition <= ?", "int", $context->latestEdition); + $q->setWhere("latest_editions.edition <= ?", "int", $context->latestEdition); } // filter based on time at which an article was changed by feed updates (modified), or by user action (marked) if ($context->modifiedSince()) { - $q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince); + $q->setWhere("arsse_articles.modified >= ?", "datetime", $context->modifiedSince); } if ($context->notModifiedSince()) { - $q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince); + $q->setWhere("arsse_articles.modified <= ?", "datetime", $context->notModifiedSince); } if ($context->markedSince()) { - $q->setWhere("marked_date >= ?", "datetime", $context->markedSince); + $q->setWhere($colDefs['marked_date']." >= ?", "datetime", $context->markedSince); } if ($context->notMarkedSince()) { - $q->setWhere("marked_date <= ?", "datetime", $context->notMarkedSince); + $q->setWhere($colDefs['marked_date']." <= ?", "datetime", $context->notMarkedSince); } // filter for un/read and un/starred status if specified if ($context->unread()) { - $q->setWhere("unread = ?", "bool", $context->unread); + $q->setWhere("coalesce(arsse_marks.read,0) = ?", "bool", !$context->unread); } if ($context->starred()) { - $q->setWhere("starred = ?", "bool", $context->starred); + $q->setWhere("coalesce(arsse_marks.starred,0) = ?", "bool", $context->starred); } // filter based on whether the article has a note if ($context->annotated()) { - $q->setWhere((!$context->annotated ? "not " : "")."exists(select modified from arsse_marks where article = arsse_articles.id and note <> '' and subscription in (select sub from subscribed_feeds))"); + $comp = ($context->annotated) ? "<>" : "="; + $q->setWhere("coalesce(arsse_marks.note,'') $comp ''"); } // return the query return $q; } - protected function articleChunk(Context $context): array { + protected function contextChunk(Context $context): array { $exception = ""; if ($context->editions()) { // editions take precedence over articles @@ -983,7 +1000,7 @@ class Database { } $context = $context ?? new Context; // if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result - if ($contexts = $this->articleChunk($context)) { + if ($contexts = $this->contextChunk($context)) { $out = []; $tr = $this->begin(); foreach ($contexts as $context) { @@ -997,32 +1014,39 @@ class Database { // NOTE: the cases all cascade into each other: a given verbosity level is always a superset of the previous one case self::LIST_FULL: // everything $columns = array_merge($columns, [ - "(select note from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)) as note", + "note", ]); // no break case self::LIST_TYPICAL: // conservative, plus content $columns = array_merge($columns, [ "content", - "arsse_enclosures.url as media_url", // enclosures are potentially large due to data: URLs - "arsse_enclosures.type as media_type", // FIXME: enclosures should eventually have their own fetch method + "media_url", // enclosures are potentially large due to data: URLs + "media_type", // FIXME: enclosures should eventually have their own fetch method ]); // no break case self::LIST_CONSERVATIVE: // base metadata, plus anything that is not likely to be large text $columns = array_merge($columns, [ - "arsse_articles.url as url", - "arsse_articles.title as title", - "(select coalesce(arsse_subscriptions.title,arsse_feeds.title) from arsse_feeds join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id where arsse_feeds.id = arsse_articles.feed) as subscription_title", + "url", + "title", + "subscription_title", "author", "guid", - "published as published_date", - "edited as edited_date", - "url_title_hash||':'||url_content_hash||':'||title_content_hash as fingerprint", + "published_date", + "edited_date", + "fingerprint", ]); // no break case self::LIST_MINIMAL: // base metadata (always included: required for context matching) $columns = array_merge($columns, [ - // id, subscription, feed, modified_date, marked_date, unread, starred, edition - "edited as edited_date", + "id", + "subscription", + "feed", + "modified_date", + "marked_date", + "unread", + "starred", + "edition", + "edited_date", ]); break; default: @@ -1031,7 +1055,6 @@ class Database { $q = $this->articleQuery($user, $context, $columns); $q->setOrder("edited_date".($context->reverse ? " desc" : "")); $q->setOrder("edition".($context->reverse ? " desc" : "")); - $q->setJoin("left join arsse_enclosures on arsse_enclosures.article = arsse_articles.id"); // perform the query and return results return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } @@ -1043,7 +1066,7 @@ class Database { } $context = $context ?? new Context; // if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result - if ($contexts = $this->articleChunk($context)) { + if ($contexts = $this->contextChunk($context)) { $out = 0; $tr = $this->begin(); foreach ($contexts as $context) { @@ -1052,9 +1075,7 @@ class Database { $tr->commit(); return $out; } else { - $q = $this->articleQuery($user, $context); - $q->pushCTE("selected_articles"); - $q->setBody("SELECT count(*) from selected_articles"); + $q = $this->articleQuery($user, $context, []); return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } } @@ -1063,9 +1084,17 @@ class Database { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } + $data = [ + 'read' => $data['read'] ?? null, + 'starred' => $data['starred'] ?? null, + 'note' => $data['note'] ?? null, + ]; + if (!isset($data['read']) && !isset($data['starred']) && !isset($data['note'])) { + return 0; + } $context = $context ?? new Context; // if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result - if ($contexts = $this->articleChunk($context)) { + if ($contexts = $this->contextChunk($context)) { $out = 0; $tr = $this->begin(); foreach ($contexts as $context) { @@ -1074,63 +1103,69 @@ class Database { $tr->commit(); return $out; } else { - // sanitize input - $values = [ - isset($data['read']) ? $data['read'] : null, - isset($data['starred']) ? $data['starred'] : null, - isset($data['note']) ? $data['note'] : null, - ]; - // the two queries we want to execute to make the requested changes - $queries = [ - "UPDATE arsse_marks - set - read = case when (select honour_read from target_articles where target_articles.id = article) = 1 then (select read from target_values) else read end, - starred = coalesce((select starred from target_values),starred), - note = coalesce((select note from target_values),note), - modified = CURRENT_TIMESTAMP - WHERE - subscription in (select sub from subscribed_feeds) - and article in (select id from target_articles where to_insert = 0 and (honour_read = 1 or honour_star = 1 or (select note from target_values) is not null))", - "INSERT INTO arsse_marks(subscription,article,read,starred,note) - select - (select id from arsse_subscriptions join userdata on userid = owner where arsse_subscriptions.feed = target_articles.feed), - id, - coalesce((select read from target_values) * honour_read,0), - coalesce((select starred from target_values),0), - coalesce((select note from target_values),'') - from target_articles where to_insert = 1 and (honour_read = 1 or honour_star = 1 or coalesce((select note from target_values),'') <> '')" - ]; - $out = 0; - // wrap this UPDATE and INSERT together into a transaction $tr = $this->begin(); - // if an edition context is specified, make sure it's valid - if ($context->edition()) { - // make sure the edition exists - $edition = $this->articleValidateEdition($user, $context->edition); - // if the edition is not the latest, do not mark the read flag - if (!$edition['current']) { - $values[0] = null; + $out = 0; + if ($data['read'] || $data['starred'] || strlen($data['note'] ?? "")) { + // first prepare a query to insert any missing marks rows for the articles we want to mark + // but only insert new mark records if we're setting at least one "positive" mark + $q = $this->articleQuery($user, $context, ["id", "subscription"]); + $q->setWhere("arsse_marks.starred is null"); // null means there is no marks row for the article + $q->pushCTE("missing_marks(article,subscription)"); + $q->setBody("INSERT INTO arsse_marks(article,subscription) SELECT article,subscription from missing_marks"); + $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); + } + if (isset($data['read']) && (isset($data['starred']) || isset($data['note'])) && ($context->edition() || $context->editions())) { + // if marking by edition both read and something else, do separate marks for starred and note than for read + // marking as read is ignored if the edition is not the latest, but the same is not true of the other two marks + $this->db->query("UPDATE arsse_marks set touched = 0 where touched <> 0"); + // set read marks + $q = $this->articleQuery($user, $context, ["id", "subscription"]); + $q->setWhere("arsse_marks.read <> coalesce(?,arsse_marks.read)", "bool", $data['read']); + $q->pushCTE("target_articles(article,subscription)"); + $q->setBody("UPDATE arsse_marks set read = ?, touched = 1 where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", "bool", $data['read']); + $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); + // get the articles associated with the requested editions + if ($context->edition()) { + $context->article($this->articleValidateEdition($user, $context->edition)['article'])->edition(null); + } else { + $context->articles($this->editionArticle(...$context->editions))->editions(null); } - } elseif ($context->article()) { - // otherwise if an article context is specified, make sure it's valid - $this->articleValidateId($user, $context->article); + // set starred and/or note marks (unless all requested editions actually do not exist) + if ($context->article || $context->articles) { + $q = $this->articleQuery($user, $context, ["id", "subscription"]); + $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred))", ["str", "bool"], [$data['note'], $data['starred']]); + $q->pushCTE("target_articles(article,subscription)"); + $data = array_filter($data, function($v) { + return isset($v); + }); + list($set, $setTypes, $setValues) = $this->generateSet($data, ['starred' => "bool", 'note' => "str"]); + $q->setBody("UPDATE arsse_marks set touched = 1, $set where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); + $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); + } + // finally set the modification date for all touched marks and return the number of affected marks + $out = $this->db->query("UPDATE arsse_marks set modified = CURRENT_TIMESTAMP, touched = 0 where touched = 1")->changes(); + } else { + if (!isset($data['read']) && ($context->edition() || $context->editions())) { + // get the articles associated with the requested editions + if ($context->edition()) { + $context->article($this->articleValidateEdition($user, $context->edition)['article'])->edition(null); + } else { + $context->articles($this->editionArticle(...$context->editions))->editions(null); + } + if (!$context->article && !$context->articles) { + return 0; + } + } + $q = $this->articleQuery($user, $context, ["id", "subscription"]); + $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.read <> coalesce(?,arsse_marks.read))", ["str", "bool", "bool"], [$data['note'], $data['starred'], $data['read']]); + $q->pushCTE("target_articles(article,subscription)"); + $data = array_filter($data, function($v) { + return isset($v); + }); + list($set, $setTypes, $setValues) = $this->generateSet($data, ['read' => "bool", 'starred' => "bool", 'note' => "str"]); + $q->setBody("UPDATE arsse_marks set $set, modified = CURRENT_TIMESTAMP where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); + $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); } - // execute each query in sequence - foreach ($queries as $query) { - // first build the query which will select the target articles; we will later turn this into a CTE for the actual query that manipulates the articles - $q = $this->articleQuery($user, $context, [ - "(not exists(select article from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds))) as to_insert", - "((select read from target_values) is not null and (select read from target_values) <> (coalesce((select read from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),0)) and (not exists(select * from requested_articles) or (select max(id) from arsse_editions where article = arsse_articles.id) in (select edition from requested_articles))) as honour_read", - "((select starred from target_values) is not null and (select starred from target_values) <> (coalesce((select starred from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),0))) as honour_star", - ]); - // common table expression with the values to set - $q->setCTE("target_values(read,starred,note)", "SELECT ?,?,?", ["bool","bool","str"], $values); - // push the current query onto the CTE stack and execute the query we're actually interested in - $q->pushCTE("target_articles"); - $q->setBody($query); - $out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); - } - // commit the transaction $tr->commit(); return $out; } @@ -1143,11 +1178,11 @@ class Database { return $this->db->prepare( "SELECT count(*) as total, - coalesce(sum(not read),0) as unread, + coalesce(sum(abs(read - 1)),0) as unread, coalesce(sum(read),0) as read FROM ( select read from arsse_marks where starred = 1 and subscription in (select id from arsse_subscriptions where owner = ?) - )", + ) as starred_data", "str" )->run($user)->getRow(); } @@ -1258,14 +1293,14 @@ class Database { join arsse_feeds on arsse_feeds.id = arsse_articles.feed join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id WHERE - edition = ? and arsse_subscriptions.owner = ?", + arsse_editions.id = ? and arsse_subscriptions.owner = ?", "int", "str" )->run($id, $user)->getRow(); if (!$out) { throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "edition", 'id' => $id]); } - return $out; + return array_map("intval", $out); } public function editionLatest(string $user, Context $context = null): int { @@ -1273,19 +1308,35 @@ class Database { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } $context = $context ?? new Context; - $q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article = arsse_articles.id left join arsse_feeds on arsse_articles.feed = arsse_feeds.id"); + $q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article = arsse_articles.id join arsse_subscriptions on arsse_articles.feed = arsse_subscriptions.feed and arsse_subscriptions.owner = ?", "str", $user); if ($context->subscription()) { // if a subscription is specified, make sure it exists - $id = $this->subscriptionValidateId($user, $context->subscription)['feed']; + $this->subscriptionValidateId($user, $context->subscription); // a simple WHERE clause is required here - $q->setWhere("arsse_feeds.id = ?", "int", $id); - } else { - $q->setCTE("userdata(userid)", "SELECT ?", "str", $user); - $q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join userdata on userid = owner", [], [], "join feeds on arsse_articles.feed = feeds.feed"); + $q->setWhere("arsse_subscriptions.id = ?", "int", $context->subscription); } return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } + public function editionArticle(int ...$edition): array { + $out = []; + $context = (new Context)->editions($edition); + // if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result + if ($contexts = $this->contextChunk($context)) { + $articles = $editions = []; + foreach ($contexts as $context) { + $out = $this->editionArticle(...$context->editions); + $editions = array_merge($editions, array_map("intval", array_keys($out))); + $articles = array_merge($articles, array_map("intval", array_values($out))); + } + return array_combine($editions, $articles); + } else { + list($in, $inTypes) = $this->generateIn($context->editions, "int"); + $out = $this->db->prepare("SELECT id as edition, article from arsse_editions where id in($in)", $inTypes)->run($context->editions)->getAll(); + return $out ? array_combine(array_column($out, "edition"), array_column($out, "article")) : []; + } + } + public function labelAdd(string $user, array $data): int { // if the user isn't authorized to perform this action then throw an exception. if (!Arsse::$user->authorize($user, __FUNCTION__)) { @@ -1304,14 +1355,16 @@ class Database { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } return $this->db->prepare( - "SELECT - id,name, - (select count(*) from arsse_label_members where label = id and assigned = 1) as articles, - (select count(*) from arsse_label_members - join arsse_marks on arsse_label_members.article = arsse_marks.article and arsse_label_members.subscription = arsse_marks.subscription - where label = id and assigned = 1 and read = 1 - ) as read - FROM arsse_labels where owner = ? and articles >= ? order by name + "SELECT * FROM ( + SELECT + id,name, + (select count(*) from arsse_label_members where label = id and assigned = 1) as articles, + (select count(*) from arsse_label_members + join arsse_marks on arsse_label_members.article = arsse_marks.article and arsse_label_members.subscription = arsse_marks.subscription + where label = id and assigned = 1 and read = 1 + ) as read + FROM arsse_labels where owner = ?) as label_data + where articles >= ? order by name ", "str", "int" @@ -1418,14 +1471,14 @@ class Database { $q->setWhere("exists(select article from arsse_label_members where label = ? and article = arsse_articles.id)", "int", $id); $q->pushCTE("target_articles"); $q->setBody( - "UPDATE arsse_label_members set assigned = ?, modified = CURRENT_TIMESTAMP where label = ? and assigned = not ? and article in (select id from target_articles)", + "UPDATE arsse_label_members set assigned = ?, modified = CURRENT_TIMESTAMP where label = ? and assigned <> ? and article in (select id from target_articles)", ["bool","int","bool"], [!$remove, $id, !$remove] ); $out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); // next, if we're not removing, add any new entries that need to be added if (!$remove) { - $q = $this->articleQuery($user, $context); + $q = $this->articleQuery($user, $context, ["id", "feed"]); $q->setWhere("not exists(select article from arsse_label_members where label = ? and article = arsse_articles.id)", "int", $id); $q->pushCTE("target_articles"); $q->setBody( @@ -1433,10 +1486,10 @@ class Database { arsse_label_members(label,article,subscription) SELECT ?,id, - (select id from arsse_subscriptions join userdata on userid = owner where arsse_subscriptions.feed = target_articles.feed) + (select id from arsse_subscriptions where owner = ? and arsse_subscriptions.feed = target_articles.feed) FROM target_articles", - "int", - $id + ["int", "str"], + [$id, $user] ); $out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); } diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index 56f5e8d0..64eca653 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -39,4 +39,6 @@ interface Driver { public function prepareArray(string $query, array $paramTypes): Statement; // report whether the database character set is correct/acceptable public function charsetAcceptable(): bool; + // return an implementation-dependent form of a reference SQL function or operator + public function sqlToken(string $token): string; } diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 5b243857..37603248 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -109,6 +109,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } } + public function sqlToken(string $token): string { + return $token; + } + public function savepointCreate(bool $lock = false): int { if (!$this->transStart) { $this->exec("BEGIN TRANSACTION"); diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index f3660488..2a475a27 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -103,6 +103,15 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return (int) $this->query("PRAGMA user_version")->getValue(); } + public function sqlToken(string $token): string { + switch(strtolower($token)) { + case "greatest": + return "max"; + default: + return $token; + } + } + public function schemaUpdate(int $to, string $basePath = null): bool { // turn off foreign keys $this->exec("PRAGMA foreign_keys = no"); diff --git a/lib/Misc/Context.php b/lib/Misc/Context.php index 5fd61ffc..93e4ac43 100644 --- a/lib/Misc/Context.php +++ b/lib/Misc/Context.php @@ -39,8 +39,13 @@ class Context { protected function act(string $prop, int $set, $value) { if ($set) { - $this->props[$prop] = true; - $this->$prop = $value; + if (is_null($value)) { + unset($this->props[$prop]); + $this->$prop = (new \ReflectionClass($this))->getDefaultProperties()[$prop]; + } else { + $this->props[$prop] = true; + $this->$prop = $value; + } return $this; } else { return isset($this->props[$prop]); @@ -136,14 +141,14 @@ class Context { } public function editions(array $spec = null) { - if ($spec) { + if (isset($spec)) { $spec = $this->cleanArray($spec); } return $this->act(__FUNCTION__, func_num_args(), $spec); } public function articles(array $spec = null) { - if ($spec) { + if (isset($spec)) { $spec = $this->cleanArray($spec); } return $this->act(__FUNCTION__, func_num_args(), $spec); diff --git a/lib/Misc/Query.php b/lib/Misc/Query.php index 9afc23de..d7a2c7f7 100644 --- a/lib/Misc/Query.php +++ b/lib/Misc/Query.php @@ -20,6 +20,7 @@ class Query { protected $qWhere = []; // WHERE clause components protected $tWhere = []; // WHERE clause type bindings protected $vWhere = []; // WHERE clause binding values + protected $group = []; // GROUP BY clause components protected $order = []; // ORDER BY clause components protected $limit = 0; protected $offset = 0; @@ -68,6 +69,13 @@ class Query { return true; } + public function setGroup(string ...$column): bool { + foreach ($column as $col) { + $this->group[] = $col; + } + return true; + } + public function setOrder(string $order, bool $prepend = false): bool { if ($prepend) { array_unshift($this->order, $order); @@ -97,6 +105,7 @@ class Query { $this->tJoin = []; $this->vJoin = []; $this->order = []; + $this->group = []; $this->setLimit(0, 0); if (strlen($join)) { $this->jCTE[] = $join; @@ -167,6 +176,10 @@ class Query { if (sizeof($this->qWhere)) { $out .= " WHERE ".implode(" AND ", $this->qWhere); } + // add any GROUP BY terms + if (sizeof($this->group)) { + $out .= " GROUP BY ".implode(", ", $this->group); + } // add any ORDER BY terms if (sizeof($this->order)) { $out .= " ORDER BY ".implode(", ", $this->order); diff --git a/sql/PostgreSQL/3.sql b/sql/PostgreSQL/3.sql new file mode 100644 index 00000000..2290ae5d --- /dev/null +++ b/sql/PostgreSQL/3.sql @@ -0,0 +1,11 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + +-- Please consult the SQLite 3 schemata for commented version + +alter table arsse_marks alter column modified drop default; +alter table arsse_marks alter column modified drop not null; +alter table arsse_marks add column touched smallint not null default 0; + +update arsse_meta set value = '4' where key = 'schema_version'; diff --git a/sql/SQLite3/3.sql b/sql/SQLite3/3.sql new file mode 100644 index 00000000..063a2f1f --- /dev/null +++ b/sql/SQLite3/3.sql @@ -0,0 +1,24 @@ +-- SPDX-License-Identifier: MIT +-- Copyright 2017 J. King, Dustin Wilson et al. +-- See LICENSE and AUTHORS files for details + +-- allow marks to initially have a null date due to changes in how marks are first created +-- and also add a "touched" column to aid in tracking changes during the course of some transactions +alter table arsse_marks rename to arsse_marks_old; +create table arsse_marks( +-- users' actions on newsfeed entries + article integer not null references arsse_articles(id) on delete cascade, -- article associated with the marks + subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- subscription associated with the marks; the subscription in turn belongs to a user + read boolean not null default 0, -- whether the article has been read + starred boolean not null default 0, -- whether the article is starred + modified text, -- time at which an article was last modified by a given user + note text not null default '', -- Tiny Tiny RSS freeform user note + touched boolean not null default 0, -- used to indicate a record has been modified during the course of some transactions + primary key(article,subscription) -- no more than one mark-set per article per user +); +insert into arsse_marks select article,subscription,read,starred,modified,note,0 from arsse_marks_old; +drop table arsse_marks_old; + +-- set version marker +pragma user_version = 4; +update arsse_meta set value = '4' where key = 'schema_version'; diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 79d1461b..05a8ded2 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -391,6 +391,43 @@ trait SeriesArticle { unset($this->data, $this->matches, $this->fields, $this->checkTables, $this->user); } + public function testRetrieveArticleIdsForEditions() { + $exp = [ + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + 9 => 9, + 10 => 10, + 11 => 11, + 12 => 12, + 13 => 13, + 14 => 14, + 15 => 15, + 16 => 16, + 17 => 17, + 18 => 18, + 19 => 19, + 20 => 20, + 101 => 101, + 102 => 102, + 103 => 103, + 104 => 104, + 105 => 105, + 202 => 102, + 203 => 103, + 204 => 104, + 205 => 105, + 305 => 105, + 1001 => 20, + ]; + $this->assertEquals($exp, Arsse::$db->editionArticle(...range(1,1001))); + } + public function testListArticlesCheckingContext() { $compareIds = function(array $exp, Context $c) { $ids = array_column($ids = Arsse::$db->articleList("john.doe@example.com", $c)->getAll(), "id"); @@ -504,6 +541,10 @@ trait SeriesArticle { Arsse::$db->articleList($this->user); } + public function testMarkNothing() { + $this->assertSame(0, Arsse::$db->articleMark($this->user, [])); + } + public function testMarkAllArticlesUnread() { Arsse::$db->articleMark($this->user, ['read'=>false]); $now = Date::transform(time(), "sql"); diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index ad04409f..af809342 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -92,6 +92,11 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertTrue($this->drv->charsetAcceptable()); } + public function testTranslateAToken() { + $this->assertRegExp("/^[a-z][a-z0-9]*$/i", $this->drv->sqlToken("greatest")); + $this->assertSame("distinct", $this->drv->sqlToken("distinct")); + } + public function testExecAValidStatement() { $this->assertTrue($this->drv->exec($this->create)); } From 258be1d54e818f639aca1045983e6c180c3cc306 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Dec 2018 09:05:43 -0500 Subject: [PATCH 36/58] Fix most PostgreSQL test failures Reasons for failures included an unhandled error code, erroneous sorting assumptions, and a broken computation of the next insert ID in tests Five failures remain. --- lib/Database.php | 12 +++++------- lib/Db/PDOError.php | 1 + tests/cases/Database/SeriesArticle.php | 2 +- tests/cases/Db/PostgreSQL/TestDatabase.php | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 812012f8..1373c9a8 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1193,12 +1193,10 @@ class Database { } $id = $this->articleValidateId($user, $id)['article']; $out = $this->db->prepare("SELECT id,name from arsse_labels where owner = ? and exists(select id from arsse_label_members where article = ? and label = arsse_labels.id and assigned = 1)", "str", "int")->run($user, $id)->getAll(); - if (!$out) { - return $out; - } else { - // flatten the result to return just the label ID or name - return array_column($out, !$byName ? "id" : "name"); - } + // flatten the result to return just the label ID or name, sorted + $out = $out ? array_column($out, !$byName ? "id" : "name") : []; + sort($out); + return $out; } public function articleCategoriesGet(string $user, $id): array { @@ -1444,7 +1442,7 @@ class Database { $this->labelValidateId($user, $id, $byName, false); $field = !$byName ? "id" : "name"; $type = !$byName ? "int" : "str"; - $out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label = id where assigned = 1 and $field = ? and owner = ?", $type, "str")->run($id, $user)->getAll(); + $out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label = id where assigned = 1 and $field = ? and owner = ? order by article", $type, "str")->run($id, $user)->getAll(); if (!$out) { // if no results were returned, do a full validation on the label ID $this->labelValidateId($user, $id, $byName, true, true); diff --git a/lib/Db/PDOError.php b/lib/Db/PDOError.php index 5aee4dfb..7e8252d2 100644 --- a/lib/Db/PDOError.php +++ b/lib/Db/PDOError.php @@ -19,6 +19,7 @@ trait PDOError { return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; case "23000": case "23502": + case "23505": return [ExceptionInput::class, "constraintViolation", $err[2]]; case "55P03": case "57014": diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 05a8ded2..2147d64d 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -957,7 +957,7 @@ trait SeriesArticle { } public function testListTheLabelsOfAnArticle() { - $this->assertEquals([2,1], Arsse::$db->articleLabelsGet("john.doe@example.com", 1)); + $this->assertEquals([1,2], Arsse::$db->articleLabelsGet("john.doe@example.com", 1)); $this->assertEquals([2], Arsse::$db->articleLabelsGet("john.doe@example.com", 5)); $this->assertEquals([], Arsse::$db->articleLabelsGet("john.doe@example.com", 2)); $this->assertEquals(["Fascinating","Interesting"], Arsse::$db->articleLabelsGet("john.doe@example.com", 1, true)); diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index 2b6e7fb3..91e326f4 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -14,7 +14,7 @@ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { protected static $implementation = "PDO PostgreSQL"; protected function nextID(string $table): int { - return ((int) static::$drv->query("SELECT last_value from pg_sequences where sequencename = '{$table}_id_seq'")->getValue()) + 1; + return (int) static::$drv->query("SELECT coalesce(last_value, (select max(id) from $table)) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue(); } public function setUp() { @@ -30,7 +30,7 @@ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { and column_default like 'nextval(%' "; foreach(static::$drv->query($seqList) as $r) { - $num = static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue(); + $num = (int) static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue(); if (!$num) { continue; } From 15301cd7dc8f07c19665cabb07eabf99aa06a546 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Dec 2018 11:05:01 -0500 Subject: [PATCH 37/58] Fix cleanup tests in PostgreSQL --- lib/Database.php | 6 +++++- lib/Db/PostgreSQL/PDOStatement.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index 1373c9a8..dc308d13 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1219,11 +1219,15 @@ class Database { "SELECT id, (select count(*) from arsse_subscriptions where feed = arsse_feeds.id) as subs from arsse_feeds where id = ?". + "), latest_editions(article,edition) as (". + "SELECT article,max(id) from arsse_editions group by article". "), excepted_articles(id,edition) as (". "SELECT - arsse_articles.id, (select max(id) from arsse_editions where article = arsse_articles.id) as edition + arsse_articles.id as id, + latest_editions.edition as edition from arsse_articles join target_feed on arsse_articles.feed = target_feed.id + join latest_editions on arsse_articles.id = latest_editions.article order by edition desc limit ?". ") ". "DELETE from arsse_articles where diff --git a/lib/Db/PostgreSQL/PDOStatement.php b/lib/Db/PostgreSQL/PDOStatement.php index 450ebab4..16582609 100644 --- a/lib/Db/PostgreSQL/PDOStatement.php +++ b/lib/Db/PostgreSQL/PDOStatement.php @@ -12,7 +12,7 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement { const BINDINGS = [ "integer" => "bigint", "float" => "decimal", - "datetime" => "timestamp", + "datetime" => "timestamp(0) without time zone", "binary" => "bytea", "string" => "text", "boolean" => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3 From 22941f5ad15c42b43f1180120591e4a02f0d86ea Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Dec 2018 12:07:45 -0500 Subject: [PATCH 38/58] Fix session tests PostgreSQL now passes all tests. Connection and permission errors still need to be accounted for before the implementation is complete. --- tests/cases/Database/Base.php | 5 ++++- tests/phpunit.xml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php index 46ce64e1..428b80c0 100644 --- a/tests/cases/Database/Base.php +++ b/tests/cases/Database/Base.php @@ -134,7 +134,10 @@ abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ public function compareExpectations(array $expected): bool { foreach ($expected as $table => $info) { - $cols = implode(",", array_keys($info['columns'])); + $cols = array_map(function($v) { + return '"'.str_replace('"', '""', $v).'"'; + }, array_keys($info['columns'])); + $cols = implode(",", $cols); $types = $info['columns']; $data = static::$drv->prepare("SELECT $cols from $table")->run()->getAll(); $cols = array_keys($info['columns']); diff --git a/tests/phpunit.xml b/tests/phpunit.xml index d4964100..29fe5e48 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -66,7 +66,7 @@ cases/Db/SQLite3/TestDatabase.php cases/Db/SQLite3PDO/TestDatabase.php - + cases/Db/PostgreSQL/TestDatabase.php cases/REST/TestTarget.php From 0129965bbdfd562cb070a6ca89da559e580f0c5a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Dec 2018 12:54:19 -0500 Subject: [PATCH 39/58] Cover some missed code --- tests/cases/Database/SeriesMiscellany.php | 4 +++- tests/cases/Misc/TestContext.php | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/cases/Database/SeriesMiscellany.php b/tests/cases/Database/SeriesMiscellany.php index 0777e4e6..a8650295 100644 --- a/tests/cases/Database/SeriesMiscellany.php +++ b/tests/cases/Database/SeriesMiscellany.php @@ -27,7 +27,9 @@ trait SeriesMiscellany { } public function testInitializeDatabase() { - $this->assertSame(Database::SCHEMA_VERSION, Arsse::$db->driverSchemaVersion()); + (static::$dbInfo->razeFunction)(static::$drv); + $d = new Database(true); + $this->assertSame(Database::SCHEMA_VERSION, $d->driverSchemaVersion()); } public function testManuallyInitializeDatabase() { diff --git a/tests/cases/Misc/TestContext.php b/tests/cases/Misc/TestContext.php index 63bf953e..07d6adb0 100644 --- a/tests/cases/Misc/TestContext.php +++ b/tests/cases/Misc/TestContext.php @@ -64,6 +64,9 @@ class TestContext extends \JKingWeb\Arsse\Test\AbstractTest { } else { $this->assertSame($c->$method, $v[$method], "Context method $method did not return the expected results"); } + // clear the context option + $c->$method(null); + $this->assertFalse($c->$method()); } } From 51755a2ce6cb7300aa255b01d2b3b557371314c6 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Dec 2018 16:55:14 -0500 Subject: [PATCH 40/58] Retire article field groups --- lib/Database.php | 56 ++------------- lib/REST/NextCloudNews/V1_2.php | 18 ++++- lib/REST/TinyTinyRSS/API.php | 49 ++++++++++--- tests/cases/Database/SeriesArticle.php | 38 +++------- tests/cases/REST/NextCloudNews/TestV1_2.php | 34 ++++----- tests/cases/REST/TinyTinyRSS/TestAPI.php | 78 ++++++++++----------- 6 files changed, 128 insertions(+), 145 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index dc308d13..d6b19c38 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -16,11 +16,6 @@ use JKingWeb\Arsse\Misc\ValueInfo; class Database { const SCHEMA_VERSION = 4; const LIMIT_ARTICLES = 50; - // articleList verbosity levels - const LIST_MINIMAL = 0; // only that metadata which is required for context matching - const LIST_CONSERVATIVE = 1; // base metadata plus anything that is not potentially large text - const LIST_TYPICAL = 2; // conservative, with the addition of content - const LIST_FULL = 3; // all possible fields /** @var Db\Driver */ public $db; @@ -994,7 +989,7 @@ class Database { } } - public function articleList(string $user, Context $context = null, int $fields = self::LIST_FULL): Db\Result { + public function articleList(string $user, Context $context = null, array $fields = ["id"]): Db\Result { if (!Arsse::$user->authorize($user, __FUNCTION__)) { throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]); } @@ -1009,52 +1004,9 @@ class Database { $tr->commit(); return new Db\ResultAggregate(...$out); } else { - $columns = []; - switch ($fields) { - // NOTE: the cases all cascade into each other: a given verbosity level is always a superset of the previous one - case self::LIST_FULL: // everything - $columns = array_merge($columns, [ - "note", - ]); - // no break - case self::LIST_TYPICAL: // conservative, plus content - $columns = array_merge($columns, [ - "content", - "media_url", // enclosures are potentially large due to data: URLs - "media_type", // FIXME: enclosures should eventually have their own fetch method - ]); - // no break - case self::LIST_CONSERVATIVE: // base metadata, plus anything that is not likely to be large text - $columns = array_merge($columns, [ - "url", - "title", - "subscription_title", - "author", - "guid", - "published_date", - "edited_date", - "fingerprint", - ]); - // no break - case self::LIST_MINIMAL: // base metadata (always included: required for context matching) - $columns = array_merge($columns, [ - "id", - "subscription", - "feed", - "modified_date", - "marked_date", - "unread", - "starred", - "edition", - "edited_date", - ]); - break; - default: - throw new Exception("constantUnknown", $fields); - } - $q = $this->articleQuery($user, $context, $columns); - $q->setOrder("edited_date".($context->reverse ? " desc" : "")); - $q->setOrder("edition".($context->reverse ? " desc" : "")); + $q = $this->articleQuery($user, $context, $fields); + $q->setOrder("arsse_articles.edited".($context->reverse ? " desc" : "")); + $q->setOrder("latest_editions.edition".($context->reverse ? " desc" : "")); // perform the query and return results return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } diff --git a/lib/REST/NextCloudNews/V1_2.php b/lib/REST/NextCloudNews/V1_2.php index 12e6cf79..f16d453d 100644 --- a/lib/REST/NextCloudNews/V1_2.php +++ b/lib/REST/NextCloudNews/V1_2.php @@ -563,7 +563,23 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler { } // perform the fetch try { - $items = Arsse::$db->articleList(Arsse::$user->id, $c, Database::LIST_TYPICAL); + $items = Arsse::$db->articleList(Arsse::$user->id, $c, [ + "edition", + "guid", + "id", + "url", + "title", + "author", + "edited_date", + "content", + "media_type", + "media_url", + "subscription", + "unread", + "starred", + "modified_date", + "fingerprint", + ]); } catch (ExceptionInput $e) { // ID of subscription or folder is not valid return new EmptyResponse(422); diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 57bb5a8b..5ee3ae5a 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -1113,8 +1113,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $out += Arsse::$db->articleMark(Arsse::$user->id, ['starred' => (bool) $data['mode']], (new Context)->articles($articles)); break; case 2: //toggle - $on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(true), Database::LIST_MINIMAL)->getAll(), "id"); - $off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(false), Database::LIST_MINIMAL)->getAll(), "id"); + $on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(true), ["id"])->getAll(), "id"); + $off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(false), ["id"])->getAll(), "id"); if ($off) { $out += Arsse::$db->articleMark(Arsse::$user->id, ['starred' => true], (new Context)->articles($off)); } @@ -1145,8 +1145,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { $out += Arsse::$db->articleMark(Arsse::$user->id, ['read' => !$data['mode']], (new Context)->articles($articles)); break; case 2: //toggle - $on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(true), Database::LIST_MINIMAL)->getAll(), "id"); - $off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(false), Database::LIST_MINIMAL)->getAll(), "id"); + $on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(true), ["id"])->getAll(), "id"); + $off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(false), ["id"])->getAll(), "id"); if ($off) { $out += Arsse::$db->articleMark(Arsse::$user->id, ['read' => false], (new Context)->articles($off)); } @@ -1183,7 +1183,22 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { } // retrieve the requested articles $out = []; - foreach (Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)) as $article) { + $columns = [ + "id", + "guid", + "title", + "url", + "unread", + "starred", + "edited_date", + "subscription", + "subscription_title", + "note", + "content", + "media_url", + "media_type", + ]; + foreach (Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles), $columns) as $article) { $out[] = [ 'id' => (string) $article['id'], // string cast to be consistent with TTRSS 'guid' => $article['guid'] ? "SHA256:".$article['guid'] : null, @@ -1246,7 +1261,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // fetch the list of IDs $out = []; try { - foreach ($this->fetchArticles($data, Database::LIST_MINIMAL) as $row) { + foreach ($this->fetchArticles($data, ["id"]) as $row) { $out[] = ['id' => (int) $row['id']]; } } catch (ExceptionInput $e) { @@ -1267,7 +1282,23 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // retrieve the requested articles $out = []; try { - foreach ($this->fetchArticles($data, Database::LIST_FULL) as $article) { + $columns = [ + "id", + "guid", + "title", + "url", + "unread", + "starred", + "edited_date", + "published_date", + "subscription", + "subscription_title", + "note", + ($data['show_content'] || $data['show_excerpt']) ? "content" : "", + ($data['include_attachments']) ? "media_url": "", + ($data['include_attachments']) ? "media_type": "", + ]; + foreach ($this->fetchArticles($data, $columns) as $article) { $row = [ 'id' => (int) $article['id'], 'guid' => $article['guid'] ? "SHA256:".$article['guid'] : "", @@ -1325,7 +1356,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { // when paginating the header returns the latest ("first") item ID in the full list; we get this ID here $data['skip'] = 0; $data['limit'] = 1; - $firstID = ($this->fetchArticles($data, Database::LIST_MINIMAL)->getRow() ?? ['id' => 0])['id']; + $firstID = ($this->fetchArticles($data, ["id"])->getRow() ?? ['id' => 0])['id']; } elseif ($data['order_by']=="date_reverse") { // the "date_reverse" sort order doesn't get a first ID because it's meaningless for ascending-order pagination (pages doesn't go stale) $firstID = 0; @@ -1346,7 +1377,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { return $out; } - protected function fetchArticles(array $data, int $fields): \JKingWeb\Arsse\Db\Result { + protected function fetchArticles(array $data, array $fields): \JKingWeb\Arsse\Db\Result { // normalize input if (is_null($data['feed_id'])) { throw new Exception("INCORRECT_USAGE"); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 2147d64d..74285f5f 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -364,24 +364,10 @@ trait SeriesArticle { ], ]; $this->fields = [ - Database::LIST_MINIMAL => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - ], - Database::LIST_CONSERVATIVE => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", - ], - Database::LIST_TYPICAL => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", - "content", "media_url", "media_type", - ], - Database::LIST_FULL => [ - "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", - "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", - "content", "media_url", "media_type", - "note", - ], + "id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date", + "url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint", + "content", "media_url", "media_type", + "note", ]; $this->checkTables = ['arsse_marks' => ["subscription","article","read","starred","modified","note"],]; $this->user = "john.doe@example.net"; @@ -522,17 +508,15 @@ trait SeriesArticle { public function testListArticlesCheckingProperties() { $this->user = "john.doe@example.org"; - $this->assertResult($this->matches, Arsse::$db->articleList($this->user)); // check that the different fieldset groups return the expected columns - foreach ($this->fields as $constant => $columns) { - $test = array_keys(Arsse::$db->articleList($this->user, (new Context)->article(101), $constant)->getRow()); - sort($columns); - sort($test); - $this->assertEquals($columns, $test, "Fields do not match expectation for verbosity $constant"); + foreach ($this->fields as $column) { + $test = array_keys(Arsse::$db->articleList($this->user, (new Context)->article(101), [$column])->getRow()); + $this->assertEquals([$column], $test); } - // check that an unknown fieldset produces an exception - $this->assertException("constantUnknown"); - Arsse::$db->articleList($this->user, (new Context)->article(101), \PHP_INT_MAX); + // check that an unknown field is silently ignored + $columns = array_merge($this->fields, ["unknown_column", "bogus_column"]); + $test = array_keys(Arsse::$db->articleList($this->user, (new Context)->article(101), $columns)->getRow()); + $this->assertEquals($this->fields, $test); } public function testListArticlesWithoutAuthority() { diff --git a/tests/cases/REST/NextCloudNews/TestV1_2.php b/tests/cases/REST/NextCloudNews/TestV1_2.php index f7936f45..9d8167cb 100644 --- a/tests/cases/REST/NextCloudNews/TestV1_2.php +++ b/tests/cases/REST/NextCloudNews/TestV1_2.php @@ -734,11 +734,11 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { ['lastModified' => $t->getTimestamp()], ['oldestFirst' => false, 'batchSize' => 5, 'offset' => 0], // offset=0 should not set the latestEdition context ]; - Phake::when(Arsse::$db)->articleList(Arsse::$user->id, $this->anything(), Database::LIST_TYPICAL)->thenReturn(new Result($this->v($this->articles['db']))); - Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("idMissing")); - Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("idMissing")); - Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("typeViolation")); - Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), Database::LIST_TYPICAL)->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->articleList(Arsse::$user->id, $this->anything(), $this->anything())->thenReturn(new Result($this->v($this->articles['db']))); + Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42), $this->anything())->thenThrow(new ExceptionInput("idMissing")); + Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112), $this->anything())->thenThrow(new ExceptionInput("idMissing")); + Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1), $this->anything())->thenThrow(new ExceptionInput("typeViolation")); + Phake::when(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), $this->anything())->thenThrow(new ExceptionInput("typeViolation")); $exp = new Response(['items' => $this->articles['rest']]); // check the contents of the response $this->assertMessage($exp, $this->req("GET", "/items")); // first instance of base context @@ -759,17 +759,17 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { $this->req("GET", "/items", json_encode($in[10])); $this->req("GET", "/items", json_encode($in[11])); // perform method verifications - Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, (new Context)->reverse(true), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->starred(true), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->limit(10)->oldestEdition(6), Database::LIST_TYPICAL); // offset is one more than specified - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5)->latestEdition(4), Database::LIST_TYPICAL); // offset is one less than specified - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->unread(true), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->markedSince($t), Database::LIST_TYPICAL); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5), Database::LIST_TYPICAL); + Phake::verify(Arsse::$db, Phake::times(4))->articleList(Arsse::$user->id, (new Context)->reverse(true), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(42), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(2112), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->subscription(-1), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->folder(-1), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->starred(true), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->limit(10)->oldestEdition(6), $this->anything()); // offset is one more than specified + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5)->latestEdition(4), $this->anything()); // offset is one less than specified + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->unread(true), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->markedSince($t), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5), $this->anything()); } public function testMarkAFolderRead() { @@ -958,6 +958,6 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { $url = "/items?type=2"; Phake::when(Arsse::$db)->articleList->thenReturn(new Result([])); $this->req("GET", $url, json_encode($in)); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->starred(true), Database::LIST_TYPICAL); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->starred(true), $this->anything()); } } diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index 10fc535d..b9b1e2cc 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -1641,9 +1641,9 @@ LONG_STRING; Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->v($this->usedLabels))); Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 101)->thenReturn([]); Phake::when(Arsse::$db)->articleLabelsGet($this->anything(), 102)->thenReturn($this->v([1,3])); - Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101, 102]))->thenReturn(new Result($this->v($this->articles))); - Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101]))->thenReturn(new Result($this->v([$this->articles[0]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([102]))->thenReturn(new Result($this->v([$this->articles[1]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101, 102]), $this->anything())->thenReturn(new Result($this->v($this->articles))); + Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([101]), $this->anything())->thenReturn(new Result($this->v([$this->articles[0]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->articles([102]), $this->anything())->thenReturn(new Result($this->v([$this->articles[1]]))); $exp = $this->respErr("INCORRECT_USAGE"); $this->assertMessage($exp, $this->req($in[0])); $this->assertMessage($exp, $this->req($in[1])); @@ -1750,18 +1750,18 @@ LONG_STRING; Phake::when(Arsse::$db)->articleCount->thenReturn(0); Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1); $c = (new Context)->reverse(true); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), Database::LIST_MINIMAL)->thenThrow(new ExceptionInput("subjectMissing")); - Phake::when(Arsse::$db)->articleList($this->anything(), $c, Database::LIST_MINIMAL)->thenReturn(new Result($this->v($this->articles))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 1]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 2]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 3]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 4]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 5]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 6]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 7]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 8]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 9]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 10]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), ["id"])->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->articleList($this->anything(), $c, ["id"])->thenReturn(new Result($this->v($this->articles))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), ["id"])->thenReturn(new Result($this->v([['id' => 1]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), ["id"])->thenReturn(new Result($this->v([['id' => 2]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), ["id"])->thenReturn(new Result($this->v([['id' => 3]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), ["id"])->thenReturn(new Result($this->v([['id' => 4]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), ["id"])->thenReturn(new Result($this->v([['id' => 5]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), ["id"])->thenReturn(new Result($this->v([['id' => 6]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), ["id"])->thenReturn(new Result($this->v([['id' => 7]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), ["id"])->thenReturn(new Result($this->v([['id' => 8]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), ["id"])->thenReturn(new Result($this->v([['id' => 9]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), ["id"])->thenReturn(new Result($this->v([['id' => 10]]))); $out1 = [ $this->respErr("INCORRECT_USAGE"), $this->respGood([]), @@ -1793,9 +1793,9 @@ LONG_STRING; $this->assertMessage($out1[$a], $this->req($in1[$a]), "Test $a failed"); } for ($a = 0; $a < sizeof($in2); $a++) { - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 1001]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 1002]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_MINIMAL)->thenReturn(new Result($this->v([['id' => 1003]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), ["id"])->thenReturn(new Result($this->v([['id' => 1001]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), ["id"])->thenReturn(new Result($this->v([['id' => 1002]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), ["id"])->thenReturn(new Result($this->v([['id' => 1003]]))); $this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed"); } } @@ -1851,23 +1851,23 @@ LONG_STRING; Phake::when(Arsse::$db)->articleCount->thenReturn(0); Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(1); $c = (new Context)->limit(200)->reverse(true); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), Database::LIST_FULL)->thenThrow(new ExceptionInput("subjectMissing")); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), Database::LIST_FULL)->thenReturn($this->generateHeadlines(2)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(3)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(4)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(5)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(6)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), Database::LIST_FULL)->thenReturn($this->generateHeadlines(7)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), Database::LIST_FULL)->thenReturn($this->generateHeadlines(8)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), Database::LIST_FULL)->thenReturn($this->generateHeadlines(9)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), Database::LIST_FULL)->thenReturn($this->generateHeadlines(10)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c), Database::LIST_FULL)->thenReturn($this->generateHeadlines(11)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->labelled(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(12)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(0), Database::LIST_FULL)->thenReturn($this->generateHeadlines(13)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(42), Database::LIST_FULL)->thenReturn($this->generateHeadlines(14)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folder(42), Database::LIST_FULL)->thenReturn($this->generateHeadlines(15)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->reverse(false), Database::LIST_FULL)->thenReturn($this->generateHeadlines(16)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(2112), $this->anything())->thenThrow(new ExceptionInput("subjectMissing")); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->starred(true), $this->anything())->thenReturn($this->generateHeadlines(1)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088), $this->anything())->thenReturn($this->generateHeadlines(2)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true), $this->anything())->thenReturn($this->generateHeadlines(3)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->label(1088)->unread(true), $this->anything())->thenReturn($this->generateHeadlines(4)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->starred(true), $this->anything())->thenReturn($this->generateHeadlines(5)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->subscription(42)->annotated(true), $this->anything())->thenReturn($this->generateHeadlines(6)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5), $this->anything())->thenReturn($this->generateHeadlines(7)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->offset(2), $this->anything())->thenReturn($this->generateHeadlines(8)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->limit(5)->offset(2), $this->anything())->thenReturn($this->generateHeadlines(9)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->oldestArticle(48), $this->anything())->thenReturn($this->generateHeadlines(10)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c), $this->anything())->thenReturn($this->generateHeadlines(11)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->labelled(true), $this->anything())->thenReturn($this->generateHeadlines(12)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(0), $this->anything())->thenReturn($this->generateHeadlines(13)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folderShallow(42), $this->anything())->thenReturn($this->generateHeadlines(14)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->folder(42), $this->anything())->thenReturn($this->generateHeadlines(15)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->reverse(false), $this->anything())->thenReturn($this->generateHeadlines(16)); $out2 = [ $this->respErr("INCORRECT_USAGE"), $this->outputHeadlines(11), @@ -1904,9 +1904,9 @@ LONG_STRING; $this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed"); } for ($a = 0; $a < sizeof($in3); $a++) { - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1001)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1002)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), Database::LIST_FULL)->thenReturn($this->generateHeadlines(1003)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), $this->anything())->thenReturn($this->generateHeadlines(1001)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), $this->anything())->thenReturn($this->generateHeadlines(1002)); + Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), $this->anything())->thenReturn($this->generateHeadlines(1003)); $this->assertMessage($out3[$a], $this->req($in3[$a]), "Test $a failed"); } } @@ -2000,7 +2000,7 @@ LONG_STRING; ]); $this->assertMessage($exp, $test); // test 'include_header' with skip - Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->reverse(true)->limit(1)->subscription(42), Database::LIST_MINIMAL)->thenReturn($this->generateHeadlines(1867)); + Phake::when(Arsse::$db)->articleList($this->anything(), (new Context)->reverse(true)->limit(1)->subscription(42), $this->anything())->thenReturn($this->generateHeadlines(1867)); $test = $this->req($in[8]); $exp = $this->respGood([ ['id' => 42, 'is_cat' => false, 'first_id' => 1867], From f2245861e32209e6efa418880b7a0c25c6a300db Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Dec 2018 17:07:47 -0500 Subject: [PATCH 41/58] Restore complete Database coverage Also suppress PostgreSQL database function tests from normal coverage, and add a "coverage:full" task to run them if needed. --- RoboFile.php | 18 ++++++++++++++++++ tests/cases/Database/SeriesArticle.php | 6 ++++++ tests/cases/Db/PostgreSQL/TestDatabase.php | 1 + 3 files changed, 25 insertions(+) diff --git a/RoboFile.php b/RoboFile.php index 65d03637..1e2c66a8 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -50,6 +50,21 @@ class RoboFile extends \Robo\Tasks { * recommended if debugging facilities are not otherwise needed. */ public function coverage(array $args): Result { + // run tests with code coverage reporting enabled + $exec = $this->findCoverageEngine(); + return $this->runTests($exec, "coverage", array_merge(["--coverage-html", self::BASE_TEST."coverage"], $args)); + } + + /** Produces a code coverage report, with redundant tests + * + * Depending on the environment, some tests that normally provide + * coverage may be skipped, while working alternatives are normally + * suppressed for reasons of time. This coverage report will try to + * run all tests which may cover code. + * + * See also help for the "coverage" task for more details. + */ + public function coverageFull(array $args): Result { // run tests with code coverage reporting enabled $exec = $this->findCoverageEngine(); return $this->runTests($exec, "typical", array_merge(["--coverage-html", self::BASE_TEST."coverage"], $args)); @@ -88,6 +103,9 @@ class RoboFile extends \Robo\Tasks { case "quick": $set = ["--exclude-group", "optional,slow"]; break; + case "coverage": + $set = ["--exclude-group", "optional,excludeFromCoverage"]; + break; case "full": $set = []; break; diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 74285f5f..96fd4224 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -772,6 +772,12 @@ trait SeriesArticle { $this->compareExpectations($state); } + public function testMarkMultipleMissingEditions() { + $this->assertSame(0, Arsse::$db->articleMark($this->user, ['starred'=>true], (new Context)->editions([500,501]))); + $state = $this->primeExpectations($this->data, $this->checkTables); + $this->compareExpectations($state); + } + public function testMarkMultipleEditionsUnread() { Arsse::$db->articleMark($this->user, ['read'=>false], (new Context)->editions([2,4,7,1001])); $now = Date::transform(time(), "sql"); diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index 91e326f4..a8b2a65a 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PosgreSQL; /** + * @group excludeFromCoverage * @covers \JKingWeb\Arsse\Database * @covers \JKingWeb\Arsse\Misc\Query */ From cf896121b225edbbd66c910e4ac0361baf3897a3 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 5 Dec 2018 17:28:11 -0500 Subject: [PATCH 42/58] Style fixes --- .php_cs.dist | 1 + RoboFile.php | 2 +- lib/Database.php | 19 ++++++++++--------- lib/Db/ResultAggregate.php | 2 +- lib/Db/SQLite3/Driver.php | 2 +- lib/Lang.php | 2 +- lib/REST.php | 4 ++-- lib/REST/TinyTinyRSS/API.php | 2 +- lib/User.php | 1 - tests/cases/CLI/TestCLI.php | 3 +-- tests/cases/Database/Base.php | 6 +++--- tests/cases/Database/SeriesArticle.php | 2 +- tests/cases/Database/SeriesSubscription.php | 1 - tests/cases/Db/BaseDriver.php | 6 +++--- tests/cases/Db/BaseStatement.php | 2 -- tests/cases/Db/PostgreSQL/TestCreation.php | 2 +- tests/cases/Db/PostgreSQL/TestDatabase.php | 6 +++--- tests/cases/Db/PostgreSQL/TestStatement.php | 3 +-- tests/cases/Db/SQLite3/TestDatabase.php | 2 +- tests/cases/Db/SQLite3/TestResult.php | 4 ++-- tests/cases/Db/SQLite3PDO/TestDatabase.php | 2 +- tests/cases/Db/TestResultPDO.php | 4 ++-- tests/cases/REST/TestREST.php | 10 +++++----- tests/cases/REST/TinyTinyRSS/TestAPI.php | 8 ++++---- tests/cases/User/TestInternal.php | 5 ++--- tests/cases/User/TestUser.php | 3 +-- tests/lib/DatabaseInformation.php | 8 ++++---- 27 files changed, 53 insertions(+), 59 deletions(-) diff --git a/.php_cs.dist b/.php_cs.dist index 90db01f7..49435d77 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -17,6 +17,7 @@ $paths = [ $rules = [ '@PSR2' => true, 'braces' => ['position_after_functions_and_oop_constructs' => "same"], + 'function_declaration' => ['closure_function_spacing' => "none"], ]; $finder = \PhpCsFixer\Finder::create(); diff --git a/RoboFile.php b/RoboFile.php index 1e2c66a8..2a5dfda2 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -61,7 +61,7 @@ class RoboFile extends \Robo\Tasks { * coverage may be skipped, while working alternatives are normally * suppressed for reasons of time. This coverage report will try to * run all tests which may cover code. - * + * * See also help for the "coverage" task for more details. */ public function coverageFull(array $args): Result { diff --git a/lib/Database.php b/lib/Database.php index d6b19c38..18da7775 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -808,20 +808,20 @@ class Database { $greatest = $this->db->sqlToken("greatest"); // prepare the output column list $colDefs = [ - 'id' => "arsse_articles.id", - 'edition' => "latest_editions.edition", - 'url' => "arsse_articles.url", - 'title' => "arsse_articles.title", - 'author' => "arsse_articles.author", - 'content' => "arsse_articles.content", - 'guid' => "arsse_articles.guid", + 'id' => "arsse_articles.id", + 'edition' => "latest_editions.edition", + 'url' => "arsse_articles.url", + 'title' => "arsse_articles.title", + 'author' => "arsse_articles.author", + 'content' => "arsse_articles.content", + 'guid' => "arsse_articles.guid", 'fingerprint' => "arsse_articles.url_title_hash || ':' || arsse_articles.url_content_hash || ':' || arsse_articles.title_content_hash", 'subscription' => "arsse_subscriptions.id", 'feed' => "arsse_subscriptions.feed", 'starred' => "coalesce(arsse_marks.starred,0)", 'unread' => "abs(coalesce(arsse_marks.read,0) - 1)", 'note' => "coalesce(arsse_marks.note,'')", - 'published_date' => "arsse_articles.published", + 'published_date' => "arsse_articles.published", 'edited_date' => "arsse_articles.edited", 'modified_date' => "arsse_articles.modified", 'marked_date' => "$greatest(arsse_articles.modified, coalesce(arsse_marks.modified, '0001-01-01 00:00:00'), coalesce(arsse_label_members.modified, '0001-01-01 00:00:00'))", @@ -855,7 +855,8 @@ class Database { left join arsse_enclosures on arsse_enclosures.article = arsse_articles.id left join arsse_label_members on arsse_label_members.subscription = arsse_subscriptions.id and arsse_label_members.article = arsse_articles.id and arsse_label_members.assigned = 1 left join arsse_labels on arsse_labels.owner = arsse_subscriptions.owner and arsse_label_members.label = arsse_labels.id", - ["str"], [$user] + ["str"], + [$user] ); $q->setCTE("latest_editions(article,edition)", "SELECT article,max(id) from arsse_editions group by article", [], [], "join latest_editions on arsse_articles.id = latest_editions.article"); if ($cols) { diff --git a/lib/Db/ResultAggregate.php b/lib/Db/ResultAggregate.php index 4f53b9d2..cc1a052c 100644 --- a/lib/Db/ResultAggregate.php +++ b/lib/Db/ResultAggregate.php @@ -16,7 +16,7 @@ class ResultAggregate extends AbstractResult { // actual public methods public function changes(): int { - return array_reduce($this->data, function ($sum, $value) { + return array_reduce($this->data, function($sum, $value) { return $sum + $value->changes(); }, 0); } diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 2a475a27..238d1f68 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -104,7 +104,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } public function sqlToken(string $token): string { - switch(strtolower($token)) { + switch (strtolower($token)) { case "greatest": return "max"; default: diff --git a/lib/Lang.php b/lib/Lang.php index 6114ba73..a6e07351 100644 --- a/lib/Lang.php +++ b/lib/Lang.php @@ -140,7 +140,7 @@ class Lang { protected function listFiles(): array { $out = $this->globFiles($this->path."*.php"); // trim the returned file paths to return just the language tag - $out = array_map(function ($file) { + $out = array_map(function($file) { $file = str_replace(DIRECTORY_SEPARATOR, "/", $file); // we replace the directory separator because we don't use native paths in testing $file = substr($file, strrpos($file, "/")+1); return strtolower(substr($file, 0, strrpos($file, "."))); diff --git a/lib/REST.php b/lib/REST.php index ac527f1d..70174c10 100644 --- a/lib/REST.php +++ b/lib/REST.php @@ -97,7 +97,7 @@ class REST { public function apiMatch(string $url): array { $map = $this->apis; // sort the API list so the longest URL prefixes come first - uasort($map, function ($a, $b) { + uasort($map, function($a, $b) { return (strlen($a['match']) <=> strlen($b['match'])) * -1; }); // normalize the target URL @@ -270,7 +270,7 @@ class REST { } else { // if the host is a domain name or IP address, split it along dots and just perform URL decoding $host = explode(".", $host); - $host = array_map(function ($segment) { + $host = array_map(function($segment) { return str_replace(".", "%2E", rawurlencode(strtolower(rawurldecode($segment)))); }, $host); $host = implode(".", $host); diff --git a/lib/REST/TinyTinyRSS/API.php b/lib/REST/TinyTinyRSS/API.php index 5ee3ae5a..014bbd65 100644 --- a/lib/REST/TinyTinyRSS/API.php +++ b/lib/REST/TinyTinyRSS/API.php @@ -330,7 +330,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler { 'id' => "FEED:".self::FEED_ALL, 'bare_id' => self::FEED_ALL, 'icon' => "images/folder.png", - 'unread' => array_reduce($subs, function ($sum, $value) { + 'unread' => array_reduce($subs, function($sum, $value) { return $sum + $value['unread']; }, 0), // the sum of all feeds' unread is the total unread ], $tSpecial), diff --git a/lib/User.php b/lib/User.php index 6ccdbcc2..e5d8e176 100644 --- a/lib/User.php +++ b/lib/User.php @@ -9,7 +9,6 @@ namespace JKingWeb\Arsse; use PasswordGenerator\Generator as PassGen; class User { - public $id = null; /** diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index d34e8b96..44d66096 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -16,7 +16,6 @@ use Phake; /** @covers \JKingWeb\Arsse\CLI */ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { - public function setUp() { self::clearData(false); } @@ -25,7 +24,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { $argv = \Clue\Arguments\split($command); $output = strlen($output) ? $output.\PHP_EOL : ""; if ($pattern) { - $this->expectOutputRegex($output); + $this->expectOutputRegex($output); } else { $this->expectOutputString($output); } diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php index 428b80c0..973ec351 100644 --- a/tests/cases/Database/Base.php +++ b/tests/cases/Database/Base.php @@ -15,7 +15,7 @@ use JKingWeb\Arsse\Db\Result; use JKingWeb\Arsse\Test\DatabaseInformation; use Phake; -abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ +abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest { use SeriesMiscellany; use SeriesMeta; use SeriesUser; @@ -34,7 +34,7 @@ abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ protected static $failureReason = ""; protected $primed = false; - protected abstract function nextID(string $table): int; + abstract protected function nextID(string $table): int; protected function findTraitOfTest(string $test): string { $class = new \ReflectionClass(self::class); @@ -50,7 +50,7 @@ abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest{ // establish a clean baseline static::clearData(); // perform an initial connection to the database to reset its version to zero - // in the case of SQLite this will always be the case (we use a memory database), + // in the case of SQLite this will always be the case (we use a memory database), // but other engines should clean up from potentially interrupted prior tests static::$dbInfo = new DatabaseInformation(static::$implementation); static::setConf(); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 96fd4224..c3c4425e 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -411,7 +411,7 @@ trait SeriesArticle { 305 => 105, 1001 => 20, ]; - $this->assertEquals($exp, Arsse::$db->editionArticle(...range(1,1001))); + $this->assertEquals($exp, Arsse::$db->editionArticle(...range(1, 1001))); } public function testListArticlesCheckingContext() { diff --git a/tests/cases/Database/SeriesSubscription.php b/tests/cases/Database/SeriesSubscription.php index ebaeea3e..f2811f1d 100644 --- a/tests/cases/Database/SeriesSubscription.php +++ b/tests/cases/Database/SeriesSubscription.php @@ -12,7 +12,6 @@ use JKingWeb\Arsse\Feed\Exception as FeedException; use Phake; trait SeriesSubscription { - public function setUpSeriesSubscription() { $this->data = [ 'arsse_users' => [ diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index af809342..f51a1215 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -76,7 +76,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { return static::$interface->query($q)->fetchColumn(); } - # TESTS + # TESTS public function testFetchDriverName() { $class = get_class($this->drv); @@ -115,7 +115,7 @@ 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; + $lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock; $this->drv->exec($lock); } @@ -144,7 +144,7 @@ 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; + $lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock; $this->drv->exec($lock); } diff --git a/tests/cases/Db/BaseStatement.php b/tests/cases/Db/BaseStatement.php index 4ac71df9..92750e95 100644 --- a/tests/cases/Db/BaseStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -275,7 +275,6 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { ]; foreach ($tests as $index => list($value, $type, $exp)) { $t = preg_replace("<^strict >", "", $type); - if (gettype($exp) != "string") var_export($index); $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); yield $index => [$value, $type, $exp]; } @@ -327,7 +326,6 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { ]; foreach ($tests as $index => list($value, $type, $exp)) { $t = preg_replace("<^strict >", "", $type); - if (gettype($exp) != "string") var_export($index); $exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); yield $index => [$value, $type, $exp]; } diff --git a/tests/cases/Db/PostgreSQL/TestCreation.php b/tests/cases/Db/PostgreSQL/TestCreation.php index fdc473b1..f9271d43 100644 --- a/tests/cases/Db/PostgreSQL/TestCreation.php +++ b/tests/cases/Db/PostgreSQL/TestCreation.php @@ -21,7 +21,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { if ($act==$postfix) { $this->assertSame($exp, ""); } else { - $test = substr($act, 0, strlen($act) - (strlen($postfix) + 1) ); + $test = substr($act, 0, strlen($act) - (strlen($postfix) + 1)); $check = substr($act, strlen($test) + 1); $this->assertSame($postfix, $check); $this->assertSame($exp, $test); diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index a8b2a65a..8d748ebb 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -6,7 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PosgreSQL; -/** +/** * @group excludeFromCoverage * @covers \JKingWeb\Arsse\Database * @covers \JKingWeb\Arsse\Misc\Query @@ -20,7 +20,7 @@ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { public function setUp() { parent::setUp(); - $seqList = + $seqList = "select replace(substring(column_default, 10), right(column_default, 12), '') as seq, table_name as table, @@ -30,7 +30,7 @@ class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { and table_name like 'arsse_%' and column_default like 'nextval(%' "; - foreach(static::$drv->query($seqList) as $r) { + foreach (static::$drv->query($seqList) as $r) { $num = (int) static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue(); if (!$num) { continue; diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php index d5f8b9d8..4385fb43 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -23,9 +23,8 @@ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { case "string": if (preg_match("<^char\((\d+)\)$>", $value, $match)) { return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; - } else { - return $value; } + return $value; default: return $value; } diff --git a/tests/cases/Db/SQLite3/TestDatabase.php b/tests/cases/Db/SQLite3/TestDatabase.php index d40dd880..35448d49 100644 --- a/tests/cases/Db/SQLite3/TestDatabase.php +++ b/tests/cases/Db/SQLite3/TestDatabase.php @@ -6,7 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\SQLite3; -/** +/** * @covers \JKingWeb\Arsse\Database * @covers \JKingWeb\Arsse\Misc\Query */ diff --git a/tests/cases/Db/SQLite3/TestResult.php b/tests/cases/Db/SQLite3/TestResult.php index 4109b8f1..b05287ee 100644 --- a/tests/cases/Db/SQLite3/TestResult.php +++ b/tests/cases/Db/SQLite3/TestResult.php @@ -8,8 +8,8 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3; use JKingWeb\Arsse\Test\DatabaseInformation; -/** - * @covers \JKingWeb\Arsse\Db\SQLite3\Result +/** + * @covers \JKingWeb\Arsse\Db\SQLite3\Result */ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $implementation = "SQLite 3"; diff --git a/tests/cases/Db/SQLite3PDO/TestDatabase.php b/tests/cases/Db/SQLite3PDO/TestDatabase.php index 1dca8b79..d52732c3 100644 --- a/tests/cases/Db/SQLite3PDO/TestDatabase.php +++ b/tests/cases/Db/SQLite3PDO/TestDatabase.php @@ -6,7 +6,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO; -/** +/** * @covers \JKingWeb\Arsse\Database * @covers \JKingWeb\Arsse\Misc\Query */ diff --git a/tests/cases/Db/TestResultPDO.php b/tests/cases/Db/TestResultPDO.php index f1792f84..7ad1edd9 100644 --- a/tests/cases/Db/TestResultPDO.php +++ b/tests/cases/Db/TestResultPDO.php @@ -8,8 +8,8 @@ namespace JKingWeb\Arsse\TestCase\Db; use JKingWeb\Arsse\Test\DatabaseInformation; -/** - * @covers \JKingWeb\Arsse\Db\PDOResult +/** + * @covers \JKingWeb\Arsse\Db\PDOResult */ class TestResultPDO extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $implementation; diff --git a/tests/cases/REST/TestREST.php b/tests/cases/REST/TestREST.php index 19e119b0..43799d19 100644 --- a/tests/cases/REST/TestREST.php +++ b/tests/cases/REST/TestREST.php @@ -153,7 +153,7 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest { public function testNegotiateCors($origin, bool $exp, string $allowed = null, string $denied = null) { self::setConf(); $r = Phake::partialMock(REST::class); - Phake::when($r)->corsNormalizeOrigin->thenReturnCallback(function ($origin) { + Phake::when($r)->corsNormalizeOrigin->thenReturnCallback(function($origin) { return $origin; }); $headers = isset($origin) ? ['Origin' => $origin] : []; @@ -255,10 +255,10 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest { public function testNormalizeHttpResponses(ResponseInterface $res, ResponseInterface $exp, RequestInterface $req = null) { $r = Phake::partialMock(REST::class); Phake::when($r)->corsNegotiate->thenReturn(true); - Phake::when($r)->challenge->thenReturnCallback(function ($res) { + Phake::when($r)->challenge->thenReturnCallback(function($res) { return $res->withHeader("WWW-Authenticate", "Fake Value"); }); - Phake::when($r)->corsApply->thenReturnCallback(function ($res) { + Phake::when($r)->corsApply->thenReturnCallback(function($res) { return $res; }); $act = $r->normalizeResponse($res, $req); @@ -298,10 +298,10 @@ class TestREST extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideMockRequests */ public function testDispatchRequests(ServerRequest $req, string $method, bool $called, string $class = "", string $target ="") { $r = Phake::partialMock(REST::class); - Phake::when($r)->normalizeResponse->thenReturnCallback(function ($res) { + Phake::when($r)->normalizeResponse->thenReturnCallback(function($res) { return $res; }); - Phake::when($r)->authenticateRequest->thenReturnCallback(function ($req) { + Phake::when($r)->authenticateRequest->thenReturnCallback(function($req) { return $req; }); if ($called) { diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index b9b1e2cc..ef3ebdc5 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -1516,13 +1516,13 @@ LONG_STRING; } protected function filterFolders(int $id = null): array { - return array_filter($this->folders, function ($value) use ($id) { + return array_filter($this->folders, function($value) use ($id) { return $value['parent']==$id; }); } protected function filterSubs(int $folder = null): array { - return array_filter($this->subscriptions, function ($value) use ($folder) { + return array_filter($this->subscriptions, function($value) use ($folder) { return $value['folder']==$folder; }); } @@ -1532,9 +1532,9 @@ LONG_STRING; foreach ($this->filterFolders($id) as $f) { $out += $this->reduceFolders($f['id']); } - $out += array_reduce(array_filter($this->subscriptions, function ($value) use ($id) { + $out += array_reduce(array_filter($this->subscriptions, function($value) use ($id) { return $value['folder']==$id; - }), function ($sum, $value) { + }), function($sum, $value) { return $sum + $value['unread']; }, 0); return $out; diff --git a/tests/cases/User/TestInternal.php b/tests/cases/User/TestInternal.php index bc43377f..f7f042dd 100644 --- a/tests/cases/User/TestInternal.php +++ b/tests/cases/User/TestInternal.php @@ -17,7 +17,6 @@ use Phake; /** @covers \JKingWeb\Arsse\User\Internal\Driver */ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { - public function setUp() { self::clearData(); self::setConf(); @@ -34,8 +33,8 @@ class TestInternal extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertTrue(strlen(Driver::driverName()) > 0); } - /** - * @dataProvider provideAuthentication + /** + * @dataProvider provideAuthentication * @group slow */ public function testAuthenticateAUser(bool $authorized, string $user, string $password, bool $exp) { diff --git a/tests/cases/User/TestUser.php b/tests/cases/User/TestUser.php index fbb47627..abe4b5a6 100644 --- a/tests/cases/User/TestUser.php +++ b/tests/cases/User/TestUser.php @@ -17,7 +17,6 @@ use Phake; /** @covers \JKingWeb\Arsse\User */ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { - public function setUp() { self::clearData(); self::setConf(); @@ -236,7 +235,7 @@ class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertException("doesNotExist", "User"); } $calls = 0; - } else{ + } else { $calls = 1; } try { diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php index 1eefa383..a550847b 100644 --- a/tests/lib/DatabaseInformation.php +++ b/tests/lib/DatabaseInformation.php @@ -80,7 +80,7 @@ class DatabaseInformation { // rollback any pending transaction try { $db->exec("ROLLBACK"); - } catch(\Throwable $e) { + } catch (\Throwable $e) { } foreach ($sqlite3TableList($db) as $table) { if ($table == "arsse_meta") { @@ -97,7 +97,7 @@ class DatabaseInformation { // rollback any pending transaction try { $db->exec("ROLLBACK"); - } catch(\Throwable $e) { + } catch (\Throwable $e) { } $db->exec("PRAGMA foreign_keys=0"); foreach ($sqlite3TableList($db) as $table) { @@ -181,7 +181,7 @@ class DatabaseInformation { // rollback any pending transaction try { $db->exec("ROLLBACK"); - } catch(\Throwable $e) { + } catch (\Throwable $e) { } foreach ($pgObjectList($db) as $obj) { if ($obj['type'] != "TABLE") { @@ -200,7 +200,7 @@ class DatabaseInformation { // rollback any pending transaction try { $db->exec("ROLLBACK"); - } catch(\Throwable $e) { + } catch (\Throwable $e) { } foreach ($pgObjectList($db) as $obj) { $db->exec("DROP {$obj['type']} IF EXISTS {$obj['name']} cascade"); From 089f666de625a5770303d4399b7bcfa6fe578409 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 6 Dec 2018 17:46:00 -0500 Subject: [PATCH 43/58] Fix PDO insert ID errors in PHP 7.1 --- lib/Db/PDODriver.php | 8 +---- lib/Db/PDOResult.php | 17 ++++++---- lib/Db/PDOStatement.php | 8 +---- lib/Db/PostgreSQL/PDODriver.php | 5 ++- lib/Db/SQLite3/PDODriver.php | 4 ++- tests/cases/Db/BaseDriver.php | 2 +- tests/cases/Db/BaseResult.php | 25 +++++++++------ tests/cases/Db/PostgreSQL/TestResult.php | 23 +++++++++++++ tests/cases/Db/SQLite3/TestResult.php | 2 ++ tests/cases/Db/SQLite3PDO/TestResult.php | 23 +++++++++++++ tests/cases/Db/TestResultPDO.php | 41 ------------------------ tests/phpunit.xml | 3 +- 12 files changed, 86 insertions(+), 75 deletions(-) create mode 100644 tests/cases/Db/PostgreSQL/TestResult.php create mode 100644 tests/cases/Db/SQLite3PDO/TestResult.php delete mode 100644 tests/cases/Db/TestResultPDO.php diff --git a/lib/Db/PDODriver.php b/lib/Db/PDODriver.php index e418cdca..413a0ccf 100644 --- a/lib/Db/PDODriver.php +++ b/lib/Db/PDODriver.php @@ -26,13 +26,7 @@ trait PDODriver { list($excClass, $excMsg, $excData) = $this->exceptionBuild(); throw new $excClass($excMsg, $excData); } - $changes = $r->rowCount(); - try { - $lastId = 0; - $lastId = ($changes) ? $this->db->lastInsertId() : 0; - } catch (\PDOException $e) { // @codeCoverageIgnore - } - return new PDOResult($r, [$changes, $lastId]); + return new PDOResult($this->db, $r); } public function prepareArray(string $query, array $paramTypes): Statement { diff --git a/lib/Db/PDOResult.php b/lib/Db/PDOResult.php index 8889511c..d817d019 100644 --- a/lib/Db/PDOResult.php +++ b/lib/Db/PDOResult.php @@ -10,26 +10,28 @@ use JKingWeb\Arsse\Db\Exception; class PDOResult extends AbstractResult { protected $set; + protected $db; protected $cur = null; - protected $rows = 0; - protected $id = 0; // actual public methods public function changes(): int { - return $this->rows; + return $this->set->rowCount(); } public function lastId(): int { - return $this->id; + try { + return (int) $this->db->lastInsertId(); + } catch (\PDOException $e) { + return 0; + } } // constructor/destructor - public function __construct(\PDOStatement $result, array $changes = [0,0]) { + public function __construct(\PDO $db, \PDOStatement $result) { $this->set = $result; - $this->rows = (int) $changes[0]; - $this->id = (int) $changes[1]; + $this->db = $db; } public function __destruct() { @@ -38,6 +40,7 @@ class PDOResult extends AbstractResult { } catch (\PDOException $e) { // @codeCoverageIgnore } unset($this->set); + unset($this->db); } // PHP iterator methods diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php index 26a50458..3a090150 100644 --- a/lib/Db/PDOStatement.php +++ b/lib/Db/PDOStatement.php @@ -40,13 +40,7 @@ class PDOStatement extends AbstractStatement { list($excClass, $excMsg, $excData) = $this->exceptionBuild(); throw new $excClass($excMsg, $excData); } - $changes = $this->st->rowCount(); - try { - $lastId = 0; - $lastId = ($changes) ? $this->db->lastInsertId() : 0; - } catch (\PDOException $e) { // @codeCoverageIgnore - } - return new PDOResult($this->st, [$changes, $lastId]); + return new PDOResult($this->db, $this->st); } protected function bindValue($value, string $type, int $position): bool { diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php index d9716dad..37763dd4 100644 --- a/lib/Db/PostgreSQL/PDODriver.php +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -22,7 +22,10 @@ class PDODriver extends Driver { protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { $dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port, $service); - $this->db = new \PDO("pgsql:$dsn", $user, $pass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $this->db = new \PDO("pgsql:$dsn", $user, $pass, [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + //\PDO::ATTR_PERSISTENT => true, + ]); } public function __destruct() { diff --git a/lib/Db/SQLite3/PDODriver.php b/lib/Db/SQLite3/PDODriver.php index 42e1cf83..cbad93c5 100644 --- a/lib/Db/SQLite3/PDODriver.php +++ b/lib/Db/SQLite3/PDODriver.php @@ -21,7 +21,9 @@ class PDODriver extends Driver { } protected function makeConnection(string $file, string $key) { - $this->db = new \PDO("sqlite:".$file, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $this->db = new \PDO("sqlite:".$file, "", "", [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + ]); } public function __destruct() { diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index f51a1215..fd8cb5a9 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -57,7 +57,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { } public static function tearDownAfterClass() { - static::$implementation = null; + static::$interface = null; static::$dbInfo = null; self::clearData(); } diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index de1ddf22..a2159e46 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -56,13 +56,20 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { } public function testGetChangeCountAndLastInsertId() { - $this->makeResult("CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)"); - $out = $this->makeResult("INSERT INTO arsse_meta(key,value) values('test', 1)"); - $rows = $out[1][0]; - $id = $out[1][1]; - $r = new $this->resultClass(...$out); - $this->assertSame((int) $rows, $r->changes()); - $this->assertSame((int) $id, $r->lastId()); + $this->makeResult(static::$createMeta); + $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")); + $this->assertSame(1, $r->changes()); + $this->assertSame(1, $r->lastId()); + $r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_test default values")); + $this->assertSame(1, $r->changes()); + $this->assertSame(2, $r->lastId()); } public function testIterateOverResults() { @@ -91,7 +98,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { public function testGetSingleValues() { $exp = [1867, 1970, 2112]; $exp = $this->stringOutput ? $this->stringify($exp) : $exp; - $test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year union select 1970 as year union select 2112 as year")); + $test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year union all select 1970 as year union all select 2112 as year")); $this->assertSame($exp[0], $test->getValue()); $this->assertSame($exp[1], $test->getValue()); $this->assertSame($exp[2], $test->getValue()); @@ -101,7 +108,7 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { public function testGetFirstValuesOnly() { $exp = [1867, 1970, 2112]; $exp = $this->stringOutput ? $this->stringify($exp) : $exp; - $test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year, 19 as century union select 1970 as year, 20 as century union select 2112 as year, 22 as century")); + $test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year, 19 as century union all select 1970 as year, 20 as century union all select 2112 as year, 22 as century")); $this->assertSame($exp[0], $test->getValue()); $this->assertSame($exp[1], $test->getValue()); $this->assertSame($exp[2], $test->getValue()); diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQL/TestResult.php new file mode 100644 index 00000000..06c10017 --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestResult.php @@ -0,0 +1,23 @@ + + */ +class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { + protected static $implementation = "PDO PostgreSQL"; + protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)"; + protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)"; + + protected function makeResult(string $q): array { + $set = static::$interface->query($q); + return [static::$interface, $set]; + } +} diff --git a/tests/cases/Db/SQLite3/TestResult.php b/tests/cases/Db/SQLite3/TestResult.php index b05287ee..1b34a1d2 100644 --- a/tests/cases/Db/SQLite3/TestResult.php +++ b/tests/cases/Db/SQLite3/TestResult.php @@ -13,6 +13,8 @@ use JKingWeb\Arsse\Test\DatabaseInformation; */ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $implementation = "SQLite 3"; + protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text) without rowid"; + protected static $createTest = "CREATE TABLE arsse_test(id integer primary key)"; public static function tearDownAfterClass() { if (static::$interface) { diff --git a/tests/cases/Db/SQLite3PDO/TestResult.php b/tests/cases/Db/SQLite3PDO/TestResult.php new file mode 100644 index 00000000..12b082e5 --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestResult.php @@ -0,0 +1,23 @@ + + */ +class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { + protected static $implementation = "PDO SQLite 3"; + protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text) without rowid"; + protected static $createTest = "CREATE TABLE arsse_test(id integer primary key)"; + + protected function makeResult(string $q): array { + $set = static::$interface->query($q); + return [static::$interface, $set]; + } +} diff --git a/tests/cases/Db/TestResultPDO.php b/tests/cases/Db/TestResultPDO.php deleted file mode 100644 index 7ad1edd9..00000000 --- a/tests/cases/Db/TestResultPDO.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ -class TestResultPDO extends \JKingWeb\Arsse\TestCase\Db\BaseResult { - protected static $implementation; - - public static function setUpBeforeClass() { - self::setConf(); - // we only need to test one PDO implementation (they all use the same result class), so we find the first usable one - $drivers = DatabaseInformation::listPDO(); - self::$implementation = $drivers[0]; - foreach ($drivers as $driver) { - $info = new DatabaseInformation($driver); - $interface = ($info->interfaceConstructor)(); - if ($interface) { - self::$implementation = $driver; - break; - } - } - unset($interface); - unset($info); - parent::setUpBeforeClass(); - } - - protected function makeResult(string $q): array { - $set = static::$interface->query($q); - $rows = $set->rowCount(); - $id = static::$interface->lastInsertID(); - return [$set, [$rows, $id]]; - } -} diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 29fe5e48..da4be238 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -45,7 +45,6 @@ cases/Db/TestTransaction.php cases/Db/TestResultAggregate.php cases/Db/TestResultEmpty.php - cases/Db/TestResultPDO.php cases/Db/SQLite3/TestResult.php cases/Db/SQLite3/TestStatement.php @@ -53,11 +52,13 @@ cases/Db/SQLite3/TestDriver.php cases/Db/SQLite3/TestUpdate.php + cases/Db/SQLite3PDO/TestResult.php cases/Db/SQLite3PDO/TestStatement.php cases/Db/SQLite3PDO/TestCreation.php cases/Db/SQLite3PDO/TestDriver.php cases/Db/SQLite3PDO/TestUpdate.php + cases/Db/PostgreSQL/TestResult.php cases/Db/PostgreSQL/TestStatement.php cases/Db/PostgreSQL/TestCreation.php cases/Db/PostgreSQL/TestDriver.php From 0513b606c2b4582e5edb49ce40c928e4938405ab Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 7 Dec 2018 19:21:44 -0500 Subject: [PATCH 44/58] Merge master --- CHANGELOG | 6 ++++++ lib/Arsse.php | 2 +- lib/Db/SQLite3/Driver.php | 10 +++++---- tests/cases/CLI/TestCLI.php | 10 ++++----- tests/cases/Database/Base.php | 2 +- tests/cases/Misc/TestValueInfo.php | 24 +++++++++++++++------ tests/cases/REST/NextCloudNews/TestV1_2.php | 2 +- tests/cases/REST/TinyTinyRSS/TestAPI.php | 22 +++++++++---------- tests/lib/AbstractTest.php | 4 ++-- tests/phpunit.xml | 1 - 10 files changed, 50 insertions(+), 33 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 449f514e..5601723e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +Version 0.5.1 (2018-11-10) +========================== + +Bug fixes: +- Correctly initialize PDO database driver + Version 0.5.0 (2018-11-07) ========================== diff --git a/lib/Arsse.php b/lib/Arsse.php index c459b288..39b83364 100644 --- a/lib/Arsse.php +++ b/lib/Arsse.php @@ -7,7 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse; class Arsse { - const VERSION = "0.5.0"; + const VERSION = "0.5.1"; /** @var Lang */ public static $lang; diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 238d1f68..5a666050 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -20,15 +20,17 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { protected $db; - public function __construct(string $dbFile = null) { + public function __construct(string $dbFile = null, string $dbKey = null) { // check to make sure required extension is loaded - if (!self::requirementsMet()) { - throw new Exception("extMissing", self::driverName()); // @codeCoverageIgnore + if (!static::requirementsMet()) { + throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore } // if no database file is specified in the configuration, use a suitable default $dbFile = $dbFile ?? Arsse::$conf->dbSQLite3File ?? \JKingWeb\Arsse\BASE."arsse.db"; + $dbKey = $dbKey ?? Arsse::$conf->dbSQLite3Key; + $timeout = Arsse::$conf->dbSQLite3Timeout * 1000; try { - $this->makeConnection($dbFile, Arsse::$conf->dbSQLite3Key); + $this->makeConnection($dbFile, $dbKey); } catch (\Throwable $e) { // if opening the database doesn't work, check various pre-conditions to find out what the problem might be $files = [ diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index 44d66096..ba4d1d0a 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -114,7 +114,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserList */ public function testListUsers(string $cmd, array $list, int $exitStatus, string $output) { - // Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead + // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("list")->willReturn($list); $this->assertConsole(new CLI, $cmd, $exitStatus, $output); @@ -133,7 +133,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserAdditions */ public function testAddAUser(string $cmd, int $exitStatus, string $output) { - // Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead + // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("add")->will($this->returnCallback(function($user, $pass = null) { switch ($user) { @@ -156,7 +156,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserAuthentication */ public function testAuthenticateAUser(string $cmd, int $exitStatus, string $output) { - // Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead + // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("auth")->will($this->returnCallback(function($user, $pass) { return ( @@ -179,7 +179,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserRemovals */ public function testRemoveAUser(string $cmd, int $exitStatus, string $output) { - // Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead + // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("remove")->will($this->returnCallback(function($user) { if ($user == "john.doe@example.com") { @@ -199,7 +199,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideUserPasswordChanges */ public function testChangeAUserPassword(string $cmd, int $exitStatus, string $output) { - // Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead + // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("passwordSet")->will($this->returnCallback(function($user, $pass = null) { switch ($user) { diff --git a/tests/cases/Database/Base.php b/tests/cases/Database/Base.php index 973ec351..d148793a 100644 --- a/tests/cases/Database/Base.php +++ b/tests/cases/Database/Base.php @@ -225,7 +225,7 @@ abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest { $found = array_search($row, $expected); unset($expected[$found]); } - $this->assertArraySubset($expected, [], "Expectations not in result set."); + $this->assertArraySubset($expected, [], false, "Expectations not in result set."); } } } diff --git a/tests/cases/Misc/TestValueInfo.php b/tests/cases/Misc/TestValueInfo.php index 2d0973e1..60c7da08 100644 --- a/tests/cases/Misc/TestValueInfo.php +++ b/tests/cases/Misc/TestValueInfo.php @@ -411,26 +411,36 @@ class TestValueInfo extends \JKingWeb\Arsse\Test\AbstractTest { [I::T_STRING, "String", ], [I::T_ARRAY, "Array", ], ]; + $assert = function($exp, $act, string $msg) { + if (is_null($exp)) { + $this->assertNull($act, $msg); + } elseif (is_float($exp) && is_nan($exp)) { + $this->assertNan($act, $msg); + } elseif (is_scalar($exp)) { + $this->assertSame($exp, $act, $msg); + } else { + $this->assertEquals($exp, $act, $msg); + } + }; foreach ($params as $index => $param) { list($type, $name) = $param; - $this->assertNull(I::normalize(null, $type | I::M_STRICT | I::M_NULL), $name." null-passthrough test failed"); + $assert(null, I::normalize(null, $type | I::M_STRICT | I::M_NULL), $name." null-passthrough test failed"); foreach ($tests as $test) { list($exp, $pass) = $index ? $test[$index] : [$test[$index], true]; $value = $test[0]; - $assert = (is_float($exp) && is_nan($exp) ? "assertNan" : (is_scalar($exp) ? "assertSame" : "assertEquals")); - $this->$assert($exp, I::normalize($value, $type), $name." test failed for value: ".var_export($value, true)); + $assert($exp, I::normalize($value, $type), $name." test failed for value: ".var_export($value, true)); if ($pass) { - $this->$assert($exp, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); - $this->$assert($exp, I::normalize($value, $type | I::M_STRICT), $name." error test failed for value: ".var_export($value, true)); + $assert($exp, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); + $assert($exp, I::normalize($value, $type | I::M_STRICT), $name." error test failed for value: ".var_export($value, true)); } else { - $this->assertNull(I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); + $assert(null, I::normalize($value, $type | I::M_DROP), $name." drop test failed for value: ".var_export($value, true)); $exc = new ExceptionType("strictFailure", $type); try { $act = I::normalize($value, $type | I::M_STRICT); } catch (ExceptionType $e) { $act = $e; } finally { - $this->assertEquals($exc, $act, $name." error test failed for value: ".var_export($value, true)); + $assert($exc, $act, $name." error test failed for value: ".var_export($value, true)); } } } diff --git a/tests/cases/REST/NextCloudNews/TestV1_2.php b/tests/cases/REST/NextCloudNews/TestV1_2.php index 9d8167cb..6e9b90af 100644 --- a/tests/cases/REST/NextCloudNews/TestV1_2.php +++ b/tests/cases/REST/NextCloudNews/TestV1_2.php @@ -768,7 +768,7 @@ class TestV1_2 extends \JKingWeb\Arsse\Test\AbstractTest { Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(false)->limit(10)->oldestEdition(6), $this->anything()); // offset is one more than specified Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5)->latestEdition(4), $this->anything()); // offset is one less than specified Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->unread(true), $this->anything()); - Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->markedSince($t), $this->anything()); + Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, $this->equalTo((new Context)->reverse(true)->markedSince($t), 2), $this->anything()); Phake::verify(Arsse::$db)->articleList(Arsse::$user->id, (new Context)->reverse(true)->limit(5), $this->anything()); } diff --git a/tests/cases/REST/TinyTinyRSS/TestAPI.php b/tests/cases/REST/TinyTinyRSS/TestAPI.php index ef3ebdc5..8ba992ba 100644 --- a/tests/cases/REST/TinyTinyRSS/TestAPI.php +++ b/tests/cases/REST/TinyTinyRSS/TestAPI.php @@ -1133,7 +1133,7 @@ LONG_STRING; Phake::when(Arsse::$db)->folderList($this->anything(), null, false)->thenReturn(new Result($this->v($this->topFolders))); Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->v($this->subscriptions))); Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->v($this->labels))); - Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context + Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); $exp = [ [ @@ -1197,7 +1197,7 @@ LONG_STRING; Phake::when(Arsse::$db)->folderList($this->anything())->thenReturn(new Result($this->v($this->folders))); Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->v($this->subscriptions))); Phake::when(Arsse::$db)->labelList($this->anything(), false)->thenReturn(new Result($this->v($this->usedLabels))); - Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context + Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); $exp = [ ['id' => "global-unread", 'counter' => 35], @@ -1318,7 +1318,7 @@ LONG_STRING; Phake::when(Arsse::$db)->folderList($this->anything(), null, true)->thenReturn(new Result($this->v($this->folders))); Phake::when(Arsse::$db)->subscriptionList($this->anything())->thenReturn(new Result($this->v($this->subscriptions))); Phake::when(Arsse::$db)->labelList($this->anything(), true)->thenReturn(new Result($this->v($this->labels))); - Phake::when(Arsse::$db)->articleCount($this->anything(), $this->anything())->thenReturn(7); // FIXME: this should check an unread+modifiedSince context + Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); // the expectations are packed tightly since they're very verbose; one can use var_export() (or convert to JSON) to pretty-print them $exp = ['categories'=>['identifier'=>'id','label'=>'name','items'=>[['name'=>'Special','id'=>'CAT:-1','bare_id'=>-1,'type'=>'category','unread'=>0,'items'=>[['name'=>'All articles','id'=>'FEED:-4','bare_id'=>-4,'icon'=>'images/folder.png','unread'=>35,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Fresh articles','id'=>'FEED:-3','bare_id'=>-3,'icon'=>'images/fresh.png','unread'=>7,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Starred articles','id'=>'FEED:-1','bare_id'=>-1,'icon'=>'images/star.png','unread'=>4,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Published articles','id'=>'FEED:-2','bare_id'=>-2,'icon'=>'images/feed.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Archived articles','id'=>'FEED:0','bare_id'=>0,'icon'=>'images/archive.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],['name'=>'Recently read','id'=>'FEED:-6','bare_id'=>-6,'icon'=>'images/time.png','unread'=>0,'type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'',],],],['name'=>'Labels','id'=>'CAT:-2','bare_id'=>-2,'type'=>'category','unread'=>6,'items'=>[['name'=>'Fascinating','id'=>'FEED:-1027','bare_id'=>-1027,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Interesting','id'=>'FEED:-1029','bare_id'=>-1029,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],['name'=>'Logical','id'=>'FEED:-1025','bare_id'=>-1025,'unread'=>0,'icon'=>'images/label.png','type'=>'feed','auxcounter'=>0,'error'=>'','updated'=>'','fg_color'=>'','bg_color'=>'',],],],['name'=>'Photography','id'=>'CAT:4','bare_id'=>4,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(0 feeds)','items'=>[],],['name'=>'Politics','id'=>'CAT:3','bare_id'=>3,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(3 feeds)','items'=>[['name'=>'Local','id'=>'CAT:5','bare_id'=>5,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'Toronto Star','id'=>'FEED:2','bare_id'=>2,'icon'=>'feed-icons/2.ico','error'=>'oops','param'=>'2011-11-11T11:11:11Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'National','id'=>'CAT:6','bare_id'=>6,'parent_id'=>3,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'CBC News','id'=>'FEED:4','bare_id'=>4,'icon'=>'feed-icons/4.ico','error'=>'','param'=>'2017-10-09T15:58:34Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],['name'=>'Ottawa Citizen','id'=>'FEED:5','bare_id'=>5,'icon'=>false,'error'=>'','param'=>'2017-07-07T17:07:17Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],['name'=>'Science','id'=>'CAT:1','bare_id'=>1,'parent_id'=>null,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(2 feeds)','items'=>[['name'=>'Rocketry','id'=>'CAT:2','bare_id'=>2,'parent_id'=>1,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'param'=>'(1 feed)','items'=>[['name'=>'NASA JPL','id'=>'FEED:1','bare_id'=>1,'icon'=>false,'error'=>'','param'=>'2017-09-15T22:54:16Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Ars Technica','id'=>'FEED:3','bare_id'=>3,'icon'=>'feed-icons/3.ico','error'=>'argh','param'=>'2016-05-23T06:40:02Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],['name'=>'Uncategorized','id'=>'CAT:0','bare_id'=>0,'type'=>'category','auxcounter'=>0,'unread'=>0,'child_unread'=>0,'checkbox'=>false,'parent_id'=>null,'param'=>'(1 feed)','items'=>[['name'=>'Eurogamer','id'=>'FEED:6','bare_id'=>6,'icon'=>'feed-icons/6.ico','error'=>'','param'=>'2010-02-12T20:08:47Z','unread'=>0,'auxcounter'=>0,'checkbox'=>false,],],],],],]; @@ -1375,7 +1375,7 @@ LONG_STRING; for ($a = 0; $a < sizeof($in3); $a++) { $this->assertMessage($exp, $this->req($in3[$a]), "Test $a failed"); } - Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], (new Context)->modifiedSince($t)); + Phake::verify(Arsse::$db)->articleMark($this->anything(), ['read' => true], $this->equalTo((new Context)->modifiedSince($t), 2)); // within two seconds } public function testRetrieveFeedList() { @@ -1405,7 +1405,7 @@ LONG_STRING; ]; // statistical mocks Phake::when(Arsse::$db)->articleStarred($this->anything())->thenReturn($this->v($this->starred)); - Phake::when(Arsse::$db)->articleCount->thenReturn(7); // FIXME: this should check an unread+modifiedSince context + Phake::when(Arsse::$db)->articleCount($this->anything(), $this->equalTo((new Context)->unread(true)->modifiedSince(Date::sub("PT24H")), 2))->thenReturn(7); Phake::when(Arsse::$db)->articleCount($this->anything(), (new Context)->unread(true))->thenReturn(35); // label mocks Phake::when(Arsse::$db)->labelList($this->anything())->thenReturn(new Result($this->v($this->labels))); @@ -1793,9 +1793,9 @@ LONG_STRING; $this->assertMessage($out1[$a], $this->req($in1[$a]), "Test $a failed"); } for ($a = 0; $a < sizeof($in2); $a++) { - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), ["id"])->thenReturn(new Result($this->v([['id' => 1001]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), ["id"])->thenReturn(new Result($this->v([['id' => 1002]]))); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), ["id"])->thenReturn(new Result($this->v([['id' => 1003]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(false)->markedSince(Date::sub("PT24H")), 2), ["id"])->thenReturn(new Result($this->v([['id' => 1001]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), 2), ["id"])->thenReturn(new Result($this->v([['id' => 1002]]))); + Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), 2), ["id"])->thenReturn(new Result($this->v([['id' => 1003]]))); $this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed"); } } @@ -1904,9 +1904,9 @@ LONG_STRING; $this->assertMessage($out2[$a], $this->req($in2[$a]), "Test $a failed"); } for ($a = 0; $a < sizeof($in3); $a++) { - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(false)->markedSince(Date::sub("PT24H")), $this->anything())->thenReturn($this->generateHeadlines(1001)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), $this->anything())->thenReturn($this->generateHeadlines(1002)); - Phake::when(Arsse::$db)->articleList($this->anything(), (clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), $this->anything())->thenReturn($this->generateHeadlines(1003)); + Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(false)->markedSince(Date::sub("PT24H")), 2), $this->anything())->thenReturn($this->generateHeadlines(1001)); + Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H")), 2), $this->anything())->thenReturn($this->generateHeadlines(1002)); + Phake::when(Arsse::$db)->articleList($this->anything(), $this->equalTo((clone $c)->unread(true)->modifiedSince(Date::sub("PT24H"))->starred(true), 2), $this->anything())->thenReturn($this->generateHeadlines(1003)); $this->assertMessage($out3[$a], $this->req($in3[$a]), "Test $a failed"); } } diff --git a/tests/lib/AbstractTest.php b/tests/lib/AbstractTest.php index 3e9af64f..9ec5ae57 100644 --- a/tests/lib/AbstractTest.php +++ b/tests/lib/AbstractTest.php @@ -69,7 +69,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { } } - protected function assertMessage(MessageInterface $exp, MessageInterface $act, string $text = null) { + protected function assertMessage(MessageInterface $exp, MessageInterface $act, string $text = '') { if ($exp instanceof ResponseInterface) { $this->assertInstanceOf(ResponseInterface::class, $act, $text); $this->assertEquals($exp->getStatusCode(), $act->getStatusCode(), $text); @@ -91,7 +91,7 @@ abstract class AbstractTest extends \PHPUnit\Framework\TestCase { $this->assertEquals($exp->getHeaders(), $act->getHeaders(), $text); } - public function assertTime($exp, $test, string $msg = null) { + public function assertTime($exp, $test, string $msg = '') { $test = $this->approximateTime($exp, $test); $exp = Date::transform($exp, "iso8601"); $test = Date::transform($test, "iso8601"); diff --git a/tests/phpunit.xml b/tests/phpunit.xml index da4be238..2733f334 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -7,7 +7,6 @@ convertWarningsToExceptions="false" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" - beStrictAboutTestSize="true" stopOnError="true"> From f6966659a9b7560a6d46a22892e9dad28c10c531 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 7 Dec 2018 20:03:04 -0500 Subject: [PATCH 45/58] Use smarter coverage executer; properly suppress stderr during CLI tests --- RoboFile.php | 18 ++++++++++-------- lib/CLI.php | 7 ++++++- tests/cases/CLI/TestCLI.php | 30 +++++++++++++++--------------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index 2a5dfda2..e957860b 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -81,13 +81,16 @@ class RoboFile extends \Robo\Tasks { } protected function findCoverageEngine(): string { - $null = null; - $code = 0; - exec("phpdbg --version", $null, $code); - if (!$code) { - return "phpdbg -qrr"; + if ($this->isWindows()) { + $dbg = dirname(\PHP_BINARY)."\\phpdbg.exe"; + $dbg = file_exists($dbg) ? $dbg : ""; } else { - return "php"; + $dbg = `which phpdbg`; + } + if ($dbg) { + return escapeshellarg($dbg)." -qrr"; + } else { + return escapeshellarg(\PHP_BINARY); } } @@ -114,9 +117,8 @@ class RoboFile extends \Robo\Tasks { } $execpath = realpath(self::BASE."vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit"); $confpath = realpath(self::BASE_TEST."phpunit.xml"); - $blackhole = $this->isWindows() ? "nul" : "/dev/null"; $this->taskServer(8000)->host("localhost")->dir(self::BASE_TEST."docroot")->rawArg("-n")->arg(self::BASE_TEST."server.php")->background()->run(); - return $this->taskExec($executor)->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->rawArg("2>$blackhole")->run(); + return $this->taskExec($executor)->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run(); } /** Packages a given commit of the software into a release tarball diff --git a/lib/CLI.php b/lib/CLI.php index 0ad8e537..7f9deacb 100644 --- a/lib/CLI.php +++ b/lib/CLI.php @@ -81,11 +81,16 @@ USAGE_TEXT; return $this->userManage($args); } } catch (AbstractException $e) { - fwrite(STDERR, $e->getMessage().\PHP_EOL); + $this->logError($e->getMessage()); return $e->getCode(); } } + /** @codeCoverageIgnore */ + protected function logError(string $msg) { + fwrite(STDERR,$msg.\PHP_EOL); + } + /** @codeCoverageIgnore */ protected function getService(): Service { return new Service; diff --git a/tests/cases/CLI/TestCLI.php b/tests/cases/CLI/TestCLI.php index ba4d1d0a..789767a6 100644 --- a/tests/cases/CLI/TestCLI.php +++ b/tests/cases/CLI/TestCLI.php @@ -18,6 +18,8 @@ use Phake; class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { public function setUp() { self::clearData(false); + $this->cli = Phake::partialMock(CLI::class); + Phake::when($this->cli)->logError->thenReturn(null); } public function assertConsole(CLI $cli, string $command, int $exitStatus, string $output = "", bool $pattern = false) { @@ -44,13 +46,13 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { } public function testPrintVersion() { - $this->assertConsole(new CLI, "arsse.php --version", 0, Arsse::VERSION); + $this->assertConsole($this->cli, "arsse.php --version", 0, Arsse::VERSION); $this->assertLoaded(false); } /** @dataProvider provideHelpText */ public function testPrintHelp(string $cmd, string $name) { - $this->assertConsole(new CLI, $cmd, 0, str_replace("arsse.php", $name, CLI::USAGE)); + $this->assertConsole($this->cli, $cmd, 0, str_replace("arsse.php", $name, CLI::USAGE)); $this->assertLoaded(false); } @@ -64,13 +66,12 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { public function testStartTheDaemon() { $srv = Phake::mock(Service::class); - $cli = Phake::partialMock(CLI::class); Phake::when($srv)->watch->thenReturn(new \DateTimeImmutable); - Phake::when($cli)->getService->thenReturn($srv); - $this->assertConsole($cli, "arsse.php daemon", 0); + Phake::when($this->cli)->getService->thenReturn($srv); + $this->assertConsole($this->cli, "arsse.php daemon", 0); $this->assertLoaded(true); Phake::verify($srv)->watch(true); - Phake::verify($cli)->getService; + Phake::verify($this->cli)->getService; } /** @dataProvider provideFeedUpdates */ @@ -78,7 +79,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { Arsse::$db = Phake::mock(Database::class); Phake::when(Arsse::$db)->feedUpdate(1, true)->thenReturn(true); Phake::when(Arsse::$db)->feedUpdate(2, true)->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/", new \PicoFeed\Client\InvalidUrlException)); - $this->assertConsole(new CLI, $cmd, $exitStatus, $output); + $this->assertConsole($this->cli, $cmd, $exitStatus, $output); $this->assertLoaded(true); Phake::verify(Arsse::$db)->feedUpdate; } @@ -93,12 +94,11 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideDefaultConfigurationSaves */ public function testSaveTheDefaultConfiguration(string $cmd, int $exitStatus, string $file) { $conf = Phake::mock(Conf::class); - $cli = Phake::partialMock(CLI::class); Phake::when($conf)->exportFile("php://output", true)->thenReturn(true); Phake::when($conf)->exportFile("good.conf", true)->thenReturn(true); Phake::when($conf)->exportFile("bad.conf", true)->thenThrow(new \JKingWeb\Arsse\Conf\Exception("fileUnwritable")); - Phake::when($cli)->getConf->thenReturn($conf); - $this->assertConsole($cli, $cmd, $exitStatus); + Phake::when($this->cli)->getConf->thenReturn($conf); + $this->assertConsole($this->cli, $cmd, $exitStatus); $this->assertLoaded(false); Phake::verify($conf)->exportFile($file, true); } @@ -117,7 +117,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { // FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead Arsse::$user = $this->createMock(User::class); Arsse::$user->method("list")->willReturn($list); - $this->assertConsole(new CLI, $cmd, $exitStatus, $output); + $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } public function provideUserList() { @@ -143,7 +143,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { return is_null($pass) ? "random password" : $pass; } })); - $this->assertConsole(new CLI, $cmd, $exitStatus, $output); + $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } public function provideUserAdditions() { @@ -164,7 +164,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { ($user == "jane.doe@example.com" && $pass == "superman") ); })); - $this->assertConsole(new CLI, $cmd, $exitStatus, $output); + $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } public function provideUserAuthentication() { @@ -187,7 +187,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { } throw new \JKingWeb\Arsse\User\Exception("doesNotExist"); })); - $this->assertConsole(new CLI, $cmd, $exitStatus, $output); + $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } public function provideUserRemovals() { @@ -209,7 +209,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest { return is_null($pass) ? "random password" : $pass; } })); - $this->assertConsole(new CLI, $cmd, $exitStatus, $output); + $this->assertConsole($this->cli, $cmd, $exitStatus, $output); } public function provideUserPasswordChanges() { From 913cf71620f1995b2726da42bd6b4ef7304760f6 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 7 Dec 2018 20:36:20 -0500 Subject: [PATCH 46/58] Fix incorrect annotations --- tests/cases/Db/PostgreSQL/TestResult.php | 2 +- tests/cases/Db/SQLite3PDO/TestResult.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQL/TestResult.php index 06c10017..de0aca2b 100644 --- a/tests/cases/Db/PostgreSQL/TestResult.php +++ b/tests/cases/Db/PostgreSQL/TestResult.php @@ -9,7 +9,7 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; use JKingWeb\Arsse\Test\DatabaseInformation; /** - * @covers \JKingWeb\Arsse\Db\ResultPDO + * @covers \JKingWeb\Arsse\Db\PDOResult */ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $implementation = "PDO PostgreSQL"; diff --git a/tests/cases/Db/SQLite3PDO/TestResult.php b/tests/cases/Db/SQLite3PDO/TestResult.php index 12b082e5..4161c44d 100644 --- a/tests/cases/Db/SQLite3PDO/TestResult.php +++ b/tests/cases/Db/SQLite3PDO/TestResult.php @@ -9,7 +9,7 @@ namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO; use JKingWeb\Arsse\Test\DatabaseInformation; /** - * @covers \JKingWeb\Arsse\Db\ResultPDO + * @covers \JKingWeb\Arsse\Db\PDOResult */ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $implementation = "PDO SQLite 3"; From 35d46d2913a7dc3855f4d45a1581644064f04631 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 10 Dec 2018 12:28:43 -0500 Subject: [PATCH 47/58] Use persistent connections with PostgreSQL --- lib/Db/PostgreSQL/PDODriver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php index 37763dd4..d6d44769 100644 --- a/lib/Db/PostgreSQL/PDODriver.php +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -24,7 +24,7 @@ class PDODriver extends Driver { $dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port, $service); $this->db = new \PDO("pgsql:$dsn", $user, $pass, [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - //\PDO::ATTR_PERSISTENT => true, + \PDO::ATTR_PERSISTENT => true, ]); } From 8dbf2376263a0fb1ac2b77707a0083df61c29777 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 10 Dec 2018 12:39:09 -0500 Subject: [PATCH 48/58] Group PostgreSQL tests as slow --- RoboFile.php | 2 +- tests/cases/Db/PostgreSQL/TestCreation.php | 1 + tests/cases/Db/PostgreSQL/TestDatabase.php | 3 ++- tests/cases/Db/PostgreSQL/TestDriver.php | 1 + tests/cases/Db/PostgreSQL/TestResult.php | 1 + tests/cases/Db/PostgreSQL/TestStatement.php | 1 + tests/cases/Db/PostgreSQL/TestUpdate.php | 1 + 7 files changed, 8 insertions(+), 2 deletions(-) diff --git a/RoboFile.php b/RoboFile.php index e957860b..e73d3e48 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -107,7 +107,7 @@ class RoboFile extends \Robo\Tasks { $set = ["--exclude-group", "optional,slow"]; break; case "coverage": - $set = ["--exclude-group", "optional,excludeFromCoverage"]; + $set = ["--exclude-group", "optional,coverageOptional"]; break; case "full": $set = []; diff --git a/tests/cases/Db/PostgreSQL/TestCreation.php b/tests/cases/Db/PostgreSQL/TestCreation.php index f9271d43..1abdf45d 100644 --- a/tests/cases/Db/PostgreSQL/TestCreation.php +++ b/tests/cases/Db/PostgreSQL/TestCreation.php @@ -10,6 +10,7 @@ use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Db\PostgreSQL\Driver; /** + * @group slow * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver */ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideConnectionStrings */ diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php index 8d748ebb..7ea7fc89 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -7,7 +7,8 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PosgreSQL; /** - * @group excludeFromCoverage + * @group slow + * @group coverageOptional * @covers \JKingWeb\Arsse\Database * @covers \JKingWeb\Arsse\Misc\Query */ diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php index ae4c4b2d..dadf9cbd 100644 --- a/tests/cases/Db/PostgreSQL/TestDriver.php +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; /** + * @group slow * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver * @covers \JKingWeb\Arsse\Db\PDODriver * @covers \JKingWeb\Arsse\Db\PDOError */ diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQL/TestResult.php index de0aca2b..0d85bdd6 100644 --- a/tests/cases/Db/PostgreSQL/TestResult.php +++ b/tests/cases/Db/PostgreSQL/TestResult.php @@ -9,6 +9,7 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; use JKingWeb\Arsse\Test\DatabaseInformation; /** + * @group slow * @covers \JKingWeb\Arsse\Db\PDOResult */ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php index 4385fb43..ba56a58b 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; /** + * @group slow * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDOStatement * @covers \JKingWeb\Arsse\Db\PDOError */ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { diff --git a/tests/cases/Db/PostgreSQL/TestUpdate.php b/tests/cases/Db/PostgreSQL/TestUpdate.php index 62f9140b..21bf1209 100644 --- a/tests/cases/Db/PostgreSQL/TestUpdate.php +++ b/tests/cases/Db/PostgreSQL/TestUpdate.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; /** + * @group slow * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver * @covers \JKingWeb\Arsse\Db\PDOError */ class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { From 73729a6be8a046e89f55902e6361d2a40732126a Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 10 Dec 2018 13:17:04 -0500 Subject: [PATCH 49/58] Simplify database cleanup between tests --- tests/cases/Db/BaseDriver.php | 8 ++++---- tests/cases/Db/BaseResult.php | 10 +++++----- tests/cases/Db/BaseStatement.php | 10 +++++----- tests/cases/Db/BaseUpdate.php | 10 +++++----- tests/cases/Db/SQLite3/TestDriver.php | 5 ++--- tests/cases/Db/SQLite3/TestResult.php | 5 ++--- tests/cases/Db/SQLite3/TestStatement.php | 1 + tests/cases/Db/SQLite3/TestUpdate.php | 6 ++++++ 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/tests/cases/Db/BaseDriver.php b/tests/cases/Db/BaseDriver.php index fd8cb5a9..5f309916 100644 --- a/tests/cases/Db/BaseDriver.php +++ b/tests/cases/Db/BaseDriver.php @@ -49,14 +49,14 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { public function tearDown() { // deconstruct the driver unset($this->drv); - if (static::$interface) { - // completely clear the database - (static::$dbInfo->razeFunction)(static::$interface); - } self::clearData(); } public static function tearDownAfterClass() { + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } static::$interface = null; static::$dbInfo = null; self::clearData(); diff --git a/tests/cases/Db/BaseResult.php b/tests/cases/Db/BaseResult.php index a2159e46..bb1725bf 100644 --- a/tests/cases/Db/BaseResult.php +++ b/tests/cases/Db/BaseResult.php @@ -38,15 +38,15 @@ abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - if (static::$interface) { - // completely clear the database - (static::$dbInfo->razeFunction)(static::$interface); - } self::clearData(); } public static function tearDownAfterClass() { - static::$implementation = null; + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } + static::$interface = null; static::$dbInfo = null; self::clearData(); } diff --git a/tests/cases/Db/BaseStatement.php b/tests/cases/Db/BaseStatement.php index 92750e95..c3c3704e 100644 --- a/tests/cases/Db/BaseStatement.php +++ b/tests/cases/Db/BaseStatement.php @@ -39,15 +39,15 @@ abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { } public function tearDown() { - if (static::$interface) { - // completely clear the database - (static::$dbInfo->razeFunction)(static::$interface); - } self::clearData(); } public static function tearDownAfterClass() { - static::$implementation = null; + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } + static::$interface = null; static::$dbInfo = null; self::clearData(); } diff --git a/tests/cases/Db/BaseUpdate.php b/tests/cases/Db/BaseUpdate.php index b4de0ea3..28cde468 100644 --- a/tests/cases/Db/BaseUpdate.php +++ b/tests/cases/Db/BaseUpdate.php @@ -48,16 +48,16 @@ class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest { public function tearDown() { // deconstruct the driver unset($this->drv); - if (static::$interface) { - // completely clear the database - (static::$dbInfo->razeFunction)(static::$interface); - } unset($this->path, $this->base, $this->vfs); self::clearData(); } public static function tearDownAfterClass() { - static::$implementation = null; + if (static::$interface) { + // completely clear the database + (static::$dbInfo->razeFunction)(static::$interface); + } + static::$interface = null; static::$dbInfo = null; self::clearData(); } diff --git a/tests/cases/Db/SQLite3/TestDriver.php b/tests/cases/Db/SQLite3/TestDriver.php index 9b2348a6..334a7843 100644 --- a/tests/cases/Db/SQLite3/TestDriver.php +++ b/tests/cases/Db/SQLite3/TestDriver.php @@ -25,9 +25,8 @@ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { } public static function tearDownAfterClass() { - if (static::$interface) { - static::$interface->close(); - } + static::$interface->close(); + static::$interface = null; parent::tearDownAfterClass(); @unlink(static::$file); static::$file = null; diff --git a/tests/cases/Db/SQLite3/TestResult.php b/tests/cases/Db/SQLite3/TestResult.php index 1b34a1d2..4f6780ac 100644 --- a/tests/cases/Db/SQLite3/TestResult.php +++ b/tests/cases/Db/SQLite3/TestResult.php @@ -17,9 +17,8 @@ class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { protected static $createTest = "CREATE TABLE arsse_test(id integer primary key)"; public static function tearDownAfterClass() { - if (static::$interface) { - static::$interface->close(); - } + static::$interface->close(); + static::$interface = null; parent::tearDownAfterClass(); } diff --git a/tests/cases/Db/SQLite3/TestStatement.php b/tests/cases/Db/SQLite3/TestStatement.php index 1684e832..7471b411 100644 --- a/tests/cases/Db/SQLite3/TestStatement.php +++ b/tests/cases/Db/SQLite3/TestStatement.php @@ -14,6 +14,7 @@ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { public static function tearDownAfterClass() { static::$interface->close(); + static::$interface = null; parent::tearDownAfterClass(); } diff --git a/tests/cases/Db/SQLite3/TestUpdate.php b/tests/cases/Db/SQLite3/TestUpdate.php index ecea58b9..53310850 100644 --- a/tests/cases/Db/SQLite3/TestUpdate.php +++ b/tests/cases/Db/SQLite3/TestUpdate.php @@ -13,4 +13,10 @@ class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { protected static $implementation = "SQLite 3"; protected static $minimal1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; protected static $minimal2 = "pragma user_version=2"; + + public static function tearDownAfterClass() { + static::$interface->close(); + static::$interface = null; + parent::tearDownAfterClass(); + } } From a8e648700149f4d93225b3487a00e04978a41c59 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Mon, 10 Dec 2018 19:13:48 -0500 Subject: [PATCH 50/58] Draft documentation --- CHANGELOG | 9 +++++++++ README.md | 10 ++++++++-- UPGRADING | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5601723e..4221df6c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +Version 0.6.0 (????-??-??) +========================== + +New features: +- Support for PostgreSQL databases (via PDO) + +Changes: +- Improve performance of common database queries by 80-90% + Version 0.5.1 (2018-11-10) ========================== diff --git a/README.md b/README.md index c8ae3135..9e55bd05 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The Arsse is a news aggregator server which implements multiple synchronization At present the software should be considered in an "alpha" state: though its core subsystems are covered by unit tests and should be free of major bugs, not everything has been rigorously tested. Additionally, many features one would expect from other similar software have yet to be implemented. Areas of future work include: -- Support for more database engines (PostgreSQL, MySQL, MariaDB) +- Support for more database engines (MySQL, MariaDB) - Providing more sync protocols (Google Reader, Fever, others) - Better packaging and configuration samples @@ -16,7 +16,9 @@ The Arsse has the following requirements: - PHP 7.0.7 or later with the following extensions: - [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [pcre](http://php.net/manual/en/book.pcre.php) - [dom](http://php.net/manual/en/book.dom.php), [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php) (for picoFeed) - - [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://ca1.php.net/manual/en/ref.pdo-sqlite.php) + - Either of: + - [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://ca1.php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases + - [pdo_pgsql](http://ca1.php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 9.1 or later databases - Privileges to create and run daemon processes on the server ## Installation @@ -69,6 +71,10 @@ The Arsse is made available under the permissive MIT license. See the `LICENSE` Please refer to `CONTRIBUTING.md` for guidelines on contributing code to The Arsse. +## Database compatibility notes + +Functionally there is no reason to prefer either SQLite or PostgreSQL over the other. SQLite, however, is significantly simpler to set up in most cases, requiring only read and write access to a containing directory in order to function. PostgreSQL may perform better than SQLite when serving hundreds of users or more, but this has not been tested. + ## Protocol compatibility notes ### General diff --git a/UPGRADING b/UPGRADING index 160574f9..3abe6eeb 100644 --- a/UPGRADING +++ b/UPGRADING @@ -9,6 +9,12 @@ When upgrading between any two versions of The Arsse, the following are usually - If installing from source, update dependencies with `composer install -o --no-dev` +Upgrading from 0.5.1 to 0.6.0 +============================= + +- The database schema has changed from rev3 to rev4; if upgrading the database manually, apply the 3.sql file + + Upgrading from 0.2.1 to 0.3.0 ============================= From 0f48ce6f3725458bbbb1a3a456771f3dea112350 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 11 Dec 2018 14:14:32 -0500 Subject: [PATCH 51/58] Use a Unicode collation for SQLite --- lib/Db/SQLite3/Driver.php | 4 ++++ sql/PostgreSQL/2.sql | 4 +--- sql/SQLite3/3.sql | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 5a666050..99254945 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -57,6 +57,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { $this->setTimeout($timeout); // set other initial options $this->exec("PRAGMA foreign_keys = yes"); + // use a case-insensitive Unicode collation sequence + $this->collator = new \Collator("@kf=false"); + $m = ($this->db instanceof \PDO) ? "sqliteCreateCollation" : "createCollation"; + $this->db->$m("nocase", [$this->collator, "compare"]); } public static function requirementsMet(): bool { diff --git a/sql/PostgreSQL/2.sql b/sql/PostgreSQL/2.sql index 021d3cd6..d37fcb50 100644 --- a/sql/PostgreSQL/2.sql +++ b/sql/PostgreSQL/2.sql @@ -4,9 +4,7 @@ -- Please consult the SQLite 3 schemata for commented version --- create a case-insensitive generic collation sequence --- this collation is Unicode-aware, whereas SQLite's built-in nocase --- collation is ASCII-only +-- create a case-insensitive generic Unicode collation sequence create collation nocase( provider = icu, locale = '@kf=false' diff --git a/sql/SQLite3/3.sql b/sql/SQLite3/3.sql index 063a2f1f..bac79a8b 100644 --- a/sql/SQLite3/3.sql +++ b/sql/SQLite3/3.sql @@ -19,6 +19,9 @@ create table arsse_marks( insert into arsse_marks select article,subscription,read,starred,modified,note,0 from arsse_marks_old; drop table arsse_marks_old; +-- reindex anything which uses the nocase collation sequence; it has been replaced with a Unicode collation +reindex nocase; + -- set version marker pragma user_version = 4; update arsse_meta set value = '4' where key = 'schema_version'; From 28f803dd284c1790b76637e9bb9efabb5b806fce Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 12 Dec 2018 11:15:07 -0500 Subject: [PATCH 52/58] Handle PostgreSQL connection errors --- lib/AbstractException.php | 1 + lib/Db/PostgreSQL/PDODriver.php | 20 ++++++++++++++++---- locale/en.php | 1 + tests/cases/Db/PostgreSQL/TestCreation.php | 13 +++++++++++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/lib/AbstractException.php b/lib/AbstractException.php index 38ff02bb..4eee0ffe 100644 --- a/lib/AbstractException.php +++ b/lib/AbstractException.php @@ -26,6 +26,7 @@ abstract class AbstractException extends \Exception { "Db/Exception.fileUnwritable" => 10205, "Db/Exception.fileUncreatable" => 10206, "Db/Exception.fileCorrupt" => 10207, + "Db/Exception.connectionFailure" => 10208, "Db/Exception.updateTooNew" => 10211, "Db/Exception.updateManual" => 10212, "Db/Exception.updateManualOnly" => 10213, diff --git a/lib/Db/PostgreSQL/PDODriver.php b/lib/Db/PostgreSQL/PDODriver.php index d6d44769..c4eebd56 100644 --- a/lib/Db/PostgreSQL/PDODriver.php +++ b/lib/Db/PostgreSQL/PDODriver.php @@ -22,10 +22,22 @@ class PDODriver extends Driver { protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { $dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port, $service); - $this->db = new \PDO("pgsql:$dsn", $user, $pass, [ - \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, - \PDO::ATTR_PERSISTENT => true, - ]); + try { + $this->db = new \PDO("pgsql:$dsn", $user, $pass, [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_PERSISTENT => true, + ]); + } catch (\PDOException $e) { + if ($e->getCode() == 7) { + switch (substr($e->getMessage(), 9, 5)) { + case "08006": + throw new Exception("connectionFailure", ["PostgreSQL", substr($e->getMessage(), 28)]); + default: + throw $e; // @codeCoverageIgnore + } + } + throw $e; // @codeCoverageIgnore + } } public function __destruct() { diff --git a/locale/en.php b/locale/en.php index 50b8b5fc..3e63ac69 100644 --- a/locale/en.php +++ b/locale/en.php @@ -122,6 +122,7 @@ return [ 'Exception.JKingWeb/Arsse/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing', 'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"', 'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database', + 'Exception.JKingWeb/Arsse/Db/Exception.connectionFailure' => 'Could not connect to {0} database: {1}', 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid', 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented', 'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified', diff --git a/tests/cases/Db/PostgreSQL/TestCreation.php b/tests/cases/Db/PostgreSQL/TestCreation.php index 1abdf45d..e22854ad 100644 --- a/tests/cases/Db/PostgreSQL/TestCreation.php +++ b/tests/cases/Db/PostgreSQL/TestCreation.php @@ -7,11 +7,11 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; use JKingWeb\Arsse\Arsse; -use JKingWeb\Arsse\Db\PostgreSQL\Driver; +use JKingWeb\Arsse\Db\PostgreSQL\PDODriver as Driver; /** * @group slow - * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver */ + * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver */ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { /** @dataProvider provideConnectionStrings */ public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { @@ -55,4 +55,13 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { [true, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"], ]; } + + public function testFailToConnect() { + // PDO dies not distinguish between different connection failure modes + self::setConf([ + 'dbPostgreSQLPass' => (string) rand(), + ]); + $this->assertException("connectionFailure", "Db"); + new Driver; + } } From 161f5f08f61e2396bc76664286386e8667c062ea Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 12 Dec 2018 12:21:28 -0500 Subject: [PATCH 53/58] Proactively support SQLite 3.25 --- lib/Db/SQLite3/Driver.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index 99254945..cee1b1ca 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -121,17 +121,15 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { public function schemaUpdate(int $to, string $basePath = null): bool { // turn off foreign keys $this->exec("PRAGMA foreign_keys = no"); + $this->exec("PRAGMA legacy_alter_table = yes"); // run the generic updater try { parent::schemaUpdate($to, $basePath); - } catch (\Throwable $e) { + } finally { // turn foreign keys back on $this->exec("PRAGMA foreign_keys = yes"); - // pass the exception up - throw $e; + $this->exec("PRAGMA legacy_alter_table = no"); } - // turn foreign keys back on - $this->exec("PRAGMA foreign_keys = yes"); return true; } From b52dadf345a392711584a276b55ac6653db24197 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Wed, 12 Dec 2018 12:42:40 -0500 Subject: [PATCH 54/58] Make existing PostgreSQL tests explicitly PDO tests --- .../Db/{PostgreSQL => PostgreSQLPDO}/TestCreation.php | 2 +- .../Db/{PostgreSQL => PostgreSQLPDO}/TestDatabase.php | 2 +- .../cases/Db/{PostgreSQL => PostgreSQLPDO}/TestDriver.php | 2 +- .../cases/Db/{PostgreSQL => PostgreSQLPDO}/TestResult.php | 2 +- .../Db/{PostgreSQL => PostgreSQLPDO}/TestStatement.php | 2 +- .../cases/Db/{PostgreSQL => PostgreSQLPDO}/TestUpdate.php | 2 +- tests/phpunit.xml | 7 +++++++ 7 files changed, 13 insertions(+), 6 deletions(-) rename tests/cases/Db/{PostgreSQL => PostgreSQLPDO}/TestCreation.php (98%) rename tests/cases/Db/{PostgreSQL => PostgreSQLPDO}/TestDatabase.php (96%) rename tests/cases/Db/{PostgreSQL => PostgreSQLPDO}/TestDriver.php (94%) rename tests/cases/Db/{PostgreSQL => PostgreSQLPDO}/TestResult.php (93%) rename tests/cases/Db/{PostgreSQL => PostgreSQLPDO}/TestStatement.php (95%) rename tests/cases/Db/{PostgreSQL => PostgreSQLPDO}/TestUpdate.php (92%) diff --git a/tests/cases/Db/PostgreSQL/TestCreation.php b/tests/cases/Db/PostgreSQLPDO/TestCreation.php similarity index 98% rename from tests/cases/Db/PostgreSQL/TestCreation.php rename to tests/cases/Db/PostgreSQLPDO/TestCreation.php index e22854ad..3561ff46 100644 --- a/tests/cases/Db/PostgreSQL/TestCreation.php +++ b/tests/cases/Db/PostgreSQLPDO/TestCreation.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; +namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; use JKingWeb\Arsse\Arsse; use JKingWeb\Arsse\Db\PostgreSQL\PDODriver as Driver; diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQLPDO/TestDatabase.php similarity index 96% rename from tests/cases/Db/PostgreSQL/TestDatabase.php rename to tests/cases/Db/PostgreSQLPDO/TestDatabase.php index 7ea7fc89..42d5f9f7 100644 --- a/tests/cases/Db/PostgreSQL/TestDatabase.php +++ b/tests/cases/Db/PostgreSQLPDO/TestDatabase.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\TestCase\Db\PosgreSQL; +namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; /** * @group slow diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQLPDO/TestDriver.php similarity index 94% rename from tests/cases/Db/PostgreSQL/TestDriver.php rename to tests/cases/Db/PostgreSQLPDO/TestDriver.php index dadf9cbd..fa043bc5 100644 --- a/tests/cases/Db/PostgreSQL/TestDriver.php +++ b/tests/cases/Db/PostgreSQLPDO/TestDriver.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; +namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; /** * @group slow diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQLPDO/TestResult.php similarity index 93% rename from tests/cases/Db/PostgreSQL/TestResult.php rename to tests/cases/Db/PostgreSQLPDO/TestResult.php index 0d85bdd6..86b4714d 100644 --- a/tests/cases/Db/PostgreSQL/TestResult.php +++ b/tests/cases/Db/PostgreSQLPDO/TestResult.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; +namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; use JKingWeb\Arsse\Test\DatabaseInformation; diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQLPDO/TestStatement.php similarity index 95% rename from tests/cases/Db/PostgreSQL/TestStatement.php rename to tests/cases/Db/PostgreSQLPDO/TestStatement.php index ba56a58b..900c2e8b 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQLPDO/TestStatement.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; +namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; /** * @group slow diff --git a/tests/cases/Db/PostgreSQL/TestUpdate.php b/tests/cases/Db/PostgreSQLPDO/TestUpdate.php similarity index 92% rename from tests/cases/Db/PostgreSQL/TestUpdate.php rename to tests/cases/Db/PostgreSQLPDO/TestUpdate.php index 21bf1209..92de5695 100644 --- a/tests/cases/Db/PostgreSQL/TestUpdate.php +++ b/tests/cases/Db/PostgreSQLPDO/TestUpdate.php @@ -4,7 +4,7 @@ * See LICENSE and AUTHORS files for details */ declare(strict_types=1); -namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; +namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; /** * @group slow diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 2733f334..ceca94ac 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -62,11 +62,18 @@ cases/Db/PostgreSQL/TestCreation.php cases/Db/PostgreSQL/TestDriver.php cases/Db/PostgreSQL/TestUpdate.php + + cases/Db/PostgreSQLPDO/TestResult.php + cases/Db/PostgreSQLPDO/TestStatement.php + cases/Db/PostgreSQLPDO/TestCreation.php + cases/Db/PostgreSQLPDO/TestDriver.php + cases/Db/PostgreSQLPDO/TestUpdate.php cases/Db/SQLite3/TestDatabase.php cases/Db/SQLite3PDO/TestDatabase.php cases/Db/PostgreSQL/TestDatabase.php + cases/Db/PostgreSQLPDO/TestDatabase.php cases/REST/TestTarget.php From 2bebdd44cf83ac6900f20d74473627587c6ef91b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 13 Dec 2018 19:47:51 -0500 Subject: [PATCH 55/58] Implementation of native PostgreSQL interface Changes to the Database class were required to avoid outputting booleans --- lib/Database.php | 8 +- lib/Db/PostgreSQL/Dispatch.php | 42 ++++++++ lib/Db/PostgreSQL/Driver.php | 51 ++++++--- lib/Db/PostgreSQL/PDOStatement.php | 28 +---- lib/Db/PostgreSQL/Result.php | 48 +++++++++ lib/Db/PostgreSQL/Statement.php | 77 ++++++++++++++ locale/en.php | 2 +- tests/cases/Db/PostgreSQL/TestCreation.php | 73 +++++++++++++ tests/cases/Db/PostgreSQL/TestDatabase.php | 43 ++++++++ tests/cases/Db/PostgreSQL/TestDriver.php | 57 ++++++++++ tests/cases/Db/PostgreSQL/TestResult.php | 33 ++++++ tests/cases/Db/PostgreSQL/TestStatement.php | 41 +++++++ tests/cases/Db/PostgreSQL/TestUpdate.php | 16 +++ tests/cases/Db/PostgreSQLPDO/TestCreation.php | 6 ++ tests/cases/Db/PostgreSQLPDO/TestDatabase.php | 1 + tests/cases/Db/SQLite3/TestDatabase.php | 1 + tests/lib/DatabaseInformation.php | 100 ++++++++++++------ 17 files changed, 547 insertions(+), 80 deletions(-) create mode 100644 lib/Db/PostgreSQL/Dispatch.php create mode 100644 lib/Db/PostgreSQL/Result.php create mode 100644 lib/Db/PostgreSQL/Statement.php create mode 100644 tests/cases/Db/PostgreSQL/TestCreation.php create mode 100644 tests/cases/Db/PostgreSQL/TestDatabase.php create mode 100644 tests/cases/Db/PostgreSQL/TestDriver.php create mode 100644 tests/cases/Db/PostgreSQL/TestResult.php create mode 100644 tests/cases/Db/PostgreSQL/TestStatement.php create mode 100644 tests/cases/Db/PostgreSQL/TestUpdate.php diff --git a/lib/Database.php b/lib/Database.php index 18da7775..ae175d7d 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -384,9 +384,9 @@ class Database { folders as (SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id) ". "SELECT - ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) as extant, - not exists(select id from folders where id = coalesce((select dest from target),0)) as valid, - not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) as available + case when ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) then 1 else 0 end as extant, + case when not exists(select id from folders where id = coalesce((select dest from target),0)) then 1 else 0 end as valid, + case when not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) then 1 else 0 end as available ", "str", "strict int", @@ -418,7 +418,7 @@ class Database { // make sure that a folder with the same prospective name and parent does not already exist: if the parent is null, // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves $parent = $parent ? $parent : null; - if ($this->db->prepare("SELECT exists(select id from arsse_folders where coalesce(parent,0) = ? and name = ?)", "strict int", "str")->run($parent, $name)->getValue()) { + if ($this->db->prepare("SELECT count(*) from arsse_folders where coalesce(parent,0) = ? and name = ?", "strict int", "str")->run($parent, $name)->getValue()) { throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]); } return true; diff --git a/lib/Db/PostgreSQL/Dispatch.php b/lib/Db/PostgreSQL/Dispatch.php new file mode 100644 index 00000000..c6cb198a --- /dev/null +++ b/lib/Db/PostgreSQL/Dispatch.php @@ -0,0 +1,42 @@ +db, $query, $params); + $result = pg_get_result($this->db); + if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) { + return $this->buildException($code, pg_result_error($result)); + } else { + return $result; + } + } + + protected function buildException(string $code, string $msg): array { + switch ($code) { + case "22P02": + case "42804": + return [ExceptionInput::class, 'engineTypeViolation', $msg]; + case "23000": + case "23502": + case "23505": + return [ExceptionInput::class, "engineConstraintViolation", $msg]; + case "55P03": + case "57014": + return [ExceptionTimeout::class, 'general', $msg]; + default: + return [Exception::class, "engineErrorGeneral", $code.": ".$msg]; // @codeCoverageIgnore + } + } +} diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index 37603248..e681f72c 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -13,6 +13,9 @@ use JKingWeb\Arsse\Db\ExceptionInput; use JKingWeb\Arsse\Db\ExceptionTimeout; class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { + use Dispatch; + + protected $db; protected $transStart = 0; public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) { @@ -156,46 +159,60 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } public function __destruct() { + if (isset($this->db)) { + pg_close($this->db); + unset($this->db); + } } - /** @codeCoverageIgnore */ public static function driverName(): string { return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name"); } - /** @codeCoverageIgnore */ public static function requirementsMet(): bool { - // stub: native interface is not yet supported - return false; + return \extension_loaded("pgsql"); } - /** @codeCoverageIgnore */ protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { - // stub: native interface is not yet supported - throw new \Exception; + $dsn = $this->makeconnectionString(false, $user, $pass, $db, $host, $port, $service); + set_error_handler(function(int $code, string $msg) { + $msg = substr($msg, 62); + throw new Exception("connectionFailure", ["PostgreSQL", $msg]); + }); + try { + $this->db = pg_connect($dsn, \PGSQL_CONNECT_FORCE_NEW); + } finally { + restore_error_handler(); + } } - /** @codeCoverageIgnore */ protected function getError(): string { - // stub: native interface is not yet supported + // stub return ""; } - /** @codeCoverageIgnore */ public function exec(string $query): bool { - // stub: native interface is not yet supported + pg_send_query($this->db, $query); + while ($result = pg_get_result($this->db)) { + if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) { + list($excClass, $excMsg, $excData) = $this->buildException($code, pg_result_error($result)); + throw new $excClass($excMsg, $excData); + } + } return true; } - /** @codeCoverageIgnore */ public function query(string $query): \JKingWeb\Arsse\Db\Result { - // stub: native interface is not yet supported - return new ResultEmpty; + $r = $this->dispatchQuery($query); + if (is_resource($r)) { + return new Result($this->db, $r); + } else { + list($excClass, $excMsg, $excData) = $r; + throw new $excClass($excMsg, $excData); + } } - /** @codeCoverageIgnore */ public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { - // stub: native interface is not yet supported - return new Statement($this->db, $s, $paramTypes); + return new Statement($this->db, $query, $paramTypes); } } diff --git a/lib/Db/PostgreSQL/PDOStatement.php b/lib/Db/PostgreSQL/PDOStatement.php index 16582609..534efbcb 100644 --- a/lib/Db/PostgreSQL/PDOStatement.php +++ b/lib/Db/PostgreSQL/PDOStatement.php @@ -6,18 +6,9 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Db\PostgreSQL; -class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement { +class PDOStatement extends Statement { use \JKingWeb\Arsse\Db\PDOError; - const BINDINGS = [ - "integer" => "bigint", - "float" => "decimal", - "datetime" => "timestamp(0) without time zone", - "binary" => "bytea", - "string" => "text", - "boolean" => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3 - ]; - protected $db; protected $st; protected $qOriginal; @@ -25,7 +16,7 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement { protected $bindings; public function __construct(\PDO $db, string $query, array $bindings = []) { - $this->db = $db; // both db and st are the same object due to the logic of the PDOError handler + $this->db = $db; $this->qOriginal = $query; $this->retypeArray($bindings); } @@ -53,19 +44,6 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement { return true; } - public static function mungeQuery(string $q, array $types, bool $mungeParamMarkers = true): string { - $q = explode("?", $q); - $out = ""; - for ($b = 1; $b < sizeof($q); $b++) { - $a = $b - 1; - $mark = $mungeParamMarkers ? "\$$b" : "?"; - $type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a]] : ""; - $out .= $q[$a].$mark.$type; - } - $out .= array_pop($q); - return $out; - } - public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { return $this->st->runArray($values); } @@ -73,6 +51,6 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement { /** @codeCoverageIgnore */ protected function bindValue($value, string $type, int $position): bool { // stub required by abstract parent, but never used - return $value; + return true; } } diff --git a/lib/Db/PostgreSQL/Result.php b/lib/Db/PostgreSQL/Result.php new file mode 100644 index 00000000..3b6cf9c6 --- /dev/null +++ b/lib/Db/PostgreSQL/Result.php @@ -0,0 +1,48 @@ +r); + } + + public function lastId(): int { + if ($r = @pg_query($this->db, "SELECT lastval()")) { + return (int) pg_fetch_result($r, 0, 0); + } else { + return 0; + } + } + + // constructor/destructor + + public function __construct($db, $result) { + $this->db = $db; + $this->r = $result; + } + + public function __destruct() { + pg_free_result($this->r); + unset($this->r, $this->db); + } + + // PHP iterator methods + + public function valid() { + $this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC); + return ($this->cur !== false); + } +} diff --git a/lib/Db/PostgreSQL/Statement.php b/lib/Db/PostgreSQL/Statement.php new file mode 100644 index 00000000..bccd0fb0 --- /dev/null +++ b/lib/Db/PostgreSQL/Statement.php @@ -0,0 +1,77 @@ + "bigint", + "float" => "decimal", + "datetime" => "timestamp(0) without time zone", + "binary" => "bytea", + "string" => "text", + "boolean" => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3 + ]; + + protected $db; + protected $in = []; + protected $qOriginal; + protected $qMunged; + protected $bindings; + + public function __construct($db, string $query, array $bindings = []) { + $this->db = $db; + $this->qOriginal = $query; + $this->retypeArray($bindings); + } + + public function retypeArray(array $bindings, bool $append = false): bool { + if ($append) { + return parent::retypeArray($bindings, $append); + } else { + $this->bindings = $bindings; + parent::retypeArray($bindings, $append); + $this->qMunged = self::mungeQuery($this->qOriginal, $this->types, true); + } + return true; + } + + public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { + $this->in = []; + $this->bindValues($values); + $r = $this->dispatchQuery($this->qMunged, $this->in); + if (is_resource($r)) { + return new Result($this->db, $r); + } else { + list($excClass, $excMsg, $excData) = $r; + throw new $excClass($excMsg, $excData); + } + } + + protected function bindValue($value, string $type, int $position): bool { + $this->in[] = $value; + return true; + } + + protected static function mungeQuery(string $q, array $types, bool $mungeParamMarkers = true): string { + $q = explode("?", $q); + $out = ""; + for ($b = 1; $b < sizeof($q); $b++) { + $a = $b - 1; + $mark = $mungeParamMarkers ? "\$$b" : "?"; + $type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a]] : ""; + $out .= $q[$a].$mark.$type; + } + $out .= array_pop($q); + return $out; + } +} diff --git a/locale/en.php b/locale/en.php index 3e63ac69..dc5381f1 100644 --- a/locale/en.php +++ b/locale/en.php @@ -159,7 +159,7 @@ return [ 'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => 'Referenced ID ({id}) in field "{field}" does not exist', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence', - 'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => 'Specified value in field "{0}" already exists', + 'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => 'Specified value in field "{field}" already exists', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineConstraintViolation' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}', diff --git a/tests/cases/Db/PostgreSQL/TestCreation.php b/tests/cases/Db/PostgreSQL/TestCreation.php new file mode 100644 index 00000000..182d3a34 --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestCreation.php @@ -0,0 +1,73 @@ + */ +class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { + public function setUp() { + if (!Driver::requirementsMet()) { + $this->markTestSkipped("PostgreSQL extension not loaded"); + } + } + + /** @dataProvider provideConnectionStrings */ + public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { + self::setConf(); + $timeout = (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0); + $postfix = "application_name='arsse' client_encoding='UTF8' connect_timeout='$timeout'"; + $act = Driver::makeConnectionString($pdo, $user, $pass, $db, $host, $port, $service); + if ($act==$postfix) { + $this->assertSame($exp, ""); + } else { + $test = substr($act, 0, strlen($act) - (strlen($postfix) + 1)); + $check = substr($act, strlen($test) + 1); + $this->assertSame($postfix, $check); + $this->assertSame($exp, $test); + } + } + + public function provideConnectionStrings() { + return [ + [false, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse' password='secret' user='arsse'"], + [false, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse' password='p word' user='arsse'"], + [false, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse' password='p\\'word' user='arsse'"], + [false, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db' password='secret' user='arsse user'"], + [false, "arsse", "secret", "", "", 5432, "", "password='secret' user='arsse'"], + [false, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost' password='secret' user='arsse'"], + [false, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' password='secret' port='9999' user='arsse'"], + [false, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' password='secret' port='9999' user='arsse'"], + [false, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket' password='secret' user='arsse'"], + [false, "T'Pau of Vulcan", "", "", "", 5432, "", "user='T\\'Pau of Vulcan'"], + [false, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"], + [true, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse'"], + [true, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse'"], + [true, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse'"], + [true, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db'"], + [true, "arsse", "secret", "", "", 5432, "", ""], + [true, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost'"], + [true, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' port='9999'"], + [true, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' port='9999'"], + [true, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket'"], + [true, "T'Pau of Vulcan", "", "", "", 5432, "", ""], + [true, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"], + ]; + } + + public function testFailToConnect() { + // we cannnot distinguish between different connection failure modes + self::setConf([ + 'dbPostgreSQLPass' => (string) rand(), + ]); + $this->assertException("connectionFailure", "Db"); + new Driver; + } +} diff --git a/tests/cases/Db/PostgreSQL/TestDatabase.php b/tests/cases/Db/PostgreSQL/TestDatabase.php new file mode 100644 index 00000000..efc19a60 --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestDatabase.php @@ -0,0 +1,43 @@ + + * @covers \JKingWeb\Arsse\Misc\Query + */ +class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { + protected static $implementation = "PostgreSQL"; + + protected function nextID(string $table): int { + return (int) static::$drv->query("SELECT coalesce(last_value, (select max(id) from $table)) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue(); + } + + public function setUp() { + parent::setUp(); + $seqList = + "select + replace(substring(column_default, 10), right(column_default, 12), '') as seq, + table_name as table, + column_name as col + from information_schema.columns + where table_schema = current_schema() + and table_name like 'arsse_%' + and column_default like 'nextval(%' + "; + foreach (static::$drv->query($seqList) as $r) { + $num = (int) static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue(); + if (!$num) { + continue; + } + $num++; + static::$drv->exec("ALTER SEQUENCE {$r['seq']} RESTART WITH $num"); + } + } +} diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php new file mode 100644 index 00000000..86decea5 --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -0,0 +1,57 @@ + */ +class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { + protected static $implementation = "PostgreSQL"; + protected $create = "CREATE TABLE arsse_test(id bigserial primary key)"; + protected $lock = ["BEGIN", "LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"]; + protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'"; + + public function tearDown() { + try { + $this->drv->exec("ROLLBACK"); + } catch (\Throwable $e) { + } + parent::tearDown(); + } + + public static function tearDownAfterClass() { + if (static::$interface) { + (static::$dbInfo->razeFunction)(static::$interface); + @pg_close(static::$interface); + static::$interface = null; + } + parent::tearDownAfterClass(); + } + + protected function exec($q): bool { + $q = (!is_array($q)) ? [$q] : $q; + foreach ($q as $query) { + set_error_handler(function($code, $msg) { + throw new \Exception($msg); + }); + try { + pg_query(static::$interface, $query); + } finally { + restore_error_handler(); + } + } + return true; + } + + protected function query(string $q) { + if ($r = pg_query_params(static::$interface, $q, [])) { + return pg_fetch_result($r, 0, 0); + } else { + return; + } + } +} diff --git a/tests/cases/Db/PostgreSQL/TestResult.php b/tests/cases/Db/PostgreSQL/TestResult.php new file mode 100644 index 00000000..08fff06b --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestResult.php @@ -0,0 +1,33 @@ + + */ +class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { + protected static $implementation = "PostgreSQL"; + protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)"; + protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)"; + + protected function makeResult(string $q): array { + $set = pg_query(static::$interface, $q); + return [static::$interface, $set]; + } + + public static function tearDownAfterClass() { + if (static::$interface) { + (static::$dbInfo->razeFunction)(static::$interface); + @pg_close(static::$interface); + static::$interface = null; + } + parent::tearDownAfterClass(); + } +} diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php new file mode 100644 index 00000000..de350dbc --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -0,0 +1,41 @@ + */ +class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { + protected static $implementation = "PostgreSQL"; + + protected function makeStatement(string $q, array $types = []): array { + return [static::$interface, $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 "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'"; + } + return $value; + default: + return $value; + } + } + + public static function tearDownAfterClass() { + if (static::$interface) { + (static::$dbInfo->razeFunction)(static::$interface); + @pg_close(static::$interface); + static::$interface = null; + } + parent::tearDownAfterClass(); + } +} diff --git a/tests/cases/Db/PostgreSQL/TestUpdate.php b/tests/cases/Db/PostgreSQL/TestUpdate.php new file mode 100644 index 00000000..cbdcb0bd --- /dev/null +++ b/tests/cases/Db/PostgreSQL/TestUpdate.php @@ -0,0 +1,16 @@ + */ +class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { + protected static $implementation = "PostgreSQL"; + protected static $minimal1 = "CREATE TABLE arsse_meta(key text 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/cases/Db/PostgreSQLPDO/TestCreation.php b/tests/cases/Db/PostgreSQLPDO/TestCreation.php index 3561ff46..83d95e89 100644 --- a/tests/cases/Db/PostgreSQLPDO/TestCreation.php +++ b/tests/cases/Db/PostgreSQLPDO/TestCreation.php @@ -13,6 +13,12 @@ use JKingWeb\Arsse\Db\PostgreSQL\PDODriver as Driver; * @group slow * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver */ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { + public function setUp() { + if (!Driver::requirementsMet()) { + $this->markTestSkipped("PDO-PostgreSQL extension not loaded"); + } + } + /** @dataProvider provideConnectionStrings */ public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { self::setConf(); diff --git a/tests/cases/Db/PostgreSQLPDO/TestDatabase.php b/tests/cases/Db/PostgreSQLPDO/TestDatabase.php index 42d5f9f7..6ce5de70 100644 --- a/tests/cases/Db/PostgreSQLPDO/TestDatabase.php +++ b/tests/cases/Db/PostgreSQLPDO/TestDatabase.php @@ -8,6 +8,7 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO; /** * @group slow + * @group optional * @group coverageOptional * @covers \JKingWeb\Arsse\Database * @covers \JKingWeb\Arsse\Misc\Query diff --git a/tests/cases/Db/SQLite3/TestDatabase.php b/tests/cases/Db/SQLite3/TestDatabase.php index 35448d49..c65027c3 100644 --- a/tests/cases/Db/SQLite3/TestDatabase.php +++ b/tests/cases/Db/SQLite3/TestDatabase.php @@ -7,6 +7,7 @@ declare(strict_types=1); namespace JKingWeb\Arsse\TestCase\Db\SQLite3; /** + * @group optional * @covers \JKingWeb\Arsse\Database * @covers \JKingWeb\Arsse\Misc\Query */ diff --git a/tests/lib/DatabaseInformation.php b/tests/lib/DatabaseInformation.php index a550847b..ea70afc0 100644 --- a/tests/lib/DatabaseInformation.php +++ b/tests/lib/DatabaseInformation.php @@ -116,7 +116,50 @@ class DatabaseInformation { } elseif ($db instanceof \PDO) { return $db->query($listObjects)->fetchAll(\PDO::FETCH_ASSOC); } else { - throw \Exception("Native PostgreSQL interface not implemented"); + $r = @pg_query($db, $listObjects); + $out = $r ? pg_fetch_all($r) : false; + return $out ? $out : []; + } + }; + $pgExecFunction = function($db, $q) { + if ($db instanceof Driver) { + $db->exec($q); + } elseif ($db instanceof \PDO) { + $db->exec($q); + } else { + pg_query($db, $q); + } + }; + $pgTruncateFunction = function($db, array $afterStatements = []) use ($pgObjectList, $pgExecFunction) { + // rollback any pending transaction + try { + @$pgExecFunction($db, "ROLLBACK"); + } catch (\Throwable $e) { + } + foreach ($pgObjectList($db) as $obj) { + if ($obj['type'] != "TABLE") { + continue; + } elseif ($obj['name'] == "arsse_meta") { + $pgExecFunction($db, "DELETE FROM {$obj['name']} where key <> 'schema_version'"); + } else { + $pgExecFunction($db, "TRUNCATE TABLE {$obj['name']} restart identity cascade"); + } + } + foreach ($afterStatements as $st) { + $pgExecFunction($db, $st); + } + }; + $pgRazeFunction = function($db, array $afterStatements = []) use ($pgObjectList, $pgExecFunction) { + // rollback any pending transaction + try { + $pgExecFunction($db, "ROLLBACK"); + } catch (\Throwable $e) { + } + foreach ($pgObjectList($db) as $obj) { + $pgExecFunction($db, "DROP {$obj['type']} IF EXISTS {$obj['name']} cascade"); + } + foreach ($afterStatements as $st) { + $pgExecFunction($db, $st); } }; return [ @@ -158,6 +201,27 @@ class DatabaseInformation { 'truncateFunction' => $sqlite3TruncateFunction, 'razeFunction' => $sqlite3RazeFunction, ], + 'PostgreSQL' => [ + 'pdo' => false, + 'backend' => "PostgreSQL", + 'statementClass' => \JKingWeb\Arsse\Db\PostgreSQL\Statement::class, + 'resultClass' => \JKingWeb\Arsse\Db\PostgreSQL\Result::class, + 'driverClass' => \JKingWeb\Arsse\Db\PostgreSQL\Driver::class, + 'stringOutput' => true, + 'interfaceConstructor' => function() { + $connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(false, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, ""); + if ($d = @pg_connect($connString, \PGSQL_CONNECT_FORCE_NEW)) { + foreach (\JKingWeb\Arsse\Db\PostgreSQL\Driver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) { + pg_query($d, $q); + } + return $d; + } else { + return; + } + }, + 'truncateFunction' => $pgTruncateFunction, + 'razeFunction' => $pgRazeFunction, + ], 'PDO PostgreSQL' => [ 'pdo' => true, 'backend' => "PostgreSQL", @@ -177,38 +241,8 @@ class DatabaseInformation { } return $d; }, - 'truncateFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { - // rollback any pending transaction - try { - $db->exec("ROLLBACK"); - } catch (\Throwable $e) { - } - foreach ($pgObjectList($db) as $obj) { - if ($obj['type'] != "TABLE") { - continue; - } elseif ($obj['name'] == "arsse_meta") { - $db->exec("DELETE FROM {$obj['name']} where key <> 'schema_version'"); - } else { - $db->exec("TRUNCATE TABLE {$obj['name']} restart identity cascade"); - } - } - foreach ($afterStatements as $st) { - $db->exec($st); - } - }, - 'razeFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { - // rollback any pending transaction - try { - $db->exec("ROLLBACK"); - } catch (\Throwable $e) { - } - foreach ($pgObjectList($db) as $obj) { - $db->exec("DROP {$obj['type']} IF EXISTS {$obj['name']} cascade"); - } - foreach ($afterStatements as $st) { - $db->exec($st); - } - }, + 'truncateFunction' => $pgTruncateFunction, + 'razeFunction' => $pgRazeFunction, ], ]; } From 29e7c1f154fd28f5ae5ccb6c8935b2fdcb6fac4f Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 13 Dec 2018 19:56:07 -0500 Subject: [PATCH 56/58] Fix coverage --- tests/cases/Db/PostgreSQL/TestDriver.php | 3 ++- tests/cases/Db/PostgreSQL/TestStatement.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/cases/Db/PostgreSQL/TestDriver.php b/tests/cases/Db/PostgreSQL/TestDriver.php index 86decea5..0c1f1525 100644 --- a/tests/cases/Db/PostgreSQL/TestDriver.php +++ b/tests/cases/Db/PostgreSQL/TestDriver.php @@ -8,7 +8,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; /** * @group slow - * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver */ + * @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver + * @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch */ class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { protected static $implementation = "PostgreSQL"; protected $create = "CREATE TABLE arsse_test(id bigserial primary key)"; diff --git a/tests/cases/Db/PostgreSQL/TestStatement.php b/tests/cases/Db/PostgreSQL/TestStatement.php index de350dbc..e0e9b02d 100644 --- a/tests/cases/Db/PostgreSQL/TestStatement.php +++ b/tests/cases/Db/PostgreSQL/TestStatement.php @@ -8,7 +8,8 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL; /** * @group slow - * @covers \JKingWeb\Arsse\Db\PostgreSQL\Statement */ + * @covers \JKingWeb\Arsse\Db\PostgreSQL\Statement + * @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch */ class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { protected static $implementation = "PostgreSQL"; From 17052d3232fc3f042d8c7bc12214760d47798288 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 13 Dec 2018 20:08:35 -0500 Subject: [PATCH 57/58] Update PostgreSQL-related documentation --- CHANGELOG | 5 ++++- README.md | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4221df6c..5bde4c42 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,7 +2,10 @@ Version 0.6.0 (????-??-??) ========================== New features: -- Support for PostgreSQL databases (via PDO) +- Support for PostgreSQL databases + +Bug fixes: +- Use a general-purpose Unicode collation with SQLite databases Changes: - Improve performance of common database queries by 80-90% diff --git a/README.md b/README.md index 9e55bd05..afd48f08 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The Arsse has the following requirements: - [dom](http://php.net/manual/en/book.dom.php), [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php) (for picoFeed) - Either of: - [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://ca1.php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases - - [pdo_pgsql](http://ca1.php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 9.1 or later databases + - [pgsql](http://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](http://ca1.php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 9.1 or later databases - Privileges to create and run daemon processes on the server ## Installation @@ -73,7 +73,7 @@ Please refer to `CONTRIBUTING.md` for guidelines on contributing code to The Ars ## Database compatibility notes -Functionally there is no reason to prefer either SQLite or PostgreSQL over the other. SQLite, however, is significantly simpler to set up in most cases, requiring only read and write access to a containing directory in order to function. PostgreSQL may perform better than SQLite when serving hundreds of users or more, but this has not been tested. +Functionally there is no reason to prefer either SQLite or PostgreSQL over the other. SQLite, however, is significantly simpler to set up in most cases, requiring only read and write access to a containing directory in order to function. On the other hand PostgreSQL may perform better than SQLite when serving hundreds of users or more, but this has not been tested. ## Protocol compatibility notes From 50f92625efd81fa148b399789416c8f951a3ce85 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Fri, 14 Dec 2018 09:18:56 -0500 Subject: [PATCH 58/58] Use PosgreSQL's existing general Unicode collation All collations appear to be case-insensitive --- lib/Database.php | 3 ++- lib/Db/PostgreSQL/Driver.php | 7 ++++++- sql/PostgreSQL/2.sql | 22 ++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/Database.php b/lib/Database.php index ae175d7d..2cb55147 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -475,7 +475,8 @@ class Database { join arsse_feeds on feed = arsse_feeds.id left join topmost on folder=f_id" ); - $q->setOrder("pinned desc, coalesce(arsse_subscriptions.title, arsse_feeds.title) collate nocase"); + $nocase = $this->db->sqlToken("nocase"); + $q->setOrder("pinned desc, coalesce(arsse_subscriptions.title, arsse_feeds.title) collate $nocase"); // define common table expressions $q->setCTE("userdata(userid)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once // topmost folders belonging to the user diff --git a/lib/Db/PostgreSQL/Driver.php b/lib/Db/PostgreSQL/Driver.php index e681f72c..89e7a7cb 100644 --- a/lib/Db/PostgreSQL/Driver.php +++ b/lib/Db/PostgreSQL/Driver.php @@ -113,7 +113,12 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { } public function sqlToken(string $token): string { - return $token; + switch (strtolower($token)) { + case "nocase": + return '"und-x-icu"'; + default: + return $token; + } } public function savepointCreate(bool $lock = false): int { diff --git a/sql/PostgreSQL/2.sql b/sql/PostgreSQL/2.sql index d37fcb50..847edb70 100644 --- a/sql/PostgreSQL/2.sql +++ b/sql/PostgreSQL/2.sql @@ -4,19 +4,13 @@ -- Please consult the SQLite 3 schemata for commented version --- create a case-insensitive generic Unicode collation sequence -create collation nocase( - provider = icu, - locale = '@kf=false' -); - -alter table arsse_users alter column id type text collate nocase; -alter table arsse_folders alter column name type text collate nocase; -alter table arsse_feeds alter column title type text collate nocase; -alter table arsse_subscriptions alter column title type text collate nocase; -alter table arsse_articles alter column title type text collate nocase; -alter table arsse_articles alter column author type text collate nocase; -alter table arsse_categories alter column name type text collate nocase; -alter table arsse_labels alter column name type text collate nocase; +alter table arsse_users alter column id type text collate "und-x-icu"; +alter table arsse_folders alter column name type text collate "und-x-icu"; +alter table arsse_feeds alter column title type text collate "und-x-icu"; +alter table arsse_subscriptions alter column title type text collate "und-x-icu"; +alter table arsse_articles alter column title type text collate "und-x-icu"; +alter table arsse_articles alter column author type text collate "und-x-icu"; +alter table arsse_categories alter column name type text collate "und-x-icu"; +alter table arsse_labels alter column name type text collate "und-x-icu"; update arsse_meta set value = '3' where key = 'schema_version';