1
1
Fork 0
mirror of https://code.mensbeam.com/MensBeam/Arsse.git synced 2024-12-22 13:12:41 +00:00

SQLite3 database driver in working condition

PDO stub for now; other drivers to come
This commit is contained in:
J. King 2016-10-05 22:08:43 -04:00
parent 03b86c222f
commit 6ffe942f99
19 changed files with 380 additions and 112 deletions

2
.gitignore vendored
View file

@ -4,7 +4,7 @@ vendor/simplepie/*
#temp files #temp files
cache/* cache/*
test.php test.php
newssync.db* /db
# Windows image file caches # Windows image file caches
Thumbs.db Thumbs.db

View file

@ -1,109 +1,111 @@
begin; 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 -- users
create table newssync_users( create table main.newssync_users(
id TEXT primary key not null, -- user id id TEXT primary key not null, -- user id
password TEXT, -- password, salted and hashed; if using external authentication this would be blank password TEXT, -- password, salted and hashed; if using external authentication this would be blank
name TEXT, -- display name name TEXT, -- display name
avatar_type TEXT, -- avatar image's MIME content type avatar_type TEXT, -- avatar image's MIME content type
avatar_data BLOB, -- avatar image's binary data avatar_data BLOB, -- avatar image's binary data
admin boolean not null default 0 -- whether the user is an administrator admin boolean not null default 0 -- whether the user is an administrator
); );
-- TT-RSS categories and ownCloud folders -- TT-RSS categories and ownCloud folders
create table newssync_categories( create table main.newssync_categories(
id integer primary key not null, -- sequence number id integer primary key not null, -- sequence number
owner TEXT references users(id) on delete cascade on update cascade, -- owner of category owner TEXT not null references users(id) on delete cascade on update cascade, -- owner of category
parent integer, -- parent category id parent integer, -- parent category id
folder integer not null, -- first-level category (ownCloud folder) folder integer not null, -- first-level category (ownCloud folder)
name TEXT not null, -- category name name TEXT not null, -- category name
modified datetime not null default CURRENT_TIMESTAMP, -- 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 unique(owner,name,parent) -- cannot have multiple categories with the same name under the same parent for the same owner
); );
-- newsfeeds, deduplicated -- newsfeeds, deduplicated
create table newssync_feeds( create table feeds.newssync_feeds(
id integer primary key not null, -- sequence number id integer primary key not null, -- sequence number
url TEXT not null, -- URL of feed url TEXT not null, -- URL of feed
title TEXT, -- default title of feed title TEXT, -- default title of feed
favicon TEXT, -- URL of favicon favicon TEXT, -- URL of favicon
source TEXT, -- URL of site to which the feed belongs source TEXT, -- URL of site to which the feed belongs
updated datetime, -- time at which the feed was last fetched updated datetime, -- time at which the feed was last fetched
modified datetime not null default CURRENT_TIMESTAMP, -- 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_count integer not null default 0, -- count of successive times update resulted in error since last successful update
err_msg TEXT, -- last error message err_msg TEXT, -- last error message
username TEXT, -- HTTP authentication username username TEXT, -- HTTP authentication username
password TEXT, -- HTTP authentication password (this is stored in plain text) password TEXT, -- HTTP authentication password (this is stored in plain text)
unique(url,username,password) -- a URL with particular credentials should only appear once unique(url,username,password) -- a URL with particular credentials should only appear once
); );
-- users' subscriptions to newsfeeds, with settings -- users' subscriptions to newsfeeds, with settings
create table newssync_subscriptions( create table main.newssync_subscriptions(
id integer primary key not null, -- sequence number id integer primary key not null, -- sequence number
owner TEXT references users(id) on delete cascade on update cascade, -- owner of subscription owner TEXT not null references users(id) on delete cascade on update cascade, -- owner of subscription
feed integer references feeds(id) on delete cascade, -- feed for the 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 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 modified datetime not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified
title TEXT, -- user-supplied title title TEXT, -- user-supplied title
order_type int not null default 0, -- ownCloud sort order order_type int not null default 0, -- ownCloud sort order
pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) 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 unique(owner,feed) -- a given feed should only appear once for a given owner
); );
-- entries in newsfeeds -- entries in newsfeeds
create table newssync_articles( create table feeds.newssync_articles(
id integer primary key not null, -- sequence number id integer primary key not null, -- sequence number
feed integer references feeds(id) on delete cascade, -- feed for the subscription feed integer not null references feeds(id) on delete cascade, -- feed for the subscription
url TEXT not null, -- URL of article url TEXT not null, -- URL of article
title TEXT, -- article title title TEXT, -- article title
author TEXT, -- author's name author TEXT, -- author's name
published datetime, -- time of original publication published datetime, -- time of original publication
edited datetime, -- time of last edit edited datetime, -- time of last edit
guid TEXT, -- GUID guid TEXT, -- GUID
content TEXT, -- content, as (X)HTML content TEXT, -- content, as (X)HTML
modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified
hash varchar(64) not null, -- ownCloud hash hash varchar(64) not null, -- ownCloud hash
fingerprint varchar(64) not null, -- ownCloud fingerprint 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 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 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 -- users' actions on newsfeed entries
create table newssync_subscription_articles( create table main.newssync_subscription_articles(
id integer primary key not null, 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, read boolean not null default 0,
starred boolean not null default 0, starred boolean not null default 0,
modified datetime not null default CURRENT_TIMESTAMP modified datetime not null default CURRENT_TIMESTAMP
); );
-- enclosures associated with articles -- enclosures associated with articles
create table newssync_enclosures( create table main.newssync_enclosures(
article integer references articles(id) on delete cascade, article integer not null references articles(id) on delete cascade,
url TEXT, url TEXT,
type varchar(255) type varchar(255)
); );
-- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries -- author labels ("categories" in RSS/Atom parlance) associated with newsfeed entries
create table newssync_tags( create table main.newssync_tags(
article integer references articles(id) on delete cascade, article integer not null references articles(id) on delete cascade,
name TEXT name TEXT
); );
-- user labels associated with newsfeed entries -- user labels associated with newsfeed entries
create table newssync_labels( create table main.newssync_labels(
sub_article integer references subscription_articles(id) on delete cascade, sub_article integer not null references subscription_articles(id) on delete cascade,
owner TEXT references users(id) on delete cascade on update cascade, owner TEXT not null references users(id) on delete cascade on update cascade,
name TEXT name TEXT
); );
create index newssync_label_names on newssync_labels(name); create index main.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');
commit; commit;

View file

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\NewsSync\Auth; namespace JKingWeb\NewsSync\Auth;
Interface AuthInterface { Interface Driver {
public function __construct($conf, $db); public function __construct($conf, $db);
public function auth(): bool; public function auth(): bool;
public function authHTTP(): bool; public function authHTTP(): bool;

View file

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
namespace JKingWeb\NewsSync\Auth; namespace JKingWeb\NewsSync\Auth;
class Internal implements AuthInterface { class Internal implements Driver {
protected $conf; protected $conf;
protected $db; protected $db;

View file

@ -6,7 +6,7 @@ class Conf {
public $lang = "en"; public $lang = "en";
public $dbClass = NS_BASE."Db\\DriverSQLite3"; public $dbClass = NS_BASE."Db\\DriverSQLite3";
public $dbSQLite3File = BASE."newssync.db"; public $dbSQLite3Path = BASE."db";
public $dbSQLite3Key = ""; public $dbSQLite3Key = "";
public $dbPostgreSQLHost = "localhost"; public $dbPostgreSQLHost = "localhost";
public $dbPostgreSQLUser = "newssync"; public $dbPostgreSQLUser = "newssync";

View file

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class Database { class Database {
@ -6,10 +7,26 @@ class Database {
public function __construct(Conf $conf) { public function __construct(Conf $conf) {
$driver = $conf->dbClass; $driver = $conf->dbClass;
$this->drv = new $driver($conf); $this->drv = $driver::create($conf);
} }
static public function listDrivers() { static public function listDrivers(): array {
$sep = \DIRECTORY_SEPARATOR;
$path = __DIR__.$sep."Db".$sep;
$classes = [];
foreach(glob($path."Driver?*.php") as $file) {
$name = basename($file, ".php");
if(substr($name,-3) != "PDO") {
$name = NS_BASE."Db\\$name";
if(class_exists($name)) {
$classes[$name] = $name::driverName();
}
}
}
return $classes;
}
public function schemaVersion(): int {
return $this->drv->schemaVersion();
} }
} }

47
vendor/JKingWeb/NewsSync/Db/Common.php vendored Normal file
View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

15
vendor/JKingWeb/NewsSync/Db/Driver.php vendored Normal file
View file

@ -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;
}

View file

@ -1,7 +0,0 @@
<?php
namespace JKingWeb\NewsSync\Db;
interface DriverInterface {
function __construct(\JKingWeb\NewsSync\Conf $conf);
static function driverName(): string;
}

View file

@ -1,44 +1,65 @@
<?php <?php
declare(strict_types=1);
namespace JKingWeb\NewsSync\Db; namespace JKingWeb\NewsSync\Db;
class DriverSQLite3 implements DriverInterface { class DriverSQLite3 implements Driver {
protected $db; use Common, CommonSQLite3;
protected $pdo = false;
public function __construct(\JKingWeb\NewsSync\Conf $conf, bool $install = false) { protected $db;
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 // check to make sure required extensions are loaded
if(class_exists("SQLite3")) { if(class_exists("SQLite3")) {
$this->pdo = false; return new self($conf, $install);
} else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) { } else if(class_exists("PDO") && in_array("sqlite",\PDO::getAvailableDrivers())) {
$this->pdo = true; return new DriverSQLite3PDO($conf, $install);
} else { } else {
throw new Exception("extMissing", self::driverName()); 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 { public function unsafeQuery(string $query): Result {
return "SQLite3"; return new ResultSQLite3($this->db->query($query));
}
public function prepareArray(string $query, array $paramTypes): Statement {
return new StatementSQLite3($query, $paramTypes);
} }
} }

View file

@ -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());
}
}
}

View file

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace JKingWeb\NewsSync\Db;
interface Result {
function __invoke(); // alias of get()
function get();
function getSingle();
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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());
}
}

View file

@ -13,7 +13,7 @@ class Exception extends \Exception {
"Lang/Exception.stringMissing" => 10105, "Lang/Exception.stringMissing" => 10105,
]; ];
public function __construct(string $msgID = "", $vars = null, Throwable $e = null) { public function __construct(string $msgID = "", $vars = null, \Throwable $e = null) {
if($msgID=="") { if($msgID=="") {
$msg = ""; $msg = "";
$code = 0; $code = 0;

View file

@ -3,9 +3,9 @@ declare(strict_types=1);
namespace JKingWeb\NewsSync; namespace JKingWeb\NewsSync;
class RuntimeData { class RuntimeData {
protected $conf; public $conf;
protected $db; public $db;
protected $auth; public $auth;
public function __construct(Conf $conf) { public function __construct(Conf $conf) {
$this->conf = $conf; $this->conf = $conf;