From ad6a09ffa127843c8de34d3a01cf7b7baa4da7e8 Mon Sep 17 00:00:00 2001 From: "J. King" Date: Tue, 19 Dec 2017 17:15:05 -0500 Subject: [PATCH] Partially tested pdo_sqlite driver; improves #72 --- lib/Db/PDODriver.php | 47 +++ lib/Db/PDOError.php | 42 +++ lib/Db/PDOResult.php | 49 +++ lib/Db/PDOStatement.php | 85 +++++ lib/Db/SQLite3/PDODriver.php | 46 +++ locale/en.php | 1 + .../TestDbDriverCreationSQLite3PDO.php | 195 ++++++++++ .../Db/SQLite3PDO/TestDbDriverSQLite3PDO.php | 337 ++++++++++++++++++ .../Db/SQLite3PDO/TestDbResultSQLite3PDO.php | 104 ++++++ .../SQLite3PDO/TestDbStatementSQLite3PDO.php | 105 ++++++ .../Db/SQLite3PDO/TestDbUpdateSQLite3PDO.php | 120 +++++++ tests/phpunit.xml | 8 +- 12 files changed, 1138 insertions(+), 1 deletion(-) create mode 100644 lib/Db/PDODriver.php create mode 100644 lib/Db/PDOError.php create mode 100644 lib/Db/PDOResult.php create mode 100644 lib/Db/PDOStatement.php create mode 100644 lib/Db/SQLite3/PDODriver.php create mode 100644 tests/cases/Db/SQLite3PDO/TestDbDriverCreationSQLite3PDO.php create mode 100644 tests/cases/Db/SQLite3PDO/TestDbDriverSQLite3PDO.php create mode 100644 tests/cases/Db/SQLite3PDO/TestDbResultSQLite3PDO.php create mode 100644 tests/cases/Db/SQLite3PDO/TestDbStatementSQLite3PDO.php create mode 100644 tests/cases/Db/SQLite3PDO/TestDbUpdateSQLite3PDO.php diff --git a/lib/Db/PDODriver.php b/lib/Db/PDODriver.php new file mode 100644 index 0000000..99dfe67 --- /dev/null +++ b/lib/Db/PDODriver.php @@ -0,0 +1,47 @@ +db->exec($query); + return true; + } catch (\PDOException $e) { + list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + throw new $excClass($excMsg, $excData); + } + } + + public function query(string $query): Result { + try { + $r = $this->db->query($query); + } catch (\PDOException $e) { + list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + throw new $excClass($excMsg, $excData); + } + $changes = $r->rowCount(); + try { + $lastId = 0; + $lastId = $this->db->lastInsertId(); + } catch (\PDOException $e) { // @codeCoverageIgnore + } + return new PDOResult($r, [$changes, $lastId]); + } + + public function prepareArray(string $query, array $paramTypes): Statement { + try { + $s = $this->db->prepare($query); + } catch (\PDOException $e) { + list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + throw new $excClass($excMsg, $excData); + } + return new PDOStatement($this->db, $s, $paramTypes); + } +} \ No newline at end of file diff --git a/lib/Db/PDOError.php b/lib/Db/PDOError.php new file mode 100644 index 0000000..929fe1e --- /dev/null +++ b/lib/Db/PDOError.php @@ -0,0 +1,42 @@ +st->errorInfo(); + } else { + $err = $this->db->errorInfo(); + } + switch ($err[0]) { + case "23000": + return [ExceptionInput::class, "constraintViolation", $err[2]]; + case "HY000": + // engine-specific errors + switch ($this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + case "sqlite": + switch ($err[1]) { + case \JKingWeb\Arsse\Db\SQLite3\Driver::SQLITE_BUSY: + return [ExceptionTimeout::class, 'general', $err[2]]; + case \JKingWeb\Arsse\Db\SQLite3\Driver::SQLITE_MISMATCH: + return [ExceptionInput::class, 'engineTypeViolation', $err[2]]; + default: + return [Exception::class, "engineErrorGeneral", $err[1]." - ".$err[2]]; + } + default: + return [Exception::class, "engineErrorGeneral", $err[2]]; // @codeCoverageIgnore + } + default: + return [Exception::class, "engineErrorGeneral", $err[0].": ".$err[2]]; // @codeCoverageIgnore + } + } + + public function getError(): string { + return (string) $this->db->errorInfo()[2]; + } +} \ No newline at end of file diff --git a/lib/Db/PDOResult.php b/lib/Db/PDOResult.php new file mode 100644 index 0000000..32400e9 --- /dev/null +++ b/lib/Db/PDOResult.php @@ -0,0 +1,49 @@ +rows; + } + + public function lastId() { + return $this->id; + } + + // constructor/destructor + + public function __construct(\PDOStatement $result, array $changes = [0,0]) { + $this->set = $result; + $this->rows = (int) $changes[0]; + $this->id = (int) $changes[1]; + } + + public function __destruct() { + try { + $this->set->closeCursor(); + } catch (\PDOException $e) { // @codeCoverageIgnore + } + unset($this->set); + } + + // PHP iterator methods + + public function valid() { + $this->cur = $this->set->fetch(\PDO::FETCH_ASSOC); + return ($this->cur !== false); + } +} diff --git a/lib/Db/PDOStatement.php b/lib/Db/PDOStatement.php new file mode 100644 index 0000000..a8d459e --- /dev/null +++ b/lib/Db/PDOStatement.php @@ -0,0 +1,85 @@ + \PDO::PARAM_NULL, + "integer" => \PDO::PARAM_INT, + "float" => \PDO::PARAM_STR, + "date" => \PDO::PARAM_STR, + "time" => \PDO::PARAM_STR, + "datetime" => \PDO::PARAM_STR, + "binary" => \PDO::PARAM_LOB, + "string" => \PDO::PARAM_STR, + "boolean" => \PDO::PARAM_BOOL, + ]; + + protected $st; + protected $db; + + public function __construct(\PDO $db, \PDOStatement $st, array $bindings = []) { + $this->db = $db; + $this->st = $st; + $this->rebindArray($bindings); + } + + public function __destruct() { + unset($this->st); + } + + public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { + $this->st->closeCursor(); + $this->bindValues($values); + try { + $this->st->execute(); + } catch (\PDOException $e) { + list($excClass, $excMsg, $excData) = $this->exceptionBuild(); + throw new $excClass($excMsg, $excData); + } + $changes = $this->st->rowCount(); + try { + $lastId = 0; + $lastId = $this->db->lastInsertId(); + } catch (\PDOException $e) { // @codeCoverageIgnore + } + return new PDOResult($this->st, [$changes, $lastId]); + } + + protected function bindValues(array $values, int $offset = 0): int { + $a = $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)) { + // if the parameter type is something other than the known values, this is an error + assert(array_key_exists($this->types[$a], self::BINDINGS), new Exception("paramTypeUnknown", $this->types[$a])); + // if the parameter type is null or the value is null (and the type is nullable), just bind null + if ($this->types[$a]=="null" || ($this->isNullable[$a] && is_null($value))) { + $this->st->bindValue($a+1, null, \PDO::PARAM_NULL); + } else { + // otherwise cast the value to the right type and bind the result + $type = self::BINDINGS[$this->types[$a]]; + $value = $this->cast($value, $this->types[$a], $this->isNullable[$a]); + // re-adjust for null casts + if ($value===null) { + $type = \PDO::PARAM_NULL; + } + // perform binding + $this->st->bindValue($a+1, $value, $type); + } + $a++; + } else { + throw new Exception("paramTypeMissing", $a+1); + } + } + return $a - $offset; + } +} diff --git a/lib/Db/SQLite3/PDODriver.php b/lib/Db/SQLite3/PDODriver.php new file mode 100644 index 0000000..a78dc24 --- /dev/null +++ b/lib/Db/SQLite3/PDODriver.php @@ -0,0 +1,46 @@ +db = new \PDO("sqlite:".$file, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + } + + 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.SQLite3PDO.Name"); + } +} diff --git a/locale/en.php b/locale/en.php index e284847..a6d1698 100644 --- a/locale/en.php +++ b/locale/en.php @@ -16,6 +16,7 @@ return [ 'API.TTRSS.FeedCount' => '{0, select, 1 {(1 feed)} other {({0} feeds)}}', 'Driver.Db.SQLite3.Name' => 'SQLite 3', + 'Driver.Db.SQLite3PDO.Name' => 'SQLite 3 (PDO)', 'Driver.Service.Curl.Name' => 'HTTP (curl)', 'Driver.Service.Internal.Name' => 'Internal', 'Driver.User.Internal.Name' => 'Internal', diff --git a/tests/cases/Db/SQLite3PDO/TestDbDriverCreationSQLite3PDO.php b/tests/cases/Db/SQLite3PDO/TestDbDriverCreationSQLite3PDO.php new file mode 100644 index 0000000..436d4e2 --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestDbDriverCreationSQLite3PDO.php @@ -0,0 +1,195 @@ + + * @covers \JKingWeb\Arsse\Db\PDODriver + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestDbDriverCreationSQLite3PDO extends Test\AbstractTest { + protected $data; + protected $drv; + protected $ch; + + public function setUp() { + if (!Driver::requirementsMet()) { + $this->markTestSkipped("PDO-SQLite extension not loaded"); + } + $this->clearData(); + // test files + $this->files = [ + // cannot create files + 'Cmain' => [], + 'Cshm' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + ], + 'Cwal' => [ + 'arsse.db' => "", + ], + // cannot write to files + 'Wmain' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + 'Wwal' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + 'Wshm' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + // cannot read from files + 'Rmain' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + 'Rwal' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + 'Rshm' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + // can neither read from or write to files + 'Amain' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + 'Awal' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + 'Ashm' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + // non-filesystem errors + 'corrupt' => [ + 'arsse.db' => "", + 'arsse.db-wal' => "", + 'arsse.db-shm' => "", + ], + ]; + $vfs = vfsStream::setup("dbtest", 0777, $this->files); + $this->path = $path = $vfs->url()."/"; + // set up access blocks + chmod($path."Cmain", 0555); + chmod($path."Cwal", 0555); + chmod($path."Cshm", 0555); + chmod($path."Rmain/arsse.db", 0333); + chmod($path."Rwal/arsse.db-wal", 0333); + chmod($path."Rshm/arsse.db-shm", 0333); + chmod($path."Wmain/arsse.db", 0555); + chmod($path."Wwal/arsse.db-wal", 0555); + chmod($path."Wshm/arsse.db-shm", 0555); + chmod($path."Amain/arsse.db", 0111); + chmod($path."Awal/arsse.db-wal", 0111); + chmod($path."Ashm/arsse.db-shm", 0111); + // set up configuration + Arsse::$conf = new Conf(); + Arsse::$conf->dbSQLite3File = ":memory:"; + } + + public function tearDown() { + $this->clearData(); + } + + public function testFailToCreateDatabase() { + Arsse::$conf->dbSQLite3File = $this->path."Cmain/arsse.db"; + $this->assertException("fileUncreatable", "Db"); + new Driver; + } + + public function testFailToCreateJournal() { + Arsse::$conf->dbSQLite3File = $this->path."Cwal/arsse.db"; + $this->assertException("fileUncreatable", "Db"); + new Driver; + } + + public function testFailToCreateSharedMmeory() { + Arsse::$conf->dbSQLite3File = $this->path."Cshm/arsse.db"; + $this->assertException("fileUncreatable", "Db"); + new Driver; + } + + public function testFailToReadDatabase() { + Arsse::$conf->dbSQLite3File = $this->path."Rmain/arsse.db"; + $this->assertException("fileUnreadable", "Db"); + new Driver; + } + + public function testFailToReadJournal() { + Arsse::$conf->dbSQLite3File = $this->path."Rwal/arsse.db"; + $this->assertException("fileUnreadable", "Db"); + new Driver; + } + + public function testFailToReadSharedMmeory() { + Arsse::$conf->dbSQLite3File = $this->path."Rshm/arsse.db"; + $this->assertException("fileUnreadable", "Db"); + new Driver; + } + + public function testFailToWriteToDatabase() { + Arsse::$conf->dbSQLite3File = $this->path."Wmain/arsse.db"; + $this->assertException("fileUnwritable", "Db"); + new Driver; + } + + public function testFailToWriteToJournal() { + Arsse::$conf->dbSQLite3File = $this->path."Wwal/arsse.db"; + $this->assertException("fileUnwritable", "Db"); + new Driver; + } + + public function testFailToWriteToSharedMmeory() { + Arsse::$conf->dbSQLite3File = $this->path."Wshm/arsse.db"; + $this->assertException("fileUnwritable", "Db"); + new Driver; + } + + public function testFailToAccessDatabase() { + Arsse::$conf->dbSQLite3File = $this->path."Amain/arsse.db"; + $this->assertException("fileUnusable", "Db"); + new Driver; + } + + public function testFailToAccessJournal() { + Arsse::$conf->dbSQLite3File = $this->path."Awal/arsse.db"; + $this->assertException("fileUnusable", "Db"); + new Driver; + } + + public function testFailToAccessSharedMmeory() { + Arsse::$conf->dbSQLite3File = $this->path."Ashm/arsse.db"; + $this->assertException("fileUnusable", "Db"); + new Driver; + } + + public function testAssumeDatabaseCorruption() { + Arsse::$conf->dbSQLite3File = $this->path."corrupt/arsse.db"; + $this->assertException("fileCorrupt", "Db"); + new Driver; + } +} diff --git a/tests/cases/Db/SQLite3PDO/TestDbDriverSQLite3PDO.php b/tests/cases/Db/SQLite3PDO/TestDbDriverSQLite3PDO.php new file mode 100644 index 0000000..bb96112 --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestDbDriverSQLite3PDO.php @@ -0,0 +1,337 @@ + + * @covers \JKingWeb\Arsse\Db\PDODriver + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestDbDriverSQLite3PDO extends Test\AbstractTest { + protected $data; + protected $drv; + protected $ch; + + public function setUp() { + if (!Db\SQLite3\PDODriver::requirementsMet()) { + $this->markTestSkipped("PDO-SQLite extension not loaded"); + } + $this->clearData(); + $conf = new Conf(); + Arsse::$conf = $conf; + $conf->dbDriver = Db\SQLite3\PDODriver::class; + $conf->dbSQLite3Timeout = 0; + $conf->dbSQLite3File = tempnam(sys_get_temp_dir(), 'ook'); + $this->drv = new Db\SQLite3\PDODriver(); + $this->ch = new \PDO("sqlite:".Arsse::$conf->dbSQLite3File, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + } + + 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(Db\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(Db\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)")); + } +} diff --git a/tests/cases/Db/SQLite3PDO/TestDbResultSQLite3PDO.php b/tests/cases/Db/SQLite3PDO/TestDbResultSQLite3PDO.php new file mode 100644 index 0000000..fbc745f --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestDbResultSQLite3PDO.php @@ -0,0 +1,104 @@ + */ +class TestDbResultSQLite3PDO extends Test\AbstractTest { + protected $c; + + public function setUp() { + $this->clearData(); + if (!Db\SQLite3\PDODriver::requirementsMet()) { + $this->markTestSkipped("PDO-SQLite extension not loaded"); + } + $c = new \PDO("sqlite::memory:", "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $this->c = $c; + } + + public function tearDown() { + unset($this->c); + $this->clearData(); + } + + public function testConstructResult() { + $set = $this->c->query("SELECT 1"); + $this->assertInstanceOf(Db\Result::class, new Db\PDOResult($set)); + } + + public function testGetChangeCountAndLastInsertId() { + $this->c->query("CREATE TABLE test(col)"); + $set = $this->c->query("INSERT INTO test(col) values(1)"); + $rows = $set->rowCount(); + $id = $this->c->lastInsertID(); + $r = new Db\PDOResult($set, [$rows,$id]); + $this->assertSame((int) $rows, $r->changes()); + $this->assertSame((int) $id, $r->lastId()); + } + + public function testIterateOverResults() { + $set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col"); + $rows = []; + foreach (new Db\PDOResult($set) as $index => $row) { + $rows[$index] = $row['col']; + } + $this->assertSame([0 => "1", 1 => "2", 2 => "3"], $rows); + } + + public function testIterateOverResultsTwice() { + $set = $this->c->query("SELECT 1 as col union select 2 as col union select 3 as col"); + $rows = []; + $test = new Db\PDOResult($set); + foreach ($test as $row) { + $rows[] = $row['col']; + } + $this->assertSame(["1","2","3"], $rows); + $this->assertException("resultReused", "Db"); + foreach ($test as $row) { + $rows[] = $row['col']; + } + } + + public function testGetSingleValues() { + $set = $this->c->query("SELECT 1867 as year union select 1970 as year union select 2112 as year"); + $test = new Db\PDOResult($set); + $this->assertEquals(1867, $test->getValue()); + $this->assertEquals(1970, $test->getValue()); + $this->assertEquals(2112, $test->getValue()); + $this->assertSame(null, $test->getValue()); + } + + public function testGetFirstValuesOnly() { + $set = $this->c->query("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 Db\PDOResult($set); + $this->assertEquals(1867, $test->getValue()); + $this->assertEquals(1970, $test->getValue()); + $this->assertEquals(2112, $test->getValue()); + $this->assertSame(null, $test->getValue()); + } + + public function testGetRows() { + $set = $this->c->query("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track"); + $rows = [ + ['album' => '2112', 'track' => '2112'], + ['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], + ]; + $test = new Db\PDOResult($set); + $this->assertEquals($rows[0], $test->getRow()); + $this->assertEquals($rows[1], $test->getRow()); + $this->assertSame(null, $test->getRow()); + } + + public function testGetAllRows() { + $set = $this->c->query("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track"); + $rows = [ + ['album' => '2112', 'track' => '2112'], + ['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], + ]; + $test = new Db\PDOResult($set); + $this->assertEquals($rows, $test->getAll()); + } +} diff --git a/tests/cases/Db/SQLite3PDO/TestDbStatementSQLite3PDO.php b/tests/cases/Db/SQLite3PDO/TestDbStatementSQLite3PDO.php new file mode 100644 index 0000000..76ca24a --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestDbStatementSQLite3PDO.php @@ -0,0 +1,105 @@ + + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestDbStatementSQLite3PDO extends Test\AbstractTest { + + protected $c; + protected static $imp = Db\PDOStatement::class; + + public function setUp() { + $this->clearData(); + if (!Db\SQLite3\PDODriver::requirementsMet()) { + $this->markTestSkipped("PDO-SQLite extension not loaded"); + } + $c = new \PDO("sqlite::memory:", "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]); + $this->c = $c; + } + + public function tearDown() { + unset($this->c); + $this->clearData(); + } + + protected function checkBinding($input, array $expectations, bool $strict = false) { + $nativeStatement = $this->c->prepare("SELECT ? as value"); + $s = new self::$imp($this->c, $nativeStatement); + $types = array_unique(Statement::TYPES); + foreach ($types as $type) { + $s->rebindArray([$strict ? "strict $type" : $type]); + $val = $s->runArray([$input])->getRow()['value']; + $this->assertSame($expectations[$type], $val, "Binding from type $type failed comparison."); + $s->rebind(...[$strict ? "strict $type" : $type]); + $val = $s->run(...[$input])->getRow()['value']; + $this->assertSame($expectations[$type], $val, "Binding from type $type failed comparison."); + } + } + + public function testConstructStatement() { + $nativeStatement = $this->c->prepare("SELECT ? as value"); + $this->assertInstanceOf(Statement::class, new Db\PDOStatement($this->c, $nativeStatement)); + } + + public function testBindMissingValue() { + $nativeStatement = $this->c->prepare("SELECT ? as value"); + $s = new self::$imp($this->c, $nativeStatement); + $val = $s->runArray()->getRow()['value']; + $this->assertSame(null, $val); + } + + public function testBindMultipleValues() { + $exp = [ + 'one' => "1", + 'two' => "2", + ]; + $nativeStatement = $this->c->prepare("SELECT ? as one, ? as two"); + $s = new self::$imp($this->c, $nativeStatement, ["int", "int"]); + $val = $s->runArray([1,2])->getRow(); + $this->assertSame($exp, $val); + } + + public function testBindRecursively() { + $exp = [ + 'one' => "1", + 'two' => "2", + 'three' => "3", + 'four' => "4", + ]; + $nativeStatement = $this->c->prepare("SELECT ? as one, ? as two, ? as three, ? as four"); + $s = new self::$imp($this->c, $nativeStatement, ["int", ["int", "int"], "int"]); + $val = $s->runArray([1, [2, 3], 4])->getRow(); + $this->assertSame($exp, $val); + } + + public function testBindWithoutType() { + $nativeStatement = $this->c->prepare("SELECT ? as value"); + $this->assertException("paramTypeMissing", "Db"); + $s = new self::$imp($this->c, $nativeStatement, []); + $s->runArray([1]); + } + + public function testViolateConstraint() { + $this->c->exec("CREATE TABLE test(id integer not null)"); + $nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)"); + $s = new self::$imp($this->c, $nativeStatement, ["int"]); + $this->assertException("constraintViolation", "Db", "ExceptionInput"); + $s->runArray([null]); + } + + public function testMismatchTypes() { + $this->c->exec("CREATE TABLE test(id integer primary key)"); + $nativeStatement = $this->c->prepare("INSERT INTO test(id) values(?)"); + $s = new self::$imp($this->c, $nativeStatement, ["str"]); + $this->assertException("typeViolation", "Db", "ExceptionInput"); + $s->runArray(['ook']); + } +} diff --git a/tests/cases/Db/SQLite3PDO/TestDbUpdateSQLite3PDO.php b/tests/cases/Db/SQLite3PDO/TestDbUpdateSQLite3PDO.php new file mode 100644 index 0000000..b75ba7d --- /dev/null +++ b/tests/cases/Db/SQLite3PDO/TestDbUpdateSQLite3PDO.php @@ -0,0 +1,120 @@ + + * @covers \JKingWeb\Arsse\Db\PDOError */ +class TestDbUpdateSQLite3PDO extends 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(Conf $conf = null) { + if (!Db\SQLite3\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 = Db\SQLite3\PDODriver::class; + $conf->dbSQLite3File = ":memory:"; + Arsse::$conf = $conf; + $this->base = $this->vfs->url(); + $this->path = $this->base."/SQLite3/"; + $this->drv = new Db\SQLite3\PDODriver(); + } + + public function tearDown() { + unset($this->drv); + unset($this->data); + unset($this->vfs); + $this->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 + $conf = new Conf(); + $conf->dbAutoUpdate = false; + $this->setUp($conf); + $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/phpunit.xml b/tests/phpunit.xml index 76d41e9..c15db12 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -51,8 +51,14 @@ cases/Db/SQLite3/TestDbDriverCreationSQLite3.php cases/Db/SQLite3/TestDbDriverSQLite3.php cases/Db/SQLite3/TestDbUpdateSQLite3.php + + cases/Db/SQLite3PDO/TestDbResultSQLite3PDO.php + cases/Db/SQLite3PDO/TestDbStatementSQLite3PDO.php + cases/Db/SQLite3PDO/TestDbDriverCreationSQLite3PDO.php + cases/Db/SQLite3PDO/TestDbDriverSQLite3PDO.php + cases/Db/SQLite3PDO/TestDbUpdateSQLite3PDO.php - + cases/Db/SQLite3/Database/TestDatabaseMiscellanySQLite3.php cases/Db/SQLite3/Database/TestDatabaseMetaSQLite3.php cases/Db/SQLite3/Database/TestDatabaseUserSQLite3.php