Browse Source

Implementation of native PostgreSQL interface

Changes to the Database class were required to avoid outputting booleans
microsub
J. King 5 years ago
parent
commit
2bebdd44cf
  1. 8
      lib/Database.php
  2. 42
      lib/Db/PostgreSQL/Dispatch.php
  3. 51
      lib/Db/PostgreSQL/Driver.php
  4. 28
      lib/Db/PostgreSQL/PDOStatement.php
  5. 48
      lib/Db/PostgreSQL/Result.php
  6. 77
      lib/Db/PostgreSQL/Statement.php
  7. 2
      locale/en.php
  8. 73
      tests/cases/Db/PostgreSQL/TestCreation.php
  9. 43
      tests/cases/Db/PostgreSQL/TestDatabase.php
  10. 57
      tests/cases/Db/PostgreSQL/TestDriver.php
  11. 33
      tests/cases/Db/PostgreSQL/TestResult.php
  12. 41
      tests/cases/Db/PostgreSQL/TestStatement.php
  13. 16
      tests/cases/Db/PostgreSQL/TestUpdate.php
  14. 6
      tests/cases/Db/PostgreSQLPDO/TestCreation.php
  15. 1
      tests/cases/Db/PostgreSQLPDO/TestDatabase.php
  16. 1
      tests/cases/Db/SQLite3/TestDatabase.php
  17. 100
      tests/lib/DatabaseInformation.php

8
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) 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
((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, 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,
not exists(select id from folders where id = coalesce((select dest from target),0)) as valid, case when not exists(select id from folders where id = coalesce((select dest from target),0)) then 1 else 0 end 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 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", "str",
"strict int", "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, // 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 // SQL will happily accept duplicates (null is not unique), so we must do this check ourselves
$parent = $parent ? $parent : null; $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"]); throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]);
} }
return true; return true;

42
lib/Db/PostgreSQL/Dispatch.php

@ -0,0 +1,42 @@
<?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\Db\PostgreSQL;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Conf;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
trait Dispatch {
protected function dispatchQuery(string $query, array $params = []) {
pg_send_query_params($this->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
}
}
}

51
lib/Db/PostgreSQL/Driver.php

@ -13,6 +13,9 @@ use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout; use JKingWeb\Arsse\Db\ExceptionTimeout;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
use Dispatch;
protected $db;
protected $transStart = 0; 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) { 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() { public function __destruct() {
if (isset($this->db)) {
pg_close($this->db);
unset($this->db);
}
} }
/** @codeCoverageIgnore */
public static function driverName(): string { public static function driverName(): string {
return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name"); return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name");
} }
/** @codeCoverageIgnore */
public static function requirementsMet(): bool { public static function requirementsMet(): bool {
// stub: native interface is not yet supported return \extension_loaded("pgsql");
return false;
} }
/** @codeCoverageIgnore */
protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) {
// stub: native interface is not yet supported $dsn = $this->makeconnectionString(false, $user, $pass, $db, $host, $port, $service);
throw new \Exception; 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 { protected function getError(): string {
// stub: native interface is not yet supported // stub
return ""; return "";
} }
/** @codeCoverageIgnore */
public function exec(string $query): bool { 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; return true;
} }
/** @codeCoverageIgnore */
public function query(string $query): \JKingWeb\Arsse\Db\Result { public function query(string $query): \JKingWeb\Arsse\Db\Result {
// stub: native interface is not yet supported $r = $this->dispatchQuery($query);
return new ResultEmpty; 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 { public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
// stub: native interface is not yet supported return new Statement($this->db, $query, $paramTypes);
return new Statement($this->db, $s, $paramTypes);
} }
} }

28
lib/Db/PostgreSQL/PDOStatement.php

@ -6,18 +6,9 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\Arsse\Db\PostgreSQL; namespace JKingWeb\Arsse\Db\PostgreSQL;
class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement { class PDOStatement extends Statement {
use \JKingWeb\Arsse\Db\PDOError; 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 $db;
protected $st; protected $st;
protected $qOriginal; protected $qOriginal;
@ -25,7 +16,7 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement {
protected $bindings; protected $bindings;
public function __construct(\PDO $db, string $query, array $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->qOriginal = $query;
$this->retypeArray($bindings); $this->retypeArray($bindings);
} }
@ -53,19 +44,6 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement {
return true; 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 { public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
return $this->st->runArray($values); return $this->st->runArray($values);
} }
@ -73,6 +51,6 @@ class PDOStatement extends \JKingWeb\Arsse\Db\AbstractStatement {
/** @codeCoverageIgnore */ /** @codeCoverageIgnore */
protected function bindValue($value, string $type, int $position): bool { protected function bindValue($value, string $type, int $position): bool {
// stub required by abstract parent, but never used // stub required by abstract parent, but never used
return $value; return true;
} }
} }

48
lib/Db/PostgreSQL/Result.php

@ -0,0 +1,48 @@
<?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\Db\PostgreSQL;
use JKingWeb\Arsse\Db\Exception;
class Result extends \JKingWeb\Arsse\Db\AbstractResult {
protected $db;
protected $r;
protected $cur;
// actual public methods
public function changes(): int {
return pg_affected_rows($this->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);
}
}

77
lib/Db/PostgreSQL/Statement.php

@ -0,0 +1,77 @@
<?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\Db\PostgreSQL;
use JKingWeb\Arsse\Db\Exception;
use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
use Dispatch;
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 $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;
}
}

2
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.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.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.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.engineConstraintViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}',
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}', 'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}',

73
tests/cases/Db/PostgreSQL/TestCreation.php

@ -0,0 +1,73 @@
<?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\PostgreSQL;
use JKingWeb\Arsse\Arsse;
use JKingWeb\Arsse\Db\PostgreSQL\Driver;
/**
* @group slow
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
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;
}
}

43
tests/cases/Db/PostgreSQL/TestDatabase.php

@ -0,0 +1,43 @@
<?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\PostgreSQL;
/**
* @group slow
* @group coverageOptional
* @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended>
*/
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");
}
}
}

57
tests/cases/Db/PostgreSQL/TestDriver.php

@ -0,0 +1,57 @@
<?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\PostgreSQL;
/**
* @group slow
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
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;
}
}
}

33
tests/cases/Db/PostgreSQL/TestResult.php

@ -0,0 +1,33 @@
<?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\PostgreSQL;
use JKingWeb\Arsse\Test\DatabaseInformation;
/**
* @group slow
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Result<extended>
*/
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();
}
}

41
tests/cases/Db/PostgreSQL/TestStatement.php

@ -0,0 +1,41 @@
<?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\PostgreSQL;
/**
* @group slow
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Statement<extended> */
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();
}
}

16
tests/cases/Db/PostgreSQL/TestUpdate.php

@ -0,0 +1,16 @@
<?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\PostgreSQL;
/**
* @group slow
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
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';";
}

6
tests/cases/Db/PostgreSQLPDO/TestCreation.php

@ -13,6 +13,12 @@ use JKingWeb\Arsse\Db\PostgreSQL\PDODriver as Driver;
* @group slow * @group slow
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended> */ * @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended> */
class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
public function setUp() {
if (!Driver::requirementsMet()) {
$this->markTestSkipped("PDO-PostgreSQL extension not loaded");
}
}
/** @dataProvider provideConnectionStrings */ /** @dataProvider provideConnectionStrings */
public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) {
self::setConf(); self::setConf();

1
tests/cases/Db/PostgreSQLPDO/TestDatabase.php

@ -8,6 +8,7 @@ namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
/** /**
* @group slow * @group slow
* @group optional
* @group coverageOptional * @group coverageOptional
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended> * @covers \JKingWeb\Arsse\Misc\Query<extended>

1
tests/cases/Db/SQLite3/TestDatabase.php

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\TestCase\Db\SQLite3; namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
/** /**
* @group optional
* @covers \JKingWeb\Arsse\Database<extended> * @covers \JKingWeb\Arsse\Database<extended>
* @covers \JKingWeb\Arsse\Misc\Query<extended> * @covers \JKingWeb\Arsse\Misc\Query<extended>
*/ */

100
tests/lib/DatabaseInformation.php

@ -116,7 +116,50 @@ class DatabaseInformation {
} elseif ($db instanceof \PDO) { } elseif ($db instanceof \PDO) {
return $db->query($listObjects)->fetchAll(\PDO::FETCH_ASSOC); return $db->query($listObjects)->fetchAll(\PDO::FETCH_ASSOC);
} else { } 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 [ return [
@ -158,6 +201,27 @@ class DatabaseInformation {
'truncateFunction' => $sqlite3TruncateFunction, 'truncateFunction' => $sqlite3TruncateFunction,
'razeFunction' => $sqlite3RazeFunction, '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 PostgreSQL' => [
'pdo' => true, 'pdo' => true,
'backend' => "PostgreSQL", 'backend' => "PostgreSQL",
@ -177,38 +241,8 @@ class DatabaseInformation {
} }
return $d; return $d;
}, },
'truncateFunction' => function($db, array $afterStatements = []) use ($pgObjectList) { 'truncateFunction' => $pgTruncateFunction,
// rollback any pending transaction 'razeFunction' => $pgRazeFunction,
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);
}
},
], ],
]; ];
} }

Loading…
Cancel
Save