17 changed files with 547 additions and 80 deletions
@ -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 |
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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"); |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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(); |
|||
} |
|||
} |
@ -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';"; |
|||
} |
Loading…
Reference in new issue