J. King
6 years ago
129 changed files with 4537 additions and 3258 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,223 @@ |
|||
<?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; |
|||
|
|||
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { |
|||
use Dispatch; |
|||
|
|||
protected $db; |
|||
protected $transStart = 0; |
|||
|
|||
public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) { |
|||
// check to make sure required extension is loaded |
|||
if (!static::requirementsMet()) { |
|||
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore |
|||
} |
|||
$user = $user ?? Arsse::$conf->dbPostgreSQLUser; |
|||
$pass = $pass ?? Arsse::$conf->dbPostgreSQLPass; |
|||
$db = $db ?? Arsse::$conf->dbPostgreSQLDb; |
|||
$host = $host ?? Arsse::$conf->dbPostgreSQLHost; |
|||
$port = $port ?? Arsse::$conf->dbPostgreSQLPort; |
|||
$schema = $schema ?? Arsse::$conf->dbPostgreSQLSchema; |
|||
$service = $service ?? Arsse::$conf->dbPostgreSQLService; |
|||
$this->makeConnection($user, $pass, $db, $host, $port, $service); |
|||
foreach (static::makeSetupQueries($schema) as $q) { |
|||
$this->exec($q); |
|||
} |
|||
} |
|||
|
|||
public static function makeConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service): string { |
|||
$base = [ |
|||
'client_encoding' => "UTF8", |
|||
'application_name' => "arsse", |
|||
'connect_timeout' => (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0), |
|||
]; |
|||
$out = []; |
|||
if ($service != "") { |
|||
$out['service'] = $service; |
|||
} else { |
|||
if ($host != "") { |
|||
$out['host'] = $host; |
|||
} |
|||
if ($port != 5432 && !($host != "" && $host[0] == "/")) { |
|||
$out['port'] = (string) $port; |
|||
} |
|||
if ($db != "") { |
|||
$out['dbname'] = $db; |
|||
} |
|||
if (!$pdo) { |
|||
$out['user'] = $user; |
|||
if ($pass != "") { |
|||
$out['password'] = $pass; |
|||
} |
|||
} |
|||
} |
|||
ksort($out); |
|||
ksort($base); |
|||
$out = array_merge($out, $base); |
|||
$out = array_map(function($v, $k) { |
|||
return "$k='".str_replace("'", "\\'", str_replace("\\", "\\\\", $v))."'"; |
|||
}, $out, array_keys($out)); |
|||
return implode(" ", $out); |
|||
} |
|||
|
|||
public static function makeSetupQueries(string $schema = ""): array { |
|||
$timeout = ceil(Arsse::$conf->dbTimeoutExec * 1000); |
|||
$out = [ |
|||
"SET TIME ZONE UTC", |
|||
"SET DateStyle = 'ISO, MDY'", |
|||
"SET statement_timeout = '$timeout'", |
|||
]; |
|||
if (strlen($schema) > 0) { |
|||
$schema = '"'.str_replace('"', '""', $schema).'"'; |
|||
$out[] = "SET search_path = $schema, public"; |
|||
} |
|||
return $out; |
|||
} |
|||
|
|||
/** @codeCoverageIgnore */ |
|||
public static function create(): \JKingWeb\Arsse\Db\Driver { |
|||
if (self::requirementsMet()) { |
|||
return new self; |
|||
} elseif (PDODriver::requirementsMet()) { |
|||
return new PDODriver; |
|||
} else { |
|||
throw new Exception("extMissing", self::driverName()); |
|||
} |
|||
} |
|||
|
|||
public static function schemaID(): string { |
|||
return "PostgreSQL"; |
|||
} |
|||
|
|||
public function charsetAcceptable(): bool { |
|||
return $this->query("SELECT pg_encoding_to_char(encoding) from pg_database where datname = current_database()")->getValue() == "UTF8"; |
|||
} |
|||
|
|||
public function schemaVersion(): int { |
|||
if ($this->query("SELECT count(*) from information_schema.tables where table_name = 'arsse_meta' and table_schema = current_schema()")->getValue()) { |
|||
return (int) $this->query("SELECT value from arsse_meta where key = 'schema_version'")->getValue(); |
|||
} else { |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
public function sqlToken(string $token): string { |
|||
switch (strtolower($token)) { |
|||
case "nocase": |
|||
return '"und-x-icu"'; |
|||
default: |
|||
return $token; |
|||
} |
|||
} |
|||
|
|||
public function savepointCreate(bool $lock = false): int { |
|||
if (!$this->transStart) { |
|||
$this->exec("BEGIN TRANSACTION"); |
|||
$this->transStart = parent::savepointCreate($lock); |
|||
return $this->transStart; |
|||
} else { |
|||
return parent::savepointCreate($lock); |
|||
} |
|||
} |
|||
|
|||
public function savepointRelease(int $index = null): bool { |
|||
$index = $index ?? $this->transDepth; |
|||
$out = parent::savepointRelease($index); |
|||
if ($index == $this->transStart) { |
|||
$this->exec("COMMIT"); |
|||
$this->transStart = 0; |
|||
} |
|||
return $out; |
|||
} |
|||
|
|||
public function savepointUndo(int $index = null): bool { |
|||
$index = $index ?? $this->transDepth; |
|||
$out = parent::savepointUndo($index); |
|||
if ($index == $this->transStart) { |
|||
$this->exec("ROLLBACK"); |
|||
$this->transStart = 0; |
|||
} |
|||
return $out; |
|||
} |
|||
|
|||
protected function lock(): bool { |
|||
if ($this->query("SELECT count(*) from information_schema.tables where table_schema = current_schema() and table_name = 'arsse_meta'")->getValue()) { |
|||
$this->exec("LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
protected function unlock(bool $rollback = false): bool { |
|||
// do nothing; transaction is committed or rolled back later |
|||
return true; |
|||
} |
|||
|
|||
public function __destruct() { |
|||
if (isset($this->db)) { |
|||
pg_close($this->db); |
|||
unset($this->db); |
|||
} |
|||
} |
|||
|
|||
public static function driverName(): string { |
|||
return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name"); |
|||
} |
|||
|
|||
public static function requirementsMet(): bool { |
|||
return \extension_loaded("pgsql"); |
|||
} |
|||
|
|||
protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { |
|||
$dsn = $this->makeconnectionString(false, $user, $pass, $db, $host, $port, $service); |
|||
set_error_handler(function(int $code, string $msg) { |
|||
$msg = substr($msg, 62); |
|||
throw new Exception("connectionFailure", ["PostgreSQL", $msg]); |
|||
}); |
|||
try { |
|||
$this->db = pg_connect($dsn, \PGSQL_CONNECT_FORCE_NEW); |
|||
} finally { |
|||
restore_error_handler(); |
|||
} |
|||
} |
|||
|
|||
protected function getError(): string { |
|||
// stub |
|||
return ""; |
|||
} |
|||
|
|||
public function exec(string $query): bool { |
|||
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; |
|||
} |
|||
|
|||
public function query(string $query): \JKingWeb\Arsse\Db\Result { |
|||
$r = $this->dispatchQuery($query); |
|||
if (is_resource($r)) { |
|||
return new Result($this->db, $r); |
|||
} else { |
|||
list($excClass, $excMsg, $excData) = $r; |
|||
throw new $excClass($excMsg, $excData); |
|||
} |
|||
} |
|||
|
|||
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { |
|||
return new Statement($this->db, $query, $paramTypes); |
|||
} |
|||
} |
@ -0,0 +1,66 @@ |
|||
<?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\Db\Exception; |
|||
use JKingWeb\Arsse\Db\ExceptionInput; |
|||
use JKingWeb\Arsse\Db\ExceptionTimeout; |
|||
|
|||
class PDODriver extends Driver { |
|||
use \JKingWeb\Arsse\Db\PDODriver; |
|||
|
|||
protected $db; |
|||
|
|||
public static function requirementsMet(): bool { |
|||
return class_exists("PDO") && in_array("pgsql", \PDO::getAvailableDrivers()); |
|||
} |
|||
|
|||
protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) { |
|||
$dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port, $service); |
|||
try { |
|||
$this->db = new \PDO("pgsql:$dsn", $user, $pass, [ |
|||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, |
|||
\PDO::ATTR_PERSISTENT => true, |
|||
]); |
|||
} catch (\PDOException $e) { |
|||
if ($e->getCode() == 7) { |
|||
switch (substr($e->getMessage(), 9, 5)) { |
|||
case "08006": |
|||
throw new Exception("connectionFailure", ["PostgreSQL", substr($e->getMessage(), 28)]); |
|||
default: |
|||
throw $e; // @codeCoverageIgnore |
|||
} |
|||
} |
|||
throw $e; // @codeCoverageIgnore |
|||
} |
|||
} |
|||
|
|||
public function __destruct() { |
|||
unset($this->db); |
|||
} |
|||
|
|||
/** @codeCoverageIgnore */ |
|||
public static function create(): \JKingWeb\Arsse\Db\Driver { |
|||
if (self::requirementsMet()) { |
|||
return new self; |
|||
} elseif (Driver::requirementsMet()) { |
|||
return new Driver; |
|||
} else { |
|||
throw new Exception("extMissing", self::driverName()); |
|||
} |
|||
} |
|||
|
|||
|
|||
public static function driverName(): string { |
|||
return Arsse::$lang->msg("Driver.Db.PostgreSQLPDO.Name"); |
|||
} |
|||
|
|||
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { |
|||
return new PDOStatement($this->db, $query, $paramTypes); |
|||
} |
|||
} |
@ -0,0 +1,56 @@ |
|||
<?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; |
|||
|
|||
class PDOStatement extends Statement { |
|||
use \JKingWeb\Arsse\Db\PDOError; |
|||
|
|||
protected $db; |
|||
protected $st; |
|||
protected $qOriginal; |
|||
protected $qMunged; |
|||
protected $bindings; |
|||
|
|||
public function __construct(\PDO $db, string $query, array $bindings = []) { |
|||
$this->db = $db; |
|||
$this->qOriginal = $query; |
|||
$this->retypeArray($bindings); |
|||
} |
|||
|
|||
public function __destruct() { |
|||
unset($this->db, $this->st); |
|||
} |
|||
|
|||
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, false); |
|||
try { |
|||
// statement creation with PostgreSQL should never fail (it is not evaluated at creation time) |
|||
$s = $this->db->prepare($this->qMunged); |
|||
} catch (\PDOException $e) { // @codeCoverageIgnore |
|||
list($excClass, $excMsg, $excData) = $this->exceptionBuild(true); // @codeCoverageIgnore |
|||
throw new $excClass($excMsg, $excData); // @codeCoverageIgnore |
|||
} |
|||
$this->st = new \JKingWeb\Arsse\Db\PDOStatement($this->db, $s, $this->bindings); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { |
|||
return $this->st->runArray($values); |
|||
} |
|||
|
|||
/** @codeCoverageIgnore */ |
|||
protected function bindValue($value, string $type, int $position): bool { |
|||
// stub required by abstract parent, but never used |
|||
return true; |
|||
} |
|||
} |
@ -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,113 @@ |
|||
-- SPDX-License-Identifier: MIT |
|||
-- Copyright 2017 J. King, Dustin Wilson et al. |
|||
-- See LICENSE and AUTHORS files for details |
|||
|
|||
-- Please consult the SQLite 3 schemata for commented version |
|||
|
|||
create table arsse_meta( |
|||
key text primary key, |
|||
value text |
|||
); |
|||
|
|||
create table arsse_users( |
|||
id text primary key, |
|||
password text, |
|||
name text, |
|||
avatar_type text, |
|||
avatar_data bytea, |
|||
admin smallint default 0, |
|||
rights bigint not null default 0 |
|||
); |
|||
|
|||
create table arsse_users_meta( |
|||
owner text not null references arsse_users(id) on delete cascade on update cascade, |
|||
key text not null, |
|||
value text, |
|||
primary key(owner,key) |
|||
); |
|||
|
|||
create table arsse_folders( |
|||
id bigserial primary key, |
|||
owner text not null references arsse_users(id) on delete cascade on update cascade, |
|||
parent bigint references arsse_folders(id) on delete cascade, |
|||
name text not null, |
|||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, -- |
|||
unique(owner,name,parent) |
|||
); |
|||
|
|||
create table arsse_feeds( |
|||
id bigserial primary key, |
|||
url text not null, |
|||
title text, |
|||
favicon text, |
|||
source text, |
|||
updated timestamp(0) without time zone, |
|||
modified timestamp(0) without time zone, |
|||
next_fetch timestamp(0) without time zone, |
|||
orphaned timestamp(0) without time zone, |
|||
etag text not null default '', |
|||
err_count bigint not null default 0, |
|||
err_msg text, |
|||
username text not null default '', |
|||
password text not null default '', |
|||
size bigint not null default 0, |
|||
scrape smallint not null default 0, |
|||
unique(url,username,password) |
|||
); |
|||
|
|||
create table arsse_subscriptions( |
|||
id bigserial primary key, |
|||
owner text not null references arsse_users(id) on delete cascade on update cascade, |
|||
feed bigint not null references arsse_feeds(id) on delete cascade, |
|||
added timestamp(0) without time zone not null default CURRENT_TIMESTAMP, |
|||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, |
|||
title text, |
|||
order_type smallint not null default 0, |
|||
pinned smallint not null default 0, |
|||
folder bigint references arsse_folders(id) on delete cascade, |
|||
unique(owner,feed) |
|||
); |
|||
|
|||
create table arsse_articles( |
|||
id bigserial primary key, |
|||
feed bigint not null references arsse_feeds(id) on delete cascade, |
|||
url text, |
|||
title text, |
|||
author text, |
|||
published timestamp(0) without time zone, |
|||
edited timestamp(0) without time zone, |
|||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, |
|||
content text, |
|||
guid text, |
|||
url_title_hash text not null, |
|||
url_content_hash text not null, |
|||
title_content_hash text not null |
|||
); |
|||
|
|||
create table arsse_enclosures( |
|||
article bigint not null references arsse_articles(id) on delete cascade, |
|||
url text, |
|||
type text |
|||
); |
|||
|
|||
create table arsse_marks( |
|||
article bigint not null references arsse_articles(id) on delete cascade, |
|||
subscription bigint not null references arsse_subscriptions(id) on delete cascade on update cascade, |
|||
read smallint not null default 0, |
|||
starred smallint not null default 0, |
|||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, |
|||
primary key(article,subscription) |
|||
); |
|||
|
|||
create table arsse_editions( |
|||
id bigserial primary key, |
|||
article bigint not null references arsse_articles(id) on delete cascade, |
|||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP |
|||
); |
|||
|
|||
create table arsse_categories( |
|||
article bigint not null references arsse_articles(id) on delete cascade, |
|||
name text |
|||
); |
|||
|
|||
insert into arsse_meta(key,value) values('schema_version','1'); |
@ -0,0 +1,33 @@ |
|||
-- SPDX-License-Identifier: MIT |
|||
-- Copyright 2017 J. King, Dustin Wilson et al. |
|||
-- See LICENSE and AUTHORS files for details |
|||
|
|||
-- Please consult the SQLite 3 schemata for commented version |
|||
|
|||
create table arsse_sessions ( |
|||
id text primary key, |
|||
created timestamp(0) without time zone not null default CURRENT_TIMESTAMP, |
|||
expires timestamp(0) without time zone not null, |
|||
"user" text not null references arsse_users(id) on delete cascade on update cascade |
|||
); |
|||
|
|||
create table arsse_labels ( |
|||
id bigserial primary key, |
|||
owner text not null references arsse_users(id) on delete cascade on update cascade, |
|||
name text not null, |
|||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, |
|||
unique(owner,name) |
|||
); |
|||
|
|||
create table arsse_label_members ( |
|||
label bigint not null references arsse_labels(id) on delete cascade, |
|||
article bigint not null references arsse_articles(id) on delete cascade, |
|||
subscription bigint not null references arsse_subscriptions(id) on delete cascade, |
|||
assigned smallint not null default 1, |
|||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, |
|||
primary key(label,article) |
|||
); |
|||
|
|||
alter table arsse_marks add column note text not null default ''; |
|||
|
|||
update arsse_meta set value = '2' where key = 'schema_version'; |
@ -0,0 +1,16 @@ |
|||
-- SPDX-License-Identifier: MIT |
|||
-- Copyright 2017 J. King, Dustin Wilson et al. |
|||
-- See LICENSE and AUTHORS files for details |
|||
|
|||
-- Please consult the SQLite 3 schemata for commented version |
|||
|
|||
alter table arsse_users alter column id type text collate "und-x-icu"; |
|||
alter table arsse_folders alter column name type text collate "und-x-icu"; |
|||
alter table arsse_feeds alter column title type text collate "und-x-icu"; |
|||
alter table arsse_subscriptions alter column title type text collate "und-x-icu"; |
|||
alter table arsse_articles alter column title type text collate "und-x-icu"; |
|||
alter table arsse_articles alter column author type text collate "und-x-icu"; |
|||
alter table arsse_categories alter column name type text collate "und-x-icu"; |
|||
alter table arsse_labels alter column name type text collate "und-x-icu"; |
|||
|
|||
update arsse_meta set value = '3' where key = 'schema_version'; |
@ -0,0 +1,11 @@ |
|||
-- SPDX-License-Identifier: MIT |
|||
-- Copyright 2017 J. King, Dustin Wilson et al. |
|||
-- See LICENSE and AUTHORS files for details |
|||
|
|||
-- Please consult the SQLite 3 schemata for commented version |
|||
|
|||
alter table arsse_marks alter column modified drop default; |
|||
alter table arsse_marks alter column modified drop not null; |
|||
alter table arsse_marks add column touched smallint not null default 0; |
|||
|
|||
update arsse_meta set value = '4' where key = 'schema_version'; |
@ -0,0 +1,27 @@ |
|||
-- SPDX-License-Identifier: MIT |
|||
-- Copyright 2017 J. King, Dustin Wilson et al. |
|||
-- See LICENSE and AUTHORS files for details |
|||
|
|||
-- allow marks to initially have a null date due to changes in how marks are first created |
|||
-- and also add a "touched" column to aid in tracking changes during the course of some transactions |
|||
alter table arsse_marks rename to arsse_marks_old; |
|||
create table arsse_marks( |
|||
-- users' actions on newsfeed entries |
|||
article integer not null references arsse_articles(id) on delete cascade, -- article associated with the marks |
|||
subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- subscription associated with the marks; the subscription in turn belongs to a user |
|||
read boolean not null default 0, -- whether the article has been read |
|||
starred boolean not null default 0, -- whether the article is starred |
|||
modified text, -- time at which an article was last modified by a given user |
|||
note text not null default '', -- Tiny Tiny RSS freeform user note |
|||
touched boolean not null default 0, -- used to indicate a record has been modified during the course of some transactions |
|||
primary key(article,subscription) -- no more than one mark-set per article per user |
|||
); |
|||
insert into arsse_marks select article,subscription,read,starred,modified,note,0 from arsse_marks_old; |
|||
drop table arsse_marks_old; |
|||
|
|||
-- reindex anything which uses the nocase collation sequence; it has been replaced with a Unicode collation |
|||
reindex nocase; |
|||
|
|||
-- set version marker |
|||
pragma user_version = 4; |
|||
update arsse_meta set value = '4' where key = 'schema_version'; |
@ -0,0 +1,398 @@ |
|||
<?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; |
|||
|
|||
use JKingWeb\Arsse\Db\Statement; |
|||
use JKingWeb\Arsse\Db\Result; |
|||
use JKingWeb\Arsse\Test\DatabaseInformation; |
|||
|
|||
abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
protected static $dbInfo; |
|||
protected static $interface; |
|||
protected $drv; |
|||
protected $create; |
|||
protected $lock; |
|||
protected $setVersion; |
|||
protected static $conf = [ |
|||
'dbTimeoutExec' => 0.5, |
|||
'dbSQLite3Timeout' => 0, |
|||
//'dbSQLite3File' => "(temporary file)", |
|||
]; |
|||
|
|||
public static function setUpBeforeClass() { |
|||
// establish a clean baseline |
|||
static::clearData(); |
|||
static::$dbInfo = new DatabaseInformation(static::$implementation); |
|||
static::setConf(static::$conf); |
|||
static::$interface = (static::$dbInfo->interfaceConstructor)(); |
|||
} |
|||
|
|||
public function setUp() { |
|||
self::clearData(); |
|||
self::setConf(static::$conf); |
|||
if (!static::$interface) { |
|||
$this->markTestSkipped(static::$implementation." database driver not available"); |
|||
} |
|||
// completely clear the database and ensure the schema version can easily be altered |
|||
(static::$dbInfo->razeFunction)(static::$interface, [ |
|||
"CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)", |
|||
"INSERT INTO arsse_meta(key,value) values('schema_version','0')", |
|||
]); |
|||
// construct a fresh driver for each test |
|||
$this->drv = new static::$dbInfo->driverClass; |
|||
} |
|||
|
|||
public function tearDown() { |
|||
// deconstruct the driver |
|||
unset($this->drv); |
|||
self::clearData(); |
|||
} |
|||
|
|||
public static function tearDownAfterClass() { |
|||
if (static::$interface) { |
|||
// completely clear the database |
|||
(static::$dbInfo->razeFunction)(static::$interface); |
|||
} |
|||
static::$interface = null; |
|||
static::$dbInfo = null; |
|||
self::clearData(); |
|||
} |
|||
|
|||
protected function exec($q): bool { |
|||
// PDO implementation |
|||
$q = (!is_array($q)) ? [$q] : $q; |
|||
foreach ($q as $query) { |
|||
static::$interface->exec((string) $query); |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
protected function query(string $q) { |
|||
// PDO implementation |
|||
return static::$interface->query($q)->fetchColumn(); |
|||
} |
|||
|
|||
# TESTS |
|||
|
|||
public function testFetchDriverName() { |
|||
$class = get_class($this->drv); |
|||
$this->assertTrue(strlen($class::driverName()) > 0); |
|||
} |
|||
|
|||
public function testFetchSchemaId() { |
|||
$class = get_class($this->drv); |
|||
$this->assertTrue(strlen($class::schemaID()) > 0); |
|||
} |
|||
|
|||
public function testCheckCharacterSetAcceptability() { |
|||
$this->assertTrue($this->drv->charsetAcceptable()); |
|||
} |
|||
|
|||
public function testTranslateAToken() { |
|||
$this->assertRegExp("/^[a-z][a-z0-9]*$/i", $this->drv->sqlToken("greatest")); |
|||
$this->assertSame("distinct", $this->drv->sqlToken("distinct")); |
|||
} |
|||
|
|||
public function testExecAValidStatement() { |
|||
$this->assertTrue($this->drv->exec($this->create)); |
|||
} |
|||
|
|||
public function testExecAnInvalidStatement() { |
|||
$this->assertException("engineErrorGeneral", "Db"); |
|||
$this->drv->exec("And the meek shall inherit the earth..."); |
|||
} |
|||
|
|||
public function testExecMultipleStatements() { |
|||
$this->assertTrue($this->drv->exec("$this->create; INSERT INTO arsse_test(id) values(2112)")); |
|||
$this->assertEquals(2112, $this->query("SELECT id from arsse_test")); |
|||
} |
|||
|
|||
public function testExecTimeout() { |
|||
$this->exec($this->create); |
|||
$this->exec($this->lock); |
|||
$this->assertException("general", "Db", "ExceptionTimeout"); |
|||
$lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock; |
|||
$this->drv->exec($lock); |
|||
} |
|||
|
|||
public function testExecConstraintViolation() { |
|||
$this->drv->exec("CREATE TABLE arsse_test(id varchar(255) not null)"); |
|||
$this->assertException("constraintViolation", "Db", "ExceptionInput"); |
|||
$this->drv->exec("INSERT INTO arsse_test default values"); |
|||
} |
|||
|
|||
public function testExecTypeViolation() { |
|||
$this->drv->exec($this->create); |
|||
$this->assertException("typeViolation", "Db", "ExceptionInput"); |
|||
$this->drv->exec("INSERT INTO arsse_test(id) values('ook')"); |
|||
} |
|||
|
|||
public function testMakeAValidQuery() { |
|||
$this->assertInstanceOf(Result::class, $this->drv->query("SELECT 1")); |
|||
} |
|||
|
|||
public function testMakeAnInvalidQuery() { |
|||
$this->assertException("engineErrorGeneral", "Db"); |
|||
$this->drv->query("Apollo was astonished; Dionysus thought me mad"); |
|||
} |
|||
|
|||
public function testQueryTimeout() { |
|||
$this->exec($this->create); |
|||
$this->exec($this->lock); |
|||
$this->assertException("general", "Db", "ExceptionTimeout"); |
|||
$lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock; |
|||
$this->drv->exec($lock); |
|||
} |
|||
|
|||
public function testQueryConstraintViolation() { |
|||
$this->drv->exec("CREATE TABLE arsse_test(id integer not null)"); |
|||
$this->assertException("constraintViolation", "Db", "ExceptionInput"); |
|||
$this->drv->query("INSERT INTO arsse_test default values"); |
|||
} |
|||
|
|||
public function testQueryTypeViolation() { |
|||
$this->drv->exec($this->create); |
|||
$this->assertException("typeViolation", "Db", "ExceptionInput"); |
|||
$this->drv->query("INSERT INTO arsse_test(id) values('ook')"); |
|||
} |
|||
|
|||
public function testPrepareAValidQuery() { |
|||
$s = $this->drv->prepare("SELECT ?, ?", "int", "int"); |
|||
$this->assertInstanceOf(Statement::class, $s); |
|||
} |
|||
|
|||
public function testPrepareAnInvalidQuery() { |
|||
$this->assertException("engineErrorGeneral", "Db"); |
|||
$s = $this->drv->prepare("This is an invalid query", "int", "int")->run(); |
|||
} |
|||
|
|||
public function testCreateASavepoint() { |
|||
$this->assertEquals(1, $this->drv->savepointCreate()); |
|||
$this->assertEquals(2, $this->drv->savepointCreate()); |
|||
$this->assertEquals(3, $this->drv->savepointCreate()); |
|||
} |
|||
|
|||
public function testReleaseASavepoint() { |
|||
$this->assertEquals(1, $this->drv->savepointCreate()); |
|||
$this->assertEquals(true, $this->drv->savepointRelease()); |
|||
$this->assertException("savepointInvalid", "Db"); |
|||
$this->drv->savepointRelease(); |
|||
} |
|||
|
|||
public function testUndoASavepoint() { |
|||
$this->assertEquals(1, $this->drv->savepointCreate()); |
|||
$this->assertEquals(true, $this->drv->savepointUndo()); |
|||
$this->assertException("savepointInvalid", "Db"); |
|||
$this->drv->savepointUndo(); |
|||
} |
|||
|
|||
public function testManipulateSavepoints() { |
|||
$this->assertEquals(1, $this->drv->savepointCreate()); |
|||
$this->assertEquals(2, $this->drv->savepointCreate()); |
|||
$this->assertEquals(3, $this->drv->savepointCreate()); |
|||
$this->assertEquals(4, $this->drv->savepointCreate()); |
|||
$this->assertEquals(5, $this->drv->savepointCreate()); |
|||
$this->assertTrue($this->drv->savepointUndo(3)); |
|||
$this->assertFalse($this->drv->savepointRelease(4)); |
|||
$this->assertEquals(6, $this->drv->savepointCreate()); |
|||
$this->assertFalse($this->drv->savepointRelease(5)); |
|||
$this->assertTrue($this->drv->savepointRelease(6)); |
|||
$this->assertEquals(3, $this->drv->savepointCreate()); |
|||
$this->assertTrue($this->drv->savepointRelease(2)); |
|||
$this->assertException("savepointStale", "Db"); |
|||
$this->drv->savepointRelease(2); |
|||
} |
|||
|
|||
public function testManipulateSavepointsSomeMore() { |
|||
$this->assertEquals(1, $this->drv->savepointCreate()); |
|||
$this->assertEquals(2, $this->drv->savepointCreate()); |
|||
$this->assertEquals(3, $this->drv->savepointCreate()); |
|||
$this->assertEquals(4, $this->drv->savepointCreate()); |
|||
$this->assertTrue($this->drv->savepointRelease(2)); |
|||
$this->assertFalse($this->drv->savepointUndo(3)); |
|||
$this->assertException("savepointStale", "Db"); |
|||
$this->drv->savepointUndo(2); |
|||
} |
|||
|
|||
public function testBeginATransaction() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(2, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
} |
|||
|
|||
public function testCommitATransaction() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr->commit(); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(1, $this->query($select)); |
|||
} |
|||
|
|||
public function testRollbackATransaction() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr->rollback(); |
|||
$this->assertEquals(0, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
} |
|||
|
|||
public function testBeginChainedTransactions() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr1 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(2, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
} |
|||
|
|||
public function testCommitChainedTransactions() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr1 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(2, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2->commit(); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr1->commit(); |
|||
$this->assertEquals(2, $this->query($select)); |
|||
} |
|||
|
|||
public function testCommitChainedTransactionsOutOfOrder() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr1 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(2, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr1->commit(); |
|||
$this->assertEquals(2, $this->query($select)); |
|||
$tr2->commit(); |
|||
} |
|||
|
|||
public function testRollbackChainedTransactions() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr1 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(2, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2->rollback(); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr1->rollback(); |
|||
$this->assertEquals(0, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
} |
|||
|
|||
public function testRollbackChainedTransactionsOutOfOrder() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr1 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(2, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr1->rollback(); |
|||
$this->assertEquals(0, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2->rollback(); |
|||
$this->assertEquals(0, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
} |
|||
|
|||
public function testPartiallyRollbackChainedTransactions() { |
|||
$select = "SELECT count(*) FROM arsse_test"; |
|||
$insert = "INSERT INTO arsse_test default values"; |
|||
$this->drv->exec($this->create); |
|||
$tr1 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2 = $this->drv->begin(); |
|||
$this->drv->query($insert); |
|||
$this->assertEquals(2, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr2->rollback(); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(0, $this->query($select)); |
|||
$tr1->commit(); |
|||
$this->assertEquals(1, $this->drv->query($select)->getValue()); |
|||
$this->assertEquals(1, $this->query($select)); |
|||
} |
|||
|
|||
public function testFetchSchemaVersion() { |
|||
$this->assertSame(0, $this->drv->schemaVersion()); |
|||
$this->drv->exec(str_replace("#", "1", $this->setVersion)); |
|||
$this->assertSame(1, $this->drv->schemaVersion()); |
|||
$this->drv->exec(str_replace("#", "2", $this->setVersion)); |
|||
$this->assertSame(2, $this->drv->schemaVersion()); |
|||
// SQLite is unaffected by the removal of the metadata table; other backends are |
|||
// in neither case should a query for the schema version produce an error, however |
|||
$this->exec("DROP TABLE IF EXISTS arsse_meta"); |
|||
$exp = (static::$dbInfo->backend == "SQLite 3") ? 2 : 0; |
|||
$this->assertSame($exp, $this->drv->schemaVersion()); |
|||
} |
|||
|
|||
public function testLockTheDatabase() { |
|||
// PostgreSQL doesn't actually lock the whole database, only the metadata table |
|||
// normally the application will first query this table to ensure the schema version is correct, |
|||
// so the effect is usually the same |
|||
$this->drv->savepointCreate(true); |
|||
$this->assertException(); |
|||
$this->exec($this->lock); |
|||
} |
|||
|
|||
public function testUnlockTheDatabase() { |
|||
$this->drv->savepointCreate(true); |
|||
$this->drv->savepointRelease(); |
|||
$this->drv->savepointCreate(true); |
|||
$this->drv->savepointUndo(); |
|||
$this->assertTrue($this->exec(str_replace("#", "3", $this->setVersion))); |
|||
} |
|||
} |
@ -0,0 +1,137 @@ |
|||
<?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; |
|||
|
|||
use JKingWeb\Arsse\Db\Result; |
|||
use JKingWeb\Arsse\Test\DatabaseInformation; |
|||
|
|||
abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
protected static $dbInfo; |
|||
protected static $interface; |
|||
protected $resultClass; |
|||
protected $stringOutput; |
|||
|
|||
abstract protected function makeResult(string $q): array; |
|||
|
|||
public static function setUpBeforeClass() { |
|||
// establish a clean baseline |
|||
static::clearData(); |
|||
static::$dbInfo = new DatabaseInformation(static::$implementation); |
|||
static::setConf(); |
|||
static::$interface = (static::$dbInfo->interfaceConstructor)(); |
|||
} |
|||
|
|||
public function setUp() { |
|||
self::clearData(); |
|||
self::setConf(); |
|||
if (!static::$interface) { |
|||
$this->markTestSkipped(static::$implementation." database driver not available"); |
|||
} |
|||
// completely clear the database |
|||
(static::$dbInfo->razeFunction)(static::$interface); |
|||
$this->resultClass = static::$dbInfo->resultClass; |
|||
$this->stringOutput = static::$dbInfo->stringOutput; |
|||
} |
|||
|
|||
public function tearDown() { |
|||
self::clearData(); |
|||
} |
|||
|
|||
public static function tearDownAfterClass() { |
|||
if (static::$interface) { |
|||
// completely clear the database |
|||
(static::$dbInfo->razeFunction)(static::$interface); |
|||
} |
|||
static::$interface = null; |
|||
static::$dbInfo = null; |
|||
self::clearData(); |
|||
} |
|||
|
|||
public function testConstructResult() { |
|||
$this->assertInstanceOf(Result::class, new $this->resultClass(...$this->makeResult("SELECT 1"))); |
|||
} |
|||
|
|||
public function testGetChangeCountAndLastInsertId() { |
|||
$this->makeResult(static::$createMeta); |
|||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_meta(key,value) values('test', 1)")); |
|||
$this->assertSame(1, $r->changes()); |
|||
$this->assertSame(0, $r->lastId()); |
|||
} |
|||
|
|||
public function testGetChangeCountAndLastInsertIdBis() { |
|||
$this->makeResult(static::$createTest); |
|||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_test default values")); |
|||
$this->assertSame(1, $r->changes()); |
|||
$this->assertSame(1, $r->lastId()); |
|||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_test default values")); |
|||
$this->assertSame(1, $r->changes()); |
|||
$this->assertSame(2, $r->lastId()); |
|||
} |
|||
|
|||
public function testIterateOverResults() { |
|||
$exp = [0 => 1, 1 => 2, 2 => 3]; |
|||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp; |
|||
foreach (new $this->resultClass(...$this->makeResult("SELECT 1 as col union select 2 as col union select 3 as col")) as $index => $row) { |
|||
$rows[$index] = $row['col']; |
|||
} |
|||
$this->assertSame($exp, $rows); |
|||
} |
|||
|
|||
public function testIterateOverResultsTwice() { |
|||
$exp = [0 => 1, 1 => 2, 2 => 3]; |
|||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp; |
|||
$result = new $this->resultClass(...$this->makeResult("SELECT 1 as col union select 2 as col union select 3 as col")); |
|||
foreach ($result as $index => $row) { |
|||
$rows[$index] = $row['col']; |
|||
} |
|||
$this->assertSame($exp, $rows); |
|||
$this->assertException("resultReused", "Db"); |
|||
foreach ($result as $row) { |
|||
$rows[] = $row['col']; |
|||
} |
|||
} |
|||
|
|||
public function testGetSingleValues() { |
|||
$exp = [1867, 1970, 2112]; |
|||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp; |
|||
$test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year union all select 1970 as year union all select 2112 as year")); |
|||
$this->assertSame($exp[0], $test->getValue()); |
|||
$this->assertSame($exp[1], $test->getValue()); |
|||
$this->assertSame($exp[2], $test->getValue()); |
|||
$this->assertSame(null, $test->getValue()); |
|||
} |
|||
|
|||
public function testGetFirstValuesOnly() { |
|||
$exp = [1867, 1970, 2112]; |
|||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp; |
|||
$test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year, 19 as century union all select 1970 as year, 20 as century union all select 2112 as year, 22 as century")); |
|||
$this->assertSame($exp[0], $test->getValue()); |
|||
$this->assertSame($exp[1], $test->getValue()); |
|||
$this->assertSame($exp[2], $test->getValue()); |
|||
$this->assertSame(null, $test->getValue()); |
|||
} |
|||
|
|||
public function testGetRows() { |
|||
$exp = [ |
|||
['album' => '2112', 'track' => '2112'], |
|||
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], |
|||
]; |
|||
$test = new $this->resultClass(...$this->makeResult("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track")); |
|||
$this->assertSame($exp[0], $test->getRow()); |
|||
$this->assertSame($exp[1], $test->getRow()); |
|||
$this->assertSame(null, $test->getRow()); |
|||
} |
|||
|
|||
public function testGetAllRows() { |
|||
$exp = [ |
|||
['album' => '2112', 'track' => '2112'], |
|||
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'], |
|||
]; |
|||
$test = new $this->resultClass(...$this->makeResult("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track")); |
|||
$this->assertEquals($exp, $test->getAll()); |
|||
} |
|||
} |
@ -0,0 +1,333 @@ |
|||
<?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; |
|||
|
|||
use JKingWeb\Arsse\Db\Statement; |
|||
use JKingWeb\Arsse\Test\DatabaseInformation; |
|||
|
|||
abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
protected static $dbInfo; |
|||
protected static $interface; |
|||
protected $statementClass; |
|||
protected $stringOutput; |
|||
|
|||
abstract protected function makeStatement(string $q, array $types = []): array; |
|||
abstract protected function decorateTypeSyntax(string $value, string $type): string; |
|||
|
|||
public static function setUpBeforeClass() { |
|||
// establish a clean baseline |
|||
static::clearData(); |
|||
static::$dbInfo = new DatabaseInformation(static::$implementation); |
|||
static::setConf(); |
|||
static::$interface = (static::$dbInfo->interfaceConstructor)(); |
|||
} |
|||
|
|||
public function setUp() { |
|||
self::clearData(); |
|||
self::setConf(); |
|||
if (!static::$interface) { |
|||
$this->markTestSkipped(static::$implementation." database driver not available"); |
|||
} |
|||
// completely clear the database |
|||
(static::$dbInfo->razeFunction)(static::$interface); |
|||
$this->statementClass = static::$dbInfo->statementClass; |
|||
$this->stringOutput = static::$dbInfo->stringOutput; |
|||
} |
|||
|
|||
public function tearDown() { |
|||
self::clearData(); |
|||
} |
|||
|
|||
public static function tearDownAfterClass() { |
|||
if (static::$interface) { |
|||
// completely clear the database |
|||
(static::$dbInfo->razeFunction)(static::$interface); |
|||
} |
|||
static::$interface = null; |
|||
static::$dbInfo = null; |
|||
self::clearData(); |
|||
} |
|||
|
|||
public function testConstructStatement() { |
|||
$this->assertInstanceOf(Statement::class, new $this->statementClass(...$this->makeStatement("SELECT ? as value"))); |
|||
} |
|||
|
|||
/** @dataProvider provideBindings */ |
|||
public function testBindATypedValue($value, string $type, string $exp) { |
|||
if ($exp=="null") { |
|||
$query = "SELECT (? is null) as pass"; |
|||
} else { |
|||
$query = "SELECT ($exp = ?) as pass"; |
|||
} |
|||
$typeStr = "'".str_replace("'", "''", $type)."'"; |
|||
$s = new $this->statementClass(...$this->makeStatement($query)); |
|||
$s->retype(...[$type]); |
|||
$act = $s->run(...[$value])->getValue(); |
|||
$this->assertTrue((bool) $act); |
|||
} |
|||
|
|||
/** @dataProvider provideBinaryBindings */ |
|||
public function testHandleBinaryData($value, string $type, string $exp) { |
|||
if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) { |
|||
$this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown"); |
|||
} |
|||
if ($exp=="null") { |
|||
$query = "SELECT (? is null) as pass"; |
|||
} else { |
|||
$query = "SELECT ($exp = ?) as pass"; |
|||
} |
|||
$typeStr = "'".str_replace("'", "''", $type)."'"; |
|||
$s = new $this->statementClass(...$this->makeStatement($query)); |
|||
$s->retype(...[$type]); |
|||
$act = $s->run(...[$value])->getValue(); |
|||
$this->assertTrue((bool) $act); |
|||
} |
|||
|
|||
public function testBindMissingValue() { |
|||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", ["int"])); |
|||
$val = $s->runArray()->getRow()['value']; |
|||
$this->assertSame(null, $val); |
|||
} |
|||
|
|||
public function testBindMultipleValues() { |
|||
$exp = [ |
|||
'one' => 1, |
|||
'two' => 2, |
|||
]; |
|||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp; |
|||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two", ["int", "int"])); |
|||
$val = $s->runArray([1,2])->getRow(); |
|||
$this->assertSame($exp, $val); |
|||
} |
|||
|
|||
public function testBindRecursively() { |
|||
$exp = [ |
|||
'one' => 1, |
|||
'two' => 2, |
|||
'three' => 3, |
|||
'four' => 4, |
|||
]; |
|||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp; |
|||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two, ? as three, ? as four", ["int", ["int", "int"], "int"])); |
|||
$val = $s->runArray([1, [2, 3], 4])->getRow(); |
|||
$this->assertSame($exp, $val); |
|||
} |
|||
|
|||
public function testBindWithoutType() { |
|||
$this->assertException("paramTypeMissing", "Db"); |
|||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", [])); |
|||
$s->runArray([1]); |
|||
} |
|||
|
|||
public function testViolateConstraint() { |
|||
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run(); |
|||
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(key) values(?)", ["str"])); |
|||
$this->assertException("constraintViolation", "Db", "ExceptionInput"); |
|||
$s->runArray([null]); |
|||
} |
|||
|
|||
public function testMismatchTypes() { |
|||
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run(); |
|||
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"])); |
|||
$this->assertException("typeViolation", "Db", "ExceptionInput"); |
|||
$s->runArray(['ook', 'eek']); |
|||
} |
|||
|
|||
public function provideBindings() { |
|||
$dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto")); |
|||
$dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto")); |
|||
$dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC")); |
|||
$tests = [ |
|||
'Null as integer' => [null, "integer", "null"], |
|||
'Null as float' => [null, "float", "null"], |
|||
'Null as string' => [null, "string", "null"], |
|||
'Null as datetime' => [null, "datetime", "null"], |
|||
'Null as boolean' => [null, "boolean", "null"], |
|||
'Null as strict integer' => [null, "strict integer", "0"], |
|||
'Null as strict float' => [null, "strict float", "0.0"], |
|||
'Null as strict string' => [null, "strict string", "''"], |
|||
'Null as strict datetime' => [null, "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'Null as strict boolean' => [null, "strict boolean", "0"], |
|||
'True as integer' => [true, "integer", "1"], |
|||
'True as float' => [true, "float", "1.0"], |
|||
'True as string' => [true, "string", "'1'"], |
|||
'True as datetime' => [true, "datetime", "null"], |
|||
'True as boolean' => [true, "boolean", "1"], |
|||
'True as strict integer' => [true, "strict integer", "1"], |
|||
'True as strict float' => [true, "strict float", "1.0"], |
|||
'True as strict string' => [true, "strict string", "'1'"], |
|||
'True as strict datetime' => [true, "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'True as strict boolean' => [true, "strict boolean", "1"], |
|||
'False as integer' => [false, "integer", "0"], |
|||
'False as float' => [false, "float", "0.0"], |
|||
'False as string' => [false, "string", "''"], |
|||
'False as datetime' => [false, "datetime", "null"], |
|||
'False as boolean' => [false, "boolean", "0"], |
|||
'False as strict integer' => [false, "strict integer", "0"], |
|||
'False as strict float' => [false, "strict float", "0.0"], |
|||
'False as strict string' => [false, "strict string", "''"], |
|||
'False as strict datetime' => [false, "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'False as strict boolean' => [false, "strict boolean", "0"], |
|||
'Integer as integer' => [2112, "integer", "2112"], |
|||
'Integer as float' => [2112, "float", "2112.0"], |
|||
'Integer as string' => [2112, "string", "'2112'"], |
|||
'Integer as datetime' => [2112, "datetime", "'1970-01-01 00:35:12'"], |
|||
'Integer as boolean' => [2112, "boolean", "1"], |
|||
'Integer as strict integer' => [2112, "strict integer", "2112"], |
|||
'Integer as strict float' => [2112, "strict float", "2112.0"], |
|||
'Integer as strict string' => [2112, "strict string", "'2112'"], |
|||
'Integer as strict datetime' => [2112, "strict datetime", "'1970-01-01 00:35:12'"], |
|||
'Integer as strict boolean' => [2112, "strict boolean", "1"], |
|||
'Integer zero as integer' => [0, "integer", "0"], |
|||
'Integer zero as float' => [0, "float", "0.0"], |
|||
'Integer zero as string' => [0, "string", "'0'"], |
|||
'Integer zero as datetime' => [0, "datetime", "'1970-01-01 00:00:00'"], |
|||
'Integer zero as boolean' => [0, "boolean", "0"], |
|||
'Integer zero as strict integer' => [0, "strict integer", "0"], |
|||
'Integer zero as strict float' => [0, "strict float", "0.0"], |
|||
'Integer zero as strict string' => [0, "strict string", "'0'"], |
|||
'Integer zero as strict datetime' => [0, "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'Integer zero as strict boolean' => [0, "strict boolean", "0"], |
|||
'Float as integer' => [2112.5, "integer", "2112"], |
|||
'Float as float' => [2112.5, "float", "2112.5"], |
|||
'Float as string' => [2112.5, "string", "'2112.5'"], |
|||
'Float as datetime' => [2112.5, "datetime", "'1970-01-01 00:35:12'"], |
|||
'Float as boolean' => [2112.5, "boolean", "1"], |
|||
'Float as strict integer' => [2112.5, "strict integer", "2112"], |
|||
'Float as strict float' => [2112.5, "strict float", "2112.5"], |
|||
'Float as strict string' => [2112.5, "strict string", "'2112.5'"], |
|||
'Float as strict datetime' => [2112.5, "strict datetime", "'1970-01-01 00:35:12'"], |
|||
'Float as strict boolean' => [2112.5, "strict boolean", "1"], |
|||
'Float zero as integer' => [0.0, "integer", "0"], |
|||
'Float zero as float' => [0.0, "float", "0.0"], |
|||
'Float zero as string' => [0.0, "string", "'0'"], |
|||
'Float zero as datetime' => [0.0, "datetime", "'1970-01-01 00:00:00'"], |
|||
'Float zero as boolean' => [0.0, "boolean", "0"], |
|||
'Float zero as strict integer' => [0.0, "strict integer", "0"], |
|||
'Float zero as strict float' => [0.0, "strict float", "0.0"], |
|||
'Float zero as strict string' => [0.0, "strict string", "'0'"], |
|||
'Float zero as strict datetime' => [0.0, "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'Float zero as strict boolean' => [0.0, "strict boolean", "0"], |
|||
'ASCII string as integer' => ["Random string", "integer", "0"], |
|||
'ASCII string as float' => ["Random string", "float", "0.0"], |
|||
'ASCII string as string' => ["Random string", "string", "'Random string'"], |
|||
'ASCII string as datetime' => ["Random string", "datetime", "null"], |
|||
'ASCII string as boolean' => ["Random string", "boolean", "1"], |
|||
'ASCII string as strict integer' => ["Random string", "strict integer", "0"], |
|||
'ASCII string as strict float' => ["Random string", "strict float", "0.0"], |
|||
'ASCII string as strict string' => ["Random string", "strict string", "'Random string'"], |
|||
'ASCII string as strict datetime' => ["Random string", "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"], |
|||
'UTF-8 string as integer' => ["\u{e9}", "integer", "0"], |
|||
'UTF-8 string as float' => ["\u{e9}", "float", "0.0"], |
|||
'UTF-8 string as string' => ["\u{e9}", "string", "char(233)"], |
|||
'UTF-8 string as datetime' => ["\u{e9}", "datetime", "null"], |
|||
'UTF-8 string as boolean' => ["\u{e9}", "boolean", "1"], |
|||
'UTF-8 string as strict integer' => ["\u{e9}", "strict integer", "0"], |
|||
'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.0"], |
|||
'UTF-8 string as strict string' => ["\u{e9}", "strict string", "char(233)"], |
|||
'UTF-8 string as strict datetime' => ["\u{e9}", "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'UTF-8 string as strict boolean' => ["\u{e9}", "strict boolean", "1"], |
|||
'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"], |
|||
'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0"], |
|||
'ISO 8601 string as string' => ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"], |
|||
'ISO 8601 string as datetime' => ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"], |
|||
'ISO 8601 string as boolean' => ["2017-01-09T13:11:17", "boolean", "1"], |
|||
'ISO 8601 string as strict integer' => ["2017-01-09T13:11:17", "strict integer", "0"], |
|||
'ISO 8601 string as strict float' => ["2017-01-09T13:11:17", "strict float", "0.0"], |
|||
'ISO 8601 string as strict string' => ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"], |
|||
'ISO 8601 string as strict datetime' => ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"], |
|||
'ISO 8601 string as strict boolean' => ["2017-01-09T13:11:17", "strict boolean", "1"], |
|||
'Arbitrary date string as integer' => ["Today", "integer", "0"], |
|||
'Arbitrary date string as float' => ["Today", "float", "0.0"], |
|||
'Arbitrary date string as string' => ["Today", "string", "'Today'"], |
|||
'Arbitrary date string as datetime' => ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], |
|||
'Arbitrary date string as boolean' => ["Today", "boolean", "1"], |
|||
'Arbitrary date string as strict integer' => ["Today", "strict integer", "0"], |
|||
'Arbitrary date string as strict float' => ["Today", "strict float", "0.0"], |
|||
'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"], |
|||
'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"], |
|||
'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"], |
|||
'DateTime as integer' => [$dateMutable, "integer", (string) $dateUTC->getTimestamp()], |
|||
'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"], |
|||
'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTime as boolean' => [$dateMutable, "boolean", "1"], |
|||
'DateTime as strict integer' => [$dateMutable, "strict integer", (string) $dateUTC->getTimestamp()], |
|||
'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"], |
|||
'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"], |
|||
'DateTimeImmutable as integer' => [$dateImmutable, "integer", (string) $dateUTC->getTimestamp()], |
|||
'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"], |
|||
'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"], |
|||
'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", (string) $dateUTC->getTimestamp()], |
|||
'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"], |
|||
'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"], |
|||
'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"], |
|||
]; |
|||
foreach ($tests as $index => list($value, $type, $exp)) { |
|||
$t = preg_replace("<^strict >", "", $type); |
|||
$exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); |
|||
yield $index => [$value, $type, $exp]; |
|||
} |
|||
} |
|||
|
|||
public function provideBinaryBindings() { |
|||
$dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto")); |
|||
$dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto")); |
|||
$dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC")); |
|||
$tests = [ |
|||
'Null as binary' => [null, "binary", "null"], |
|||
'Null as strict binary' => [null, "strict binary", "x''"], |
|||
'True as binary' => [true, "binary", "x'31'"], |
|||
'True as strict binary' => [true, "strict binary", "x'31'"], |
|||
'False as binary' => [false, "binary", "x''"], |
|||
'False as strict binary' => [false, "strict binary", "x''"], |
|||
'Integer as binary' => [2112, "binary", "x'32313132'"], |
|||
'Integer as strict binary' => [2112, "strict binary", "x'32313132'"], |
|||
'Integer zero as binary' => [0, "binary", "x'30'"], |
|||
'Integer zero as strict binary' => [0, "strict binary", "x'30'"], |
|||
'Float as binary' => [2112.5, "binary", "x'323131322e35'"], |
|||
'Float as strict binary' => [2112.5, "strict binary", "x'323131322e35'"], |
|||
'Float zero as binary' => [0.0, "binary", "x'30'"], |
|||
'Float zero as strict binary' => [0.0, "strict binary", "x'30'"], |
|||
'ASCII string as binary' => ["Random string", "binary", "x'52616e646f6d20737472696e67'"], |
|||
'ASCII string as strict binary' => ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"], |
|||
'UTF-8 string as binary' => ["\u{e9}", "binary", "x'c3a9'"], |
|||
'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"], |
|||
'Binary string as integer' => [chr(233).chr(233), "integer", "0"], |
|||
'Binary string as float' => [chr(233).chr(233), "float", "0.0"], |
|||
'Binary string as string' => [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"], |
|||
'Binary string as binary' => [chr(233).chr(233), "binary", "x'e9e9'"], |
|||
'Binary string as datetime' => [chr(233).chr(233), "datetime", "null"], |
|||
'Binary string as boolean' => [chr(233).chr(233), "boolean", "1"], |
|||
'Binary string as strict integer' => [chr(233).chr(233), "strict integer", "0"], |
|||
'Binary string as strict float' => [chr(233).chr(233), "strict float", "0.0"], |
|||
'Binary string as strict string' => [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"], |
|||
'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"], |
|||
'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"], |
|||
'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"], |
|||
'ISO 8601 string as binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"], |
|||
'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"], |
|||
'Arbitrary date string as binary' => ["Today", "binary", "x'546f646179'"], |
|||
'Arbitrary date string as strict binary' => ["Today", "strict binary", "x'546f646179'"], |
|||
'DateTime as binary' => [$dateMutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], |
|||
'DateTime as strict binary' => [$dateMutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], |
|||
'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], |
|||
'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"], |
|||
]; |
|||
foreach ($tests as $index => list($value, $type, $exp)) { |
|||
$t = preg_replace("<^strict >", "", $type); |
|||
$exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t); |
|||
yield $index => [$value, $type, $exp]; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,136 @@ |
|||
<?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; |
|||
|
|||
use JKingWeb\Arsse\Arsse; |
|||
use JKingWeb\Arsse\Database; |
|||
use JKingWeb\Arsse\Db\Exception; |
|||
use JKingWeb\Arsse\Test\DatabaseInformation; |
|||
use org\bovigo\vfs\vfsStream; |
|||
|
|||
class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
protected static $dbInfo; |
|||
protected static $interface; |
|||
protected $drv; |
|||
protected $vfs; |
|||
protected $base; |
|||
protected $path; |
|||
|
|||
public static function setUpBeforeClass() { |
|||
// establish a clean baseline |
|||
static::clearData(); |
|||
static::$dbInfo = new DatabaseInformation(static::$implementation); |
|||
static::setConf(); |
|||
static::$interface = (static::$dbInfo->interfaceConstructor)(); |
|||
} |
|||
|
|||
public function setUp() { |
|||
if (!static::$interface) { |
|||
$this->markTestSkipped(static::$implementation." database driver not available"); |
|||
} |
|||
self::clearData(); |
|||
self::setConf(); |
|||
// construct a fresh driver for each test |
|||
$this->drv = new static::$dbInfo->driverClass; |
|||
$schemaId = (get_class($this->drv))::schemaID(); |
|||
// set up a virtual filesystem for schema files |
|||
$this->vfs = vfsStream::setup("schemata", null, [$schemaId => []]); |
|||
$this->base = $this->vfs->url(); |
|||
$this->path = $this->base."/$schemaId/"; |
|||
// completely clear the database |
|||
(static::$dbInfo->razeFunction)(static::$interface); |
|||
} |
|||
|
|||
public function tearDown() { |
|||
// deconstruct the driver |
|||
unset($this->drv); |
|||
unset($this->path, $this->base, $this->vfs); |
|||
self::clearData(); |
|||
} |
|||
|
|||
public static function tearDownAfterClass() { |
|||
if (static::$interface) { |
|||
// completely clear the database |
|||
(static::$dbInfo->razeFunction)(static::$interface); |
|||
} |
|||
static::$interface = null; |
|||
static::$dbInfo = null; |
|||
self::clearData(); |
|||
} |
|||
|
|||
public function testLoadMissingFile() { |
|||
$this->assertException("updateFileMissing", "Db"); |
|||
$this->drv->schemaUpdate(1, $this->base); |
|||
} |
|||
|
|||
public function testLoadUnreadableFile() { |
|||
touch($this->path."0.sql"); |
|||
chmod($this->path."0.sql", 0000); |
|||
$this->assertException("updateFileUnreadable", "Db"); |
|||
$this->drv->schemaUpdate(1, $this->base); |
|||
} |
|||
|
|||
public function testLoadCorruptFile() { |
|||
file_put_contents($this->path."0.sql", "This is a corrupt file"); |
|||
$this->assertException("updateFileError", "Db"); |
|||
$this->drv->schemaUpdate(1, $this->base); |
|||
} |
|||
|
|||
public function testLoadIncompleteFile() { |
|||
file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);"); |
|||
$this->assertException("updateFileIncomplete", "Db"); |
|||
$this->drv->schemaUpdate(1, $this->base); |
|||
} |
|||
|
|||
public function testLoadEmptyFile() { |
|||
file_put_contents($this->path."0.sql", ""); |
|||
$this->assertException("updateFileIncomplete", "Db"); |
|||
$this->drv->schemaUpdate(1, $this->base); |
|||
} |
|||
|
|||
public function testLoadCorrectFile() { |
|||
file_put_contents($this->path."0.sql", static::$minimal1); |
|||
$this->drv->schemaUpdate(1, $this->base); |
|||
$this->assertEquals(1, $this->drv->schemaVersion()); |
|||
} |
|||
|
|||
public function testPerformPartialUpdate() { |
|||
file_put_contents($this->path."0.sql", static::$minimal1); |
|||
file_put_contents($this->path."1.sql", "UPDATE arsse_meta set value = '1' where key = 'schema_version'"); |
|||
$this->assertException("updateFileIncomplete", "Db"); |
|||
try { |
|||
$this->drv->schemaUpdate(2, $this->base); |
|||
} catch (Exception $e) { |
|||
$this->assertEquals(1, $this->drv->schemaVersion()); |
|||
throw $e; |
|||
} |
|||
} |
|||
|
|||
public function testPerformSequentialUpdate() { |
|||
file_put_contents($this->path."0.sql", static::$minimal1); |
|||
file_put_contents($this->path."1.sql", static::$minimal2); |
|||
$this->drv->schemaUpdate(2, $this->base); |
|||
$this->assertEquals(2, $this->drv->schemaVersion()); |
|||
} |
|||
|
|||
public function testPerformActualUpdate() { |
|||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION); |
|||
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion()); |
|||
} |
|||
|
|||
public function testDeclineManualUpdate() { |
|||
// turn auto-updating off |
|||
Arsse::$conf->dbAutoUpdate = false; |
|||
$this->assertException("updateManual", "Db"); |
|||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION); |
|||
} |
|||
|
|||
public function testDeclineDowngrade() { |
|||
$this->assertException("updateTooNew", "Db"); |
|||
$this->drv->schemaUpdate(-1, $this->base); |
|||
} |
|||
} |
@ -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,58 @@ |
|||
<?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> |
|||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<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,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\TestCase\Db\PostgreSQL; |
|||
|
|||
/** |
|||
* @group slow |
|||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Statement<extended> |
|||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<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';"; |
|||
} |
@ -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\PostgreSQLPDO; |
|||
|
|||
use JKingWeb\Arsse\Arsse; |
|||
use JKingWeb\Arsse\Db\PostgreSQL\PDODriver as Driver; |
|||
|
|||
/** |
|||
* @group slow |
|||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended> */ |
|||
class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
public function setUp() { |
|||
if (!Driver::requirementsMet()) { |
|||
$this->markTestSkipped("PDO-PostgreSQL extension not loaded"); |
|||
} |
|||
} |
|||
|
|||
/** @dataProvider provideConnectionStrings */ |
|||
public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) { |
|||
self::setConf(); |
|||
$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() { |
|||
// PDO dies not distinguish between different connection failure modes |
|||
self::setConf([ |
|||
'dbPostgreSQLPass' => (string) rand(), |
|||
]); |
|||
$this->assertException("connectionFailure", "Db"); |
|||
new Driver; |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
<?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\PostgreSQLPDO; |
|||
|
|||
/** |
|||
* @group slow |
|||
* @group optional |
|||
* @group coverageOptional |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query<extended> |
|||
*/ |
|||
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { |
|||
protected static $implementation = "PDO 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,27 @@ |
|||
<?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\PostgreSQLPDO; |
|||
|
|||
/** |
|||
* @group slow |
|||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended> |
|||
* @covers \JKingWeb\Arsse\Db\PDODriver |
|||
* @covers \JKingWeb\Arsse\Db\PDOError */ |
|||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver { |
|||
protected static $implementation = "PDO 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(); |
|||
} |
|||
} |
@ -0,0 +1,24 @@ |
|||
<?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\PostgreSQLPDO; |
|||
|
|||
use JKingWeb\Arsse\Test\DatabaseInformation; |
|||
|
|||
/** |
|||
* @group slow |
|||
* @covers \JKingWeb\Arsse\Db\PDOResult<extended> |
|||
*/ |
|||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { |
|||
protected static $implementation = "PDO 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 = static::$interface->query($q); |
|||
return [static::$interface, $set]; |
|||
} |
|||
} |
@ -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\PostgreSQLPDO; |
|||
|
|||
/** |
|||
* @group slow |
|||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDOStatement<extended> |
|||
* @covers \JKingWeb\Arsse\Db\PDOError */ |
|||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { |
|||
protected static $implementation = "PDO 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; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,17 @@ |
|||
<?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\PostgreSQLPDO; |
|||
|
|||
/** |
|||
* @group slow |
|||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended> |
|||
* @covers \JKingWeb\Arsse\Db\PDOError */ |
|||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate { |
|||
protected static $implementation = "PDO 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';"; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestArticle extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesArticle; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestCleanup extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesCleanup; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesFeed; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestFolder extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesFolder; |
|||
} |
@ -1,13 +0,0 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestLabel extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesLabel; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestMeta extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesMeta; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestMiscellany extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesMiscellany; |
|||
} |
@ -1,13 +0,0 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestSession extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesSession; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestSubscription extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesSubscription; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesUser; |
|||
} |
@ -0,0 +1,20 @@ |
|||
<?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\SQLite3; |
|||
|
|||
/** |
|||
* @group optional |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query<extended> |
|||
*/ |
|||
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { |
|||
protected static $implementation = "SQLite 3"; |
|||
|
|||
protected function nextID(string $table): int { |
|||
return static::$drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue(); |
|||
} |
|||
} |
@ -0,0 +1,31 @@ |
|||
<?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\SQLite3; |
|||
|
|||
use JKingWeb\Arsse\Test\DatabaseInformation; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Db\SQLite3\Result<extended> |
|||
*/ |
|||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult { |
|||
protected static $implementation = "SQLite 3"; |
|||
protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text) without rowid"; |
|||
protected static $createTest = "CREATE TABLE arsse_test(id integer primary key)"; |
|||
|
|||
public static function tearDownAfterClass() { |
|||
static::$interface->close(); |
|||
static::$interface = null; |
|||
parent::tearDownAfterClass(); |
|||
} |
|||
|
|||
protected function makeResult(string $q): array { |
|||
$set = static::$interface->query($q); |
|||
$rows = static::$interface->changes(); |
|||
$id = static::$interface->lastInsertRowID(); |
|||
return [$set, [$rows, $id]]; |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
<?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\SQLite3; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Db\SQLite3\Statement<extended> |
|||
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */ |
|||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement { |
|||
protected static $implementation = "SQLite 3"; |
|||
|
|||
public static function tearDownAfterClass() { |
|||
static::$interface->close(); |
|||
static::$interface = null; |
|||
parent::tearDownAfterClass(); |
|||
} |
|||
|
|||
protected function makeStatement(string $q, array $types = []): array { |
|||
return [static::$interface, static::$interface->prepare($q), $types]; |
|||
} |
|||
|
|||
protected function decorateTypeSyntax(string $value, string $type): string { |
|||
return $value; |
|||
} |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestArticle extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesArticle; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestCleanup extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesCleanup; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesFeed; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestFolder extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesFolder; |
|||
} |
@ -1,13 +0,0 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestLabel extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesLabel; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestMeta extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesMeta; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestMiscellany extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesMiscellany; |
|||
} |
@ -1,13 +0,0 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestSession extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesSession; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestSubscription extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesSubscription; |
|||
} |
@ -1,17 +0,0 @@ |
|||
<?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\SQLite3PDO\Database; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query |
|||
*/ |
|||
class TestUser extends \JKingWeb\Arsse\Test\AbstractTest { |
|||
use \JKingWeb\Arsse\Test\Database\Setup; |
|||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO; |
|||
use \JKingWeb\Arsse\Test\Database\SeriesUser; |
|||
} |
@ -0,0 +1,19 @@ |
|||
<?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\SQLite3PDO; |
|||
|
|||
/** |
|||
* @covers \JKingWeb\Arsse\Database<extended> |
|||
* @covers \JKingWeb\Arsse\Misc\Query<extended> |
|||
*/ |
|||
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base { |
|||
protected static $implementation = "PDO SQLite 3"; |
|||
|
|||
protected function nextID(string $table): int { |
|||
return (int) static::$drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue(); |
|||
} |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue