J. King
5 years ago
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