From 2bebdd44cf83ac6900f20d74473627587c6ef91b Mon Sep 17 00:00:00 2001 From: "J. King" Date: Thu, 13 Dec 2018 19:47:51 -0500 Subject: [PATCH] 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 18da777..ae175d7 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 0000000..c6cb198 --- /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 3760324..e681f72 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 1658260..534efbc 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 0000000..3b6cf9c --- /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 0000000..bccd0fb --- /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 3e63ac6..dc5381f 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 0000000..182d3a3 --- /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 0000000..efc19a6 --- /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 0000000..86decea --- /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 0000000..08fff06 --- /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 0000000..de350db --- /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 0000000..cbdcb0b --- /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 3561ff4..83d95e8 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 42d5f9f..6ce5de7 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 35448d4..c65027c 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 a550847..ea70afc 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, ], ]; }