J. King
8 years ago
19 changed files with 379 additions and 111 deletions
@ -1,109 +1,111 @@ |
|||
begin; |
|||
|
|||
create table main.newssync_settings( |
|||
key varchar(255) primary key not null, -- |
|||
value varchar(255), -- |
|||
type varchar(255) not null check( |
|||
type in('numeric','text','timestamp', 'date', 'time', 'bool') |
|||
) -- |
|||
); |
|||
insert into main.newssync_settings values('schema_version',1,'int'); |
|||
|
|||
-- users |
|||
create table newssync_users( |
|||
id TEXT primary key not null, -- user id |
|||
password TEXT, -- password, salted and hashed; if using external authentication this would be blank |
|||
name TEXT, -- display name |
|||
avatar_type TEXT, -- avatar image's MIME content type |
|||
avatar_data BLOB, -- avatar image's binary data |
|||
admin boolean not null default 0 -- whether the user is an administrator |
|||
create table main.newssync_users( |
|||
id TEXT primary key not null, -- user id |
|||
password TEXT, -- password, salted and hashed; if using external authentication this would be blank |
|||
name TEXT, -- display name |
|||
avatar_type TEXT, -- avatar image's MIME content type |
|||
avatar_data BLOB, -- avatar image's binary data |
|||
admin boolean not null default 0 -- whether the user is an administrator |
|||
); |
|||
|
|||
-- TT-RSS categories and ownCloud folders |
|||
create table newssync_categories( |
|||
id integer primary key not null, -- sequence number |
|||
owner TEXT references users(id) on delete cascade on update cascade, -- owner of category |
|||
parent integer, -- parent category id |
|||
folder integer not null, -- first-level category (ownCloud folder) |
|||
name TEXT not null, -- category name |
|||
modified datetime not null default CURRENT_TIMESTAMP, -- |
|||
unique(owner,name,parent) -- cannot have multiple categories with the same name under the same parent for the same owner |
|||
create table main.newssync_categories( |
|||
id integer primary key not null, -- sequence number |
|||
owner TEXT not null references users(id) on delete cascade on update cascade, -- owner of category |
|||
parent integer, -- parent category id |
|||
folder integer not null, -- first-level category (ownCloud folder) |
|||
name TEXT not null, -- category name |
|||
modified datetime not null default CURRENT_TIMESTAMP, -- |
|||
unique(owner,name,parent) -- cannot have multiple categories with the same name under the same parent for the same owner |
|||
); |
|||
|
|||
-- newsfeeds, deduplicated |
|||
create table newssync_feeds( |
|||
id integer primary key not null, -- sequence number |
|||
url TEXT not null, -- URL of feed |
|||
title TEXT, -- default title of feed |
|||
favicon TEXT, -- URL of favicon |
|||
source TEXT, -- URL of site to which the feed belongs |
|||
updated datetime, -- time at which the feed was last fetched |
|||
modified datetime not null default CURRENT_TIMESTAMP, -- |
|||
err_count integer not null default 0, -- count of successive times update resulted in error since last successful update |
|||
err_msg TEXT, -- last error message |
|||
username TEXT, -- HTTP authentication username |
|||
password TEXT, -- HTTP authentication password (this is stored in plain text) |
|||
unique(url,username,password) -- a URL with particular credentials should only appear once |
|||
create table feeds.newssync_feeds( |
|||
id integer primary key not null, -- sequence number |
|||
url TEXT not null, -- URL of feed |
|||
title TEXT, -- default title of feed |
|||
favicon TEXT, -- URL of favicon |
|||
source TEXT, -- URL of site to which the feed belongs |
|||
updated datetime, -- time at which the feed was last fetched |
|||
modified datetime not null default CURRENT_TIMESTAMP, -- |
|||
err_count integer not null default 0, -- count of successive times update resulted in error since last successful update |
|||
err_msg TEXT, -- last error message |
|||
username TEXT, -- HTTP authentication username |
|||
password TEXT, -- HTTP authentication password (this is stored in plain text) |
|||
unique(url,username,password) -- a URL with particular credentials should only appear once |
|||
); |
|||
|
|||
-- users' subscriptions to newsfeeds, with settings |
|||
create table newssync_subscriptions( |
|||
create table main.newssync_subscriptions( |
|||
id integer primary key not null, -- sequence number |
|||
owner TEXT references users(id) on delete cascade on update cascade, -- owner of subscription |
|||
feed integer references feeds(id) on delete cascade, -- feed for the subscription |
|||
owner TEXT not null references users(id) on delete cascade on update cascade, -- owner of subscription |
|||
feed integer not null references feeds(id) on delete cascade, -- feed for the subscription |
|||
added datetime not null default CURRENT_TIMESTAMP, -- time at which feed was added |
|||
modified datetime not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified |
|||
title TEXT, -- user-supplied title |
|||
order_type int not null default 0, -- ownCloud sort order |
|||
pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) |
|||
category integer references categories(id) on delete set null, -- TT-RSS category (nestable); the first-level category (which acts as ownCloud folder) is joined in when needed |
|||
category integer not null references categories(id) on delete set null, -- TT-RSS category (nestable); the first-level category (which acts as ownCloud folder) is joined in when needed |
|||
unique(owner,feed) -- a given feed should only appear once for a given owner |
|||
); |
|||
|
|||
-- entries in newsfeeds |
|||
create table newssync_articles( |
|||
id integer primary key not null, -- sequence number |
|||
feed integer references feeds(id) on delete cascade, -- feed for the subscription |
|||
url TEXT not null, -- URL of article |
|||
title TEXT, -- article title |
|||
author TEXT, -- author's name |
|||
published datetime, -- time of original publication |
|||
edited datetime, -- time of last edit |
|||
guid TEXT, -- GUID |
|||
content TEXT, -- content, as (X)HTML |
|||
modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified |
|||
hash varchar(64) not null, -- ownCloud hash |
|||
fingerprint varchar(64) not null, -- ownCloud fingerprint |
|||
enclosures_hash varchar(64), -- hash of enclosures, if any; since enclosures are not uniquely identified, we need to know when they change |
|||
tags_hash varchar(64) -- hash of RSS/Atom categories included in article; since these categories are not uniquely identified, we need to know when they change |
|||
create table feeds.newssync_articles( |
|||
id integer primary key not null, -- sequence number |
|||
feed integer not null references feeds(id) on delete cascade, -- feed for the subscription |
|||
url TEXT not null, -- URL of article |
|||
title TEXT, -- article title |
|||
author TEXT, -- author's name |
|||
published datetime, -- time of original publication |
|||
edited datetime, -- time of last edit |
|||
guid TEXT, -- GUID |
|||
content TEXT, -- content, as (X)HTML |
|||
modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified |
|||
hash varchar(64) not null, -- ownCloud hash |
|||
fingerprint varchar(64) not null, -- ownCloud fingerprint |
|||
enclosures_hash varchar(64), -- hash of enclosures, if any; since enclosures are not uniquely identified, we need to know when they change |
|||
tags_hash varchar(64) -- hash of RSS/Atom categories included in article; since these categories are not uniquely identified, we need to know when they change |
|||
); |
|||
|
|||
-- users' actions on newsfeed entries |
|||
create table newssync_subscription_articles( |
|||
create table main.newssync_subscription_articles( |
|||
id integer primary key not null, |
|||
article integer references articles(id) on delete cascade, |
|||
article integer not null references articles(id) on delete cascade, |
|||
read boolean not null default 0, |
|||
starred boolean not null default 0, |
|||
modified datetime not null default CURRENT_TIMESTAMP |
|||
); |
|||
|
|||
-- enclosures associated with articles |
|||
create table newssync_enclosures( |
|||
article integer references articles(id) on delete cascade, |
|||
create table main.newssync_enclosures( |
|||
article integer not null references articles(id) on delete cascade, |
|||
url TEXT, |
|||
type varchar(255) |
|||
); |
|||
|
|||
-- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries |
|||
create table newssync_tags( |
|||
article integer references articles(id) on delete cascade, |
|||
create table main.newssync_tags( |
|||
article integer not null references articles(id) on delete cascade, |
|||
name TEXT |
|||
); |
|||
|
|||
-- user labels associated with newsfeed entries |
|||
create table newssync_labels( |
|||
sub_article integer references subscription_articles(id) on delete cascade, |
|||
owner TEXT references users(id) on delete cascade on update cascade, |
|||
create table main.newssync_labels( |
|||
sub_article integer not null references subscription_articles(id) on delete cascade, |
|||
owner TEXT not null references users(id) on delete cascade on update cascade, |
|||
name TEXT |
|||
); |
|||
create index newssync_label_names on newssync_labels(name); |
|||
|
|||
create table newssync_settings( |
|||
key varchar(255) primary key not null, |
|||
value varchar(255), |
|||
type varchar(255) not null |
|||
); |
|||
insert into newssync_settings values('schema_version',0,'int'); |
|||
create index main.newssync_label_names on newssync_labels(name); |
|||
|
|||
commit; |
@ -0,0 +1,47 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
Trait Common { |
|||
protected $transDepth; |
|||
|
|||
public function begin(): bool { |
|||
if($this->transDepth==0) { |
|||
$this->exec("BEGIN TRANSACTION"); |
|||
} else{ |
|||
$this->exec("SAVEPOINT newssync_".$this->transDepth); |
|||
} |
|||
$this->transDepth += 1; |
|||
return true; |
|||
} |
|||
|
|||
public function commit(bool $all = false): bool { |
|||
if($this->transDepth==0) return false; |
|||
if(!$all) { |
|||
$this->exec("RELEASE SAVEPOINT newssync_".$this->transDepth-1); |
|||
$this->transDepth -= 1; |
|||
} else { |
|||
$this->exec("COMMIT TRANSACTION"); |
|||
$this->transDepth = 0; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
public function rollback(bool $all = false): bool { |
|||
if($this->transDepth==0) return false; |
|||
if(!$all) { |
|||
$this->exec("ROLLBACK TRANSACTION TO SAVEPOINT newssync_".$this->transDepth-1); |
|||
$this->transDepth -= 1; |
|||
if($this->transDepth==0) $this->exec("ROLLBACK TRANSACTION"); |
|||
} else { |
|||
$this->exec("ROLLBACK TRANSACTION"); |
|||
$this->transDepth = 0; |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
public function prepare(string $query, string ...$paramType): Statement { |
|||
return $this->prepareArray($query, $paramType); |
|||
} |
|||
|
|||
} |
@ -0,0 +1,17 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
Trait CommonPDO { |
|||
public function unsafeQuery(string $query): Result { |
|||
return new ResultPDO($this->db->query($query)); |
|||
} |
|||
|
|||
public function prepareArray(string $query, array $paramTypes): Statement { |
|||
return new StatementPDO($query, $paramTypes); |
|||
} |
|||
|
|||
public function prepare(string $query, string ...$paramType): Statement { |
|||
return $this->prepareArray($query, $paramType); |
|||
} |
|||
} |
@ -0,0 +1,18 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
Trait CommonSQLite3 { |
|||
|
|||
static public function driverName(): string { |
|||
return "SQLite 3"; |
|||
} |
|||
|
|||
public function schemaVersion(string $schema = "main"): int { |
|||
return $this->unsafeQuery("PRAGMA $schema.user_version")->getSingle(); |
|||
} |
|||
|
|||
public function exec(string $query): bool { |
|||
return (bool) $this->db->exec($query); |
|||
} |
|||
} |
@ -0,0 +1,15 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
interface Driver { |
|||
static function create(\JKingWeb\NewsSync\Conf $conf, bool $install = false): Driver; |
|||
static function driverName(): string; |
|||
function schemaVersion(): int; |
|||
function begin(): bool; |
|||
function commit(): bool; |
|||
function rollback(): bool; |
|||
function exec(string $query): bool; |
|||
function unsafeQuery(string $query): Result; |
|||
function prepare(string $query, string ...$paramType): Statement; |
|||
} |
@ -1,7 +0,0 @@ |
|||
<?php |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
interface DriverInterface { |
|||
function __construct(\JKingWeb\NewsSync\Conf $conf); |
|||
static function driverName(): string; |
|||
} |
@ -1,44 +1,65 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
class DriverSQLite3 implements DriverInterface { |
|||
class DriverSQLite3 implements Driver { |
|||
use Common, CommonSQLite3; |
|||
|
|||
protected $db; |
|||
protected $pdo = false; |
|||
|
|||
public function __construct(\JKingWeb\NewsSync\Conf $conf, bool $install = false) { |
|||
private function __construct(\JKingWeb\NewsSync\Conf $conf, bool $install = false) { |
|||
// normalize the path |
|||
$path = $conf->dbSQLite3Path; |
|||
$sep = \DIRECTORY_SEPARATOR; |
|||
if(substr($path,-(strlen($sep))) != $sep) $path .= $sep; |
|||
$mainfile = $path."newssync-main.db"; |
|||
$feedfile = $path."newssync-feeds.db"; |
|||
// if the files exists (or we're initializing the database), try to open it and set initial options |
|||
try { |
|||
$this->db = new \SQLite3($mainfile, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, $conf->dbSQLite3Key); |
|||
$this->db->enableExceptions(true); |
|||
$attach = "'".$this->db->escapeString($feedfile)."'"; |
|||
$this->exec("ATTACH DATABASE $attach AS feeds"); |
|||
$this->exec("PRAGMA main.jounral_mode = wal"); |
|||
$this->exec("PRAGMA feeds.jounral_mode = wal"); |
|||
$this->exec("PRAGMA foreign_keys = yes"); |
|||
} catch(\Throwable $e) { |
|||
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be |
|||
foreach([$mainfile, $mainfile."-wal", $mainfile."-shm", $feedfile, $feedfile."-wal", $feedfile."-shm"] as $file) { |
|||
if(!file_exists($file)) { |
|||
if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); |
|||
throw new Exception("fileMissing", $file); |
|||
} |
|||
if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file); |
|||
if(!is_readable($file)) throw new Exception("fileUnreadable", $file); |
|||
if(!is_writable($file)) throw new Exception("fileUnwritable", $file); |
|||
} |
|||
// otherwise the database is probably corrupt |
|||
throw new Exception("fileCorrupt", $mainfile); |
|||
} |
|||
} |
|||
|
|||
public function __destruct() { |
|||
$this->db->close(); |
|||
unset($this->db); |
|||
} |
|||
|
|||
static public function create(\JKingWeb\NewsSync\Conf $conf, bool $install = false): Driver { |
|||
// check to make sure required extensions are loaded |
|||
if(class_exists("SQLite3")) { |
|||
$this->pdo = false; |
|||
return new self($conf, $install); |
|||
} else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { |
|||
$this->pdo = true; |
|||
return new DriverSQLite3PDO($conf, $install); |
|||
} else { |
|||
throw new Exception("extMissing", self::driverName()); |
|||
} |
|||
// if the file exists (or we're initializing the database), try to open it and set initial options |
|||
if((!$install && file_exists($conf->dbSQLite3File)) || $install) { |
|||
try { |
|||
$this->db = ($this->PDO) ? (new \SQLite3($conf->dbSQLite3File, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $conf->dbSQLite3Key)) : (new PDO("sqlite:".$conf->dbSQLite3File)); |
|||
//FIXME: add foreign key enforcement, WAL mode |
|||
} catch(\Throwable $e) { |
|||
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be |
|||
foreach([$conf->dbSQLite3File, $conf->dbSQLite3File."-wal", $conf->dbSQLite3File."-shm"] as $file) { |
|||
if(!file_exists($file)) { |
|||
if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file)); |
|||
throw new Exception("fileMissing", $file); |
|||
} |
|||
if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file); |
|||
if(!is_readable($file)) throw new Exception("fileUnreadable", $file); |
|||
if(!is_writable($file)) throw new Exception("fileUnwritable", $file); |
|||
} |
|||
// otherwise the database is probably corrupt |
|||
throw new Exception("fileCorrupt", $conf->dbSQLite3File); |
|||
} |
|||
} else { |
|||
throw new Exception("fileMissing", $conf->dbSQLite3File); |
|||
} |
|||
} |
|||
|
|||
static public function driverName(): string { |
|||
return "SQLite3"; |
|||
public function unsafeQuery(string $query): Result { |
|||
return new ResultSQLite3($this->db->query($query)); |
|||
} |
|||
|
|||
public function prepareArray(string $query, array $paramTypes): Statement { |
|||
return new StatementSQLite3($query, $paramTypes); |
|||
} |
|||
} |
@ -0,0 +1,28 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
class DriverSQLite3 implements Driver { |
|||
use CommonPDO, CommonSQLite3; |
|||
|
|||
protected $db; |
|||
|
|||
private function __construct(\JKingWeb\NewsSync\Conf $conf, bool $install = false) { |
|||
// FIXME: stub |
|||
} |
|||
|
|||
public function __destruct() { |
|||
// FIXME: stub |
|||
} |
|||
|
|||
static public function create(\JKingWeb\NewsSync\Conf $conf, bool $install = false): Driver { |
|||
// check to make sure required extensions are loaded |
|||
if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { |
|||
return new self($conf, $install); |
|||
} else if(class_exists("SQLite3")) { |
|||
return new DriverSQLite3($conf, $install); |
|||
} else { |
|||
throw new Exception("extMissing", self::driverName()); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
interface Result { |
|||
function __invoke(); // alias of get() |
|||
function get(); |
|||
function getSingle(); |
|||
} |
@ -0,0 +1,30 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
class ResultSQLite3 implements Result { |
|||
protected $set; |
|||
|
|||
public function __construct(\SQLite3Result $resultObj) { |
|||
$this->set = $resultObj; |
|||
} |
|||
|
|||
public function __destruct() { |
|||
$this->set->finalize(); |
|||
unset($this->set); |
|||
} |
|||
|
|||
public function __invoke() { |
|||
return $this->get(); |
|||
} |
|||
|
|||
public function get() { |
|||
return $this->set->fetchArray(\SQLITE3_ASSOC); |
|||
} |
|||
|
|||
public function getSingle() { |
|||
$res = $this->get(); |
|||
if($res===FALSE) return null; |
|||
return array_shift($res); |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
interface Statement { |
|||
function __invoke(...$bindings); // alias of run() |
|||
function run(...$bindings): Result; |
|||
function runArray(array $bindings): Result; |
|||
} |
@ -0,0 +1,62 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace JKingWeb\NewsSync\Db; |
|||
|
|||
class StatementSQLite3 implements Statement { |
|||
protected $st; |
|||
protected $types; |
|||
|
|||
public function __construct(\SQLite3Stmt $st, $bindings = null) { |
|||
$this->st = $st; |
|||
$this->types = []; |
|||
foreach($bindings as $binding) { |
|||
switch(trim(strtolower($binding))) { |
|||
case "int": |
|||
case "integer": |
|||
$this->types[] = \SQLITE3_INTEGER; break; |
|||
case "float": |
|||
case "double": |
|||
case "real": |
|||
case "numeric": |
|||
$this->types[] = \SQLITE3_FLOAT; break; |
|||
case "blob": |
|||
case "bin": |
|||
case "binary": |
|||
$this->types[] = \SQLITE3_BLOB; break; |
|||
case "text": |
|||
case "string": |
|||
case "str": |
|||
$this->types[] = \SQLITE3_TEXT; break; |
|||
default: |
|||
$this->types[] = \SQLITE3_TEXT; break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public function __destruct() { |
|||
$this->st->close(); |
|||
unset($this->st); |
|||
} |
|||
|
|||
public function __invoke(&...$values) { |
|||
return $this->runArray($values); |
|||
} |
|||
|
|||
public function run(&...$values): Result { |
|||
return $this->runArray($values); |
|||
} |
|||
|
|||
public function runArray(array &$values = null): Result { |
|||
$this->st->clear(); |
|||
$l = sizeof($values); |
|||
for($a = 0; $a < $l; $a++) { |
|||
if($values[$a]===null) { |
|||
$type = \SQLITE3_NULL; |
|||
} else { |
|||
$type = (array_key_exists($a,$this->types)) ? $this->types[$a] : \SQLITE3_TEXT; |
|||
} |
|||
$st->bindParam($a+1, $values[$a], $type); |
|||
} |
|||
return new ResultSQLite3($st->execute()); |
|||
} |
|||
} |
Loading…
Reference in new issue