mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-23 09:02:41 +00:00
8fc31cfc40
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.
398 lines
16 KiB
PHP
398 lines
16 KiB
PHP
<?php
|
|
/** @license MIT
|
|
* Copyright 2017 J. King, Dustin Wilson et al.
|
|
* See LICENSE and AUTHORS files for details */
|
|
|
|
declare(strict_types=1);
|
|
namespace JKingWeb\Arsse\TestCase\Db;
|
|
|
|
use JKingWeb\Arsse\Db\Statement;
|
|
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 $create;
|
|
protected $lock;
|
|
protected $setVersion;
|
|
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(static::$conf);
|
|
if (!static::$interface) {
|
|
$this->markTestSkipped(static::$implementation." database driver not available");
|
|
}
|
|
// 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() {
|
|
// deconstruct the driver
|
|
unset($this->drv);
|
|
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();
|
|
}
|
|
|
|
protected function exec($q): bool {
|
|
// PDO implementation
|
|
$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 static::$interface->query($q)->fetchColumn();
|
|
}
|
|
|
|
# TESTS
|
|
|
|
public function testFetchDriverName() {
|
|
$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());
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
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");
|
|
$lock = is_array($this->lock) ? implode("; ",$this->lock) : $this->lock;
|
|
$this->drv->exec($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");
|
|
$lock = is_array($this->lock) ? implode("; ",$this->lock) : $this->lock;
|
|
$this->drv->exec($lock);
|
|
}
|
|
|
|
public function testQueryConstraintViolation() {
|
|
$this->drv->exec("CREATE TABLE arsse_test(id integer not null)");
|
|
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
|
$this->drv->query("INSERT INTO arsse_test default values");
|
|
}
|
|
|
|
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());
|
|
// 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->lock);
|
|
}
|
|
|
|
public function testUnlockTheDatabase() {
|
|
$this->drv->savepointCreate(true);
|
|
$this->drv->savepointRelease();
|
|
$this->drv->savepointCreate(true);
|
|
$this->drv->savepointUndo();
|
|
$this->assertTrue($this->exec(str_replace("#", "3", $this->setVersion)));
|
|
}
|
|
}
|