Browse Source

Nominally complete PostgreSQL driver

Connection error handling as well as uprade error handling still need
to be implemented.
microsub
J. King 5 years ago
parent
commit
c0c4810662
  1. 6
      lib/Db/AbstractDriver.php
  2. 79
      lib/Db/PostgreSQL/Driver.php
  3. 123
      sql/PostgreSQL/0.sql
  4. 36
      sql/PostgreSQL/1.sql
  5. 22
      sql/PostgreSQL/2.sql
  6. 8
      tests/cases/Db/TestStatement.php

6
lib/Db/AbstractDriver.php

@ -13,13 +13,13 @@ abstract class AbstractDriver implements Driver {
protected $transDepth = 0;
protected $transStatus = [];
abstract protected function lock(): bool;
abstract protected function unlock(bool $rollback = false): bool;
abstract protected function getError(): string;
/** @codeCoverageIgnore */
public function schemaVersion(): int {
// FIXME: generic schemaVersion() will need to be covered for database engines other than SQLite
try {
return (int) $this->query("SELECT value from arsse_meta where key is schema_version")->getValue();
return (int) $this->query("SELECT value from arsse_meta where key = 'schema_version'")->getValue();
} catch (Exception $e) {
return 0;
}

79
lib/Db/PostgreSQL/Driver.php

@ -13,11 +13,10 @@ use JKingWeb\Arsse\Db\ExceptionInput;
use JKingWeb\Arsse\Db\ExceptionTimeout;
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
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", self::driverName()); // @codeCoverageIgnore
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
}
$user = $user ?? Arsse::$conf->dbPostgreSQLUser;
$pass = $pass ?? Arsse::$conf->dbPostgreSQLPass;
@ -27,16 +26,9 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
$schema = $schema ?? Arsse::$conf->dbPostgreSQLSchema;
$service = $service ?? Arsse::$conf->dbPostgreSQLService;
$this->makeConnection($user, $pass, $db, $host, $port, $service);
}
public static function requirementsMet(): bool {
// stub: native interface is not yet supported
return false;
}
protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) {
// stub: native interface is not yet supported
throw new \Exception;
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 {
@ -73,7 +65,15 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
return implode(" ", $out);
}
public function __destruct() {
public static function makeSetupQueries(string $schema = ""): array {
$out = [
"SET TIME ZONE UTC",
"SET DateStyle = 'ISO, MDY'"
];
if (strlen($schema) > 0) {
$out[] = 'SET search_path = \'"'.str_replace('"', '""', $schema).'", "$user", public\'';
}
return $out;
}
/** @codeCoverageIgnore */
@ -96,48 +96,57 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
return "PostgreSQL";
}
public function schemaVersion(): int {
// stub
return 0;
public function charsetAcceptable(): bool {
return $this->query("SELECT pg_encoding_to_char(encoding) from pg_database where datname = current_database()")->getValue() == "UTF8";
}
public function schemaUpdate(int $to, string $basePath = null): bool {
// stub
return false;
protected function lock(): bool {
$this->exec("BEGIN TRANSACTION");
if ($this->schemaVersion()) {
$this->exec("LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT");
}
return true;
}
public function charsetAcceptable(): bool {
// stub
protected function unlock(bool $rollback = false): bool {
$this->exec((!$rollback) ? "COMMIT" : "ROLLBACK");
return true;
}
public function __destruct() {
}
public static function requirementsMet(): bool {
// stub: native interface is not yet supported
return false;
}
protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) {
// stub: native interface is not yet supported
throw new \Exception;
}
/** @codeCoverageIgnore */
protected function getError(): string {
// stub
// stub: native interface is not yet supported
return "";
}
/** @codeCoverageIgnore */
public function exec(string $query): bool {
// stub
// stub: native interface is not yet supported
return true;
}
/** @codeCoverageIgnore */
public function query(string $query): \JKingWeb\Arsse\Db\Result {
// stub
// stub: native interface is not yet supported
return new ResultEmpty;
}
/** @codeCoverageIgnore */
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
// stub
// stub: native interface is not yet supported
return new Statement($this->db, $s, $paramTypes);
}
protected function lock(): bool {
// stub
return true;
}
protected function unlock(bool $rollback = false): bool {
// stub
return true;
}
}

123
sql/PostgreSQL/0.sql

@ -0,0 +1,123 @@
-- SPDX-License-Identifier: MIT
-- Copyright 2017 J. King, Dustin Wilson et al.
-- See LICENSE and AUTHORS files for details
-- metadata
create table arsse_meta(
key text primary key,
value text
);
-- users
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
);
-- extra user metadata
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)
);
-- NextCloud News folders and TT-RSS categories
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) with time zone not null default CURRENT_TIMESTAMP, --
unique(owner,name,parent)
);
-- newsfeeds, deduplicated
create table arsse_feeds(
id bigserial primary key,
url text not null,
title text,
favicon text,
source text,
updated timestamp(0) with time zone,
modified timestamp(0) with time zone,
next_fetch timestamp(0) with time zone,
orphaned timestamp(0) with 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)
);
-- users' subscriptions to newsfeeds, with settings
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) with time zone not null default CURRENT_TIMESTAMP,
modified timestamp(0) with 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)
);
-- entries in newsfeeds
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) with time zone,
edited timestamp(0) with time zone,
modified timestamp(0) with 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
);
-- enclosures associated with articles
create table arsse_enclosures(
article bigint not null references arsse_articles(id) on delete cascade,
url text,
type text
);
-- users' actions on newsfeed entries
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) with time zone not null default CURRENT_TIMESTAMP,
primary key(article,subscription)
);
-- IDs for specific editions of articles (required for at least NextCloud News)
create table arsse_editions(
id bigserial primary key,
article bigint not null references arsse_articles(id) on delete cascade,
modified timestamp(0) with time zone not null default CURRENT_TIMESTAMP
);
-- author categories associated with newsfeed entries
create table arsse_categories(
article bigint not null references arsse_articles(id) on delete cascade,
name text
);
-- set version marker
insert into arsse_meta(key,value) values('schema_version','1');

36
sql/PostgreSQL/1.sql

@ -0,0 +1,36 @@
-- SPDX-License-Identifier: MIT
-- Copyright 2017 J. King, Dustin Wilson et al.
-- See LICENSE and AUTHORS files for details
-- Sessions for Tiny Tiny RSS (and possibly others)
create table arsse_sessions (
id text primary key,
created timestamp(0) with time zone not null default CURRENT_TIMESTAMP,
expires timestamp(0) with time zone not null,
user text not null references arsse_users(id) on delete cascade on update cascade
);
-- User-defined article labels for Tiny Tiny RSS
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) with time zone not null default CURRENT_TIMESTAMP,
unique(owner,name)
);
-- Labels assignments for articles
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) with time zone not null default CURRENT_TIMESTAMP,
primary key(label,article)
);
-- alter marks table to add Tiny Tiny RSS' notes
alter table arsse_marks add column note text not null default '';
-- set version marker
update arsse_meta set value = '2' where key = 'schema_version';

22
sql/PostgreSQL/2.sql

@ -0,0 +1,22 @@
-- SPDX-License-Identifier: MIT
-- Copyright 2017 J. King, Dustin Wilson et al.
-- See LICENSE and AUTHORS files for details
-- create a case-insensitive generic collation sequence
create collation nocase(
provider = icu,
locale = '@kf=false'
);
-- Correct collation sequences
alter table arsse_users alter column id type text collate nocase;
alter table arsse_folders alter column name type text collate nocase;
alter table arsse_feeds alter column title type text collate nocase;
alter table arsse_subscriptions alter column title type text collate nocase;
alter table arsse_articles alter column title type text collate nocase;
alter table arsse_articles alter column author type text collate nocase;
alter table arsse_categories alter column name type text collate nocase;
alter table arsse_labels alter column name type text collate nocase;
-- set version marker
update arsse_meta set value = '3' where key = 'schema_version';

8
tests/cases/Db/TestStatement.php

@ -28,7 +28,11 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest {
$drvPgsql = (function() {
if (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::requirementsMet()) {
$connString = \JKingWeb\Arsse\Db\PostgreSQL\Driver::makeConnectionString(true, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, Arsse::$conf->dbPostgreSQLDb, Arsse::$conf->dbPostgreSQLHost, Arsse::$conf->dbPostgreSQLPort, "");
return new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
$c = new \PDO("pgsql:".$connString, Arsse::$conf->dbPostgreSQLUser, Arsse::$conf->dbPostgreSQLPass, [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
foreach (\JKingWeb\Arsse\Db\PostgreSQL\PDODriver::makeSetupQueries(Arsse::$conf->dbPostgreSQLSchema) as $q) {
$c->exec($q);
}
return $c;
}
})();
$drvPdo = (function() {
@ -173,7 +177,6 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest {
$dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto"));
$dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC"));
$tests = [
/* input, type, expected binding as SQL fragment */
'Null as integer' => [null, "integer", "null"],
'Null as float' => [null, "float", "null"],
'Null as string' => [null, "string", "null"],
@ -321,7 +324,6 @@ class TestStatement extends \JKingWeb\Arsse\Test\AbstractTest {
$dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto"));
$dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC"));
$tests = [
/* input, type, expected binding as SQL fragment */
'Null as binary' => [null, "binary", "null"],
'Null as strict binary' => [null, "strict binary", "x''"],
'True as binary' => [true, "binary", "x'31'"],

Loading…
Cancel
Save