mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Implementation of native PostgreSQL interface
Changes to the Database class were required to avoid outputting booleans
This commit is contained in:
parent
b52dadf345
commit
2bebdd44cf
17 changed files with 547 additions and 80 deletions
|
@ -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
Normal file
42
lib/Db/PostgreSQL/Dispatch.php
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Normal file
48
lib/Db/PostgreSQL/Result.php
Normal file
|
@ -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
Normal file
77
lib/Db/PostgreSQL/Statement.php
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Normal file
73
tests/cases/Db/PostgreSQL/TestCreation.php
Normal file
|
@ -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
Normal file
43
tests/cases/Db/PostgreSQL/TestDatabase.php
Normal file
|
@ -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
Normal file
57
tests/cases/Db/PostgreSQL/TestDriver.php
Normal file
|
@ -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
Normal file
33
tests/cases/Db/PostgreSQL/TestResult.php
Normal file
|
@ -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
Normal file
41
tests/cases/Db/PostgreSQL/TestStatement.php
Normal file
|
@ -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
Normal file
16
tests/cases/Db/PostgreSQL/TestUpdate.php
Normal file
|
@ -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';";
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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…
Reference in a new issue