diff --git a/lib/Database.php b/lib/Database.php index 152a0998..9c58c91b 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -91,19 +91,19 @@ class Database { } public function settingGet(string $key) { - return $this->db->prepare("SELECT value from arsse_settings where key is ?", "str")->run($key)->getValue(); + return $this->db->prepare("SELECT value from arsse_meta where key is ?", "str")->run($key)->getValue(); } public function settingSet(string $key, string $value): bool { - $out = !$this->db->prepare("UPDATE arsse_settings set value = ? where key is ?", "str", "str")->run($value, $key)->changes(); + $out = !$this->db->prepare("UPDATE arsse_meta set value = ? where key is ?", "str", "str")->run($value, $key)->changes(); if(!$out) { - $out = $this->db->prepare("INSERT INTO arsse_settings(key,value)", "str", "str")->run($key, $value)->changes(); + $out = $this->db->prepare("INSERT INTO arsse_meta(key,value)", "str", "str")->run($key, $value)->changes(); } return (bool) $out; } public function settingRemove(string $key): bool { - $this->db->prepare("DELETE from arsse_settings where key is ?", "str")->run($key); + $this->db->prepare("DELETE from arsse_meta where key is ?", "str")->run($key); return true; } @@ -358,13 +358,14 @@ class Database { join user on user is owner join arsse_feeds on feed = arsse_feeds.id left join topmost on folder=f_id", - "", // where terms - "pinned desc, title" // order by terms + "str", // where terms + $this->dateFormatDefault ); + $q->setOrder("pinned desc, title"); // define common table expressions - $q->setCTE("user(user) as (SELECT ?)", "str", $user); // the subject user; this way we only have to pass it to prepare() once + $q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once // topmost folders belonging to the user - $q->setCTE("topmost(f_id,top) as (select id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id)"); + $q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner is user where parent is null union select id,top from arsse_folders join topmost on parent=f_id"); if(!is_null($id)) { // this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder // if an ID is specified, add a suitable WHERE condition and bindings @@ -373,11 +374,11 @@ class Database { // if a folder is specified, make sure it exists $this->folderValidateId($user, $folder); // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree - $q->setCTE("folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder)", "int", $folder); + $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $folder); // add a suitable WHERE condition $q->setWhere("folder in (select folder from folders)"); } - return $this->db->prepare($q, "str")->run($this->dateFormatDefault); + return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } public function subscriptionRemove(string $user, int $id): bool { @@ -598,15 +599,10 @@ class Database { // a simple WHERE clause is required here $q->setWhere("arsse_feeds.id is ?", "int", $id); } else { - $q->setCTE("user(user) as (SELECT ?)", "str", $user); - $q->setCTE( - "feeds(feed) as (SELECT feed from arsse_subscriptions join user on user is owner)", - [], // binding types - [], // binding values - "join feeds on arsse_articles.feed is feeds.feed" // join expression - ); + $q->setCTE("user(user)", "SELECT ?", "str", $user); + $q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join user on user is owner", [], [], "join feeds on arsse_articles.feed is feeds.feed"); } - return (int) $this->db->prepare($q)->run()->getValue(); + return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); } public function articleList(string $user, Context $context = null): Db\Result { @@ -634,27 +630,27 @@ class Database { join subscribed_feeds on arsse_articles.feed is subscribed_feeds.id left join arsse_enclosures on arsse_enclosures.article is arsse_articles.id ", - "", // WHERE clause - "edition".($context->reverse ? " desc" : ""), // ORDER BY clause - $context->limit, - $context->offset + ["str", "str", "str"], + [$this->dateFormatDefault, $this->dateFormatDefault, $this->dateFormatDefault] ); - $q->setCTE("user(user) as (SELECT ?)", "str", $user); + $q->setOrder("edition".($context->reverse ? " desc" : "")); + $q->setLimit($context->limit, $context->offset); + $q->setCTE("user(user)", "SELECT ?", "str", $user); if($context->subscription()) { // if a subscription is specified, make sure it exists $id = $this->subscriptionValidateId($user, $context->subscription)['feed']; // add a basic CTE that will join in only the requested subscription - $q->setCTE("subscribed_feeds(id,sub) as (SELECT ?,?)", ["int","int"], [$id,$context->subscription]); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription]); } else if($context->folder()) { // if a folder is specified, make sure it exists $this->folderValidateId($user, $context->folder); // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree - $q->setCTE("folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder)", "int", $context->folder); + $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $context->folder); // add another CTE for the subscriptions within the folder - $q->setCTE("subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner join folders on arsse_subscriptions.folder is folders.folder)"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner join folders on arsse_subscriptions.folder is folders.folder"); } else { // otherwise add a CTE for all the user's subscriptions - $q->setCTE("subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner)"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner"); } // filter based on edition offset if($context->oldestEdition()) $q->setWhere("edition >= ?", "int", $context->oldestEdition); @@ -666,7 +662,7 @@ class Database { if($context->unread()) $q->setWhere("unread is ?", "bool", $context->unread); if($context->starred()) $q->setWhere("starred is ?", "bool", $context->starred); // perform the query and return results - return $this->db->prepare($q, "str", "str", "str")->run($this->dateFormatDefault, $this->dateFormatDefault, $this->dateFormatDefault); + return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } public function articleMark(string $user, array $data, Context $context = null): bool { @@ -724,9 +720,9 @@ class Database { FROM arsse_articles" ); // common table expression for the affected user - $q->setCTE("user(user) as (SELECT ?)", "str", $user); + $q->setCTE("user(user)", "SELECT ?", "str", $user); // common table expression with the values to set - $q->setCTE("target_values(read,starred) as (select ?,?)", ["bool","bool"], $values); + $q->setCTE("target_values(read,starred)", "SELECT ?,?", ["bool","bool"], $values); if($context->edition()) { // if an edition is specified, filter for its previously identified article $q->setWhere("arsse_articles.id is ?", "int", $edition['article']); @@ -737,25 +733,25 @@ class Database { // if a subscription is specified, make sure it exists $id = $this->subscriptionValidateId($user, $context->subscription)['feed']; // add a basic CTE that will join in only the requested subscription - $q->setCTE("subscribed_feeds(id,sub) as (SELECT ?,?)", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed is subscribed_feeds.id"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed is subscribed_feeds.id"); } else if($context->folder()) { // if a folder is specified, make sure it exists $this->folderValidateId($user, $context->folder); // if it does exist, add a common table expression to list it and its children so that we select from the entire subtree - $q->setCTE("folders(folder) as (SELECT ? union select id from arsse_folders join folders on parent is folder)", "int", $context->folder); + $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent is folder", "int", $context->folder); // add another CTE for the subscriptions within the folder - $q->setCTE("subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner join folders on arsse_subscriptions.folder is folders.folder)", [], [], "join subscribed_feeds on feed is subscribed_feeds.id"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner join folders on arsse_subscriptions.folder is folders.folder", [], [], "join subscribed_feeds on feed is subscribed_feeds.id"); } else { // otherwise add a CTE for all the user's subscriptions - $q->setCTE("subscribed_feeds(id,sub) as (SELECT feed,id from arsse_subscriptions join user on user is owner)", [], [], "join subscribed_feeds on feed is subscribed_feeds.id"); + $q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user is owner", [], [], "join subscribed_feeds on feed is subscribed_feeds.id"); } if($context->editions()) { // if multiple specific editions have been requested, prepare a CTE to list them and their articles if(!$context->editions) throw new Db\ExceptionInput("tooShort", ['field' => "editions", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element if(sizeof($context->editions) > 50) throw new Db\ExceptionInput("tooLong", ['field' => "editions", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements list($inParams, $inTypes) = $this->generateIn($context->editions, "int"); - $q->setCTE( - "requested_articles(id,edition) as (select article,id as edition from arsse_editions where edition in ($inParams))", + $q->setCTE("requested_articles(id,edition)", + "SELECT article,id as edition from arsse_editions where edition in ($inParams)", $inTypes, $context->editions ); @@ -765,15 +761,15 @@ class Database { if(!$context->articles) throw new Db\ExceptionInput("tooShort", ['field' => "articles", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element if(sizeof($context->articles) > 50) throw new Db\ExceptionInput("tooLong", ['field' => "articles", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements list($inParams, $inTypes) = $this->generateIn($context->articles, "int"); - $q->setCTE( - "requested_articles(id,edition) as (select id,(select max(id) from arsse_editions where article is arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams))", + $q->setCTE("requested_articles(id,edition)", + "SELECT id,(select max(id) from arsse_editions where article is arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams)", $inTypes, $context->articles ); $q->setWhere("arsse_articles.id in (select id from requested_articles)"); } else { // if neither list is specified, mock an empty table - $q->setCTE("requested_articles(id,edition) as (select 'empty','table' where 1 is 0)"); + $q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 is 0"); } // filter based on edition offset if($context->oldestEdition()) $q->setWhere("edition >= ?", "int", $context->oldestEdition); @@ -782,13 +778,9 @@ class Database { if($context->modifiedSince()) $q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince); if($context->notModifiedSince()) $q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince); // push the current query onto the CTE stack and execute the query we're actually interested in - $q->pushCTE( - "target_articles(id,edition,modified_date,to_insert,honour_read,honour_star)", // CTE table specification - [], // CTE types - [], // CTE values - $query // new query body - ); - $out += $this->db->prepare($q)->run()->changes(); + $q->pushCTE("target_articles(id,edition,modified_date,to_insert,honour_read,honour_star)"); + $q->setBody($query); + $out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); } // commit the transaction $tr->commit(); diff --git a/lib/Db/AbstractDriver.php b/lib/Db/AbstractDriver.php index d92556f8..7893f13f 100644 --- a/lib/Db/AbstractDriver.php +++ b/lib/Db/AbstractDriver.php @@ -7,11 +7,11 @@ abstract class AbstractDriver implements Driver { protected $transDepth = 0; protected $transStatus = []; - public abstract function prepareArray($query, array $paramTypes): Statement; + public abstract function prepareArray(string $query, array $paramTypes): Statement; public function schemaVersion(): int { try { - return (int) $this->query("SELECT value from arsse_settings where key is schema_version")->getValue(); + return (int) $this->query("SELECT value from arsse_meta where key is schema_version")->getValue(); } catch(Exception $e) { return 0; } @@ -111,26 +111,26 @@ abstract class AbstractDriver implements Driver { if($this->isLocked()) return false; $uuid = UUID::mintStr(); try { - $this->prepare("INSERT INTO arsse_settings(key,value) values(?,?)", "str", "str")->run("lock", $uuid); + $this->prepare("INSERT INTO arsse_meta(key,value) values(?,?)", "str", "str")->run("lock", $uuid); } catch(ExceptionInput $e) { return false; } sleep(1); - return ($this->query("SELECT value from arsse_settings where key is 'lock'")->getValue() == $uuid); + return ($this->query("SELECT value from arsse_meta where key is 'lock'")->getValue() == $uuid); } public function unlock(): bool { if($this->schemaVersion() < 1) return true; - $this->exec("DELETE from arsse_settings where key is 'lock'"); + $this->exec("DELETE from arsse_meta where key is 'lock'"); return true; } public function isLocked(): bool { if($this->schemaVersion() < 1) return false; - return ($this->query("SELECT count(*) from arsse_settings where key is 'lock'")->getValue() > 0); + return ($this->query("SELECT count(*) from arsse_meta where key is 'lock'")->getValue() > 0); } - public function prepare($query, ...$paramType): Statement { + public function prepare(string $query, ...$paramType): Statement { return $this->prepareArray($query, $paramType); } } \ No newline at end of file diff --git a/lib/Db/Driver.php b/lib/Db/Driver.php index 51c50de1..579d69e6 100644 --- a/lib/Db/Driver.php +++ b/lib/Db/Driver.php @@ -33,6 +33,6 @@ interface Driver { // perform a single unsanitized query and return a result set function query(string $query): Result; // ready a prepared statement for later execution - function prepare($query, ...$paramType): Statement; - function prepareArray($query, array $paramTypes): Statement; + function prepare(string $query, ...$paramType): Statement; + function prepareArray(string $query, array $paramTypes): Statement; } \ No newline at end of file diff --git a/lib/Db/SQLite3/Driver.php b/lib/Db/SQLite3/Driver.php index d1583bd6..a97e4732 100644 --- a/lib/Db/SQLite3/Driver.php +++ b/lib/Db/SQLite3/Driver.php @@ -125,22 +125,13 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver { return new Result($r, [$changes, $lastId]); } - public function prepareArray($query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { - if($query instanceof \JKingWeb\Arsse\Misc\Query) { - $preValues = $query->getCTEValues(); - $postValues = $query->getWhereValues(); - $paramTypes = [$query->getCTETypes(), $paramTypes, $query->getWhereTypes()]; - $query = $query->getQuery(); - } else { - $preValues = []; - $postValues = []; - } + public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement { try { $s = $this->db->prepare($query); } catch(\Exception $e) { list($excClass, $excMsg, $excData) = $this->exceptionBuild(); throw new $excClass($excMsg, $excData); } - return new Statement($this->db, $s, $paramTypes, $preValues, $postValues); + return new Statement($this->db, $s, $paramTypes); } } \ No newline at end of file diff --git a/lib/Db/SQLite3/Statement.php b/lib/Db/SQLite3/Statement.php index 7a8b4119..7f64096c 100644 --- a/lib/Db/SQLite3/Statement.php +++ b/lib/Db/SQLite3/Statement.php @@ -26,12 +26,10 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { protected $db; protected $st; - public function __construct(\SQLite3 $db, \SQLite3Stmt $st, array $bindings = [], array $preValues = [], array $postValues = []) { + public function __construct(\SQLite3 $db, \SQLite3Stmt $st, array $bindings = []) { $this->db = $db; $this->st = $st; $this->rebindArray($bindings); - $this->values['pre'] = $preValues; - $this->values['post'] = $postValues; } public function __destruct() { @@ -49,7 +47,6 @@ class Statement extends \JKingWeb\Arsse\Db\AbstractStatement { public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result { $this->st->clear(); - $values = [$this->values['pre'], $values, $this->values['post']]; $this->bindValues($values); try { $r = $this->st->execute(); diff --git a/lib/Misc/Query.php b/lib/Misc/Query.php index 32edc837..a25d195d 100644 --- a/lib/Misc/Query.php +++ b/lib/Misc/Query.php @@ -3,7 +3,9 @@ declare(strict_types=1); namespace JKingWeb\Arsse\Misc; class Query { - protected $body = ""; + protected $qBody = ""; // main query body + protected $tBody = []; // main query parameter types + protected $vBody = []; // main query parameter values protected $qCTE = []; // Common table expression query components protected $tCTE = []; // Common table expression type bindings protected $vCTE = []; // Common table expression binding values @@ -16,27 +18,30 @@ class Query { protected $offset = 0; - function __construct(string $body, string $where = "", string $order = "", int $limit = 0, int $offset = 0) { - if(strlen($body)) $this->body = $body; - if(strlen($where)) $this->qWhere[] = $where; - if(strlen($order)) $this->order[] = $order; - $this->limit = $limit; - $this->offset = $offset; + function __construct(string $body = "", $types = null, $values = null) { + $this->setBody($body, $types, $values); } - function setCTE(string $body, $types = null, $values = null, string $join = ''): bool { - if(!strlen($body)) return false; - $this->qCTE[] = $body; + function setBody(string $body = "", $types = null, $values = null): bool { + $this->qBody = $body; + if(!is_null($types)) { + $this->tBody[] = $types; + $this->vBody[] = $values; + } + return true; + } + + function setCTE(string $tableSpec, string $body, $types = null, $values = null, string $join = ''): bool { + $this->qCTE[] = "$tableSpec as ($body)"; if(!is_null($types)) { $this->tCTE[] = $types; $this->vCTE[] = $values; } - if(strlen($join)) $this->jCTE[] = $join; // the CTE may only participate in subqueries rather than a join on the main query + if(strlen($join)) $this->jCTE[] = $join; // the CTE might only participate in subqueries rather than a join on the main query return true; } function setWhere(string $where, $types = null, $values = null): bool { - if(!strlen($where)) return false; $this->qWhere[] = $where; if(!is_null($types)) { $this->tWhere[] = $types; @@ -45,8 +50,7 @@ class Query { return true; } - function setOrder(string $oder, bool $prepend = false): bool { - if(!strlen($order)) return false; + function setOrder(string $order, bool $prepend = false): bool { if($prepend) { array_unshift($this->order, $order); } else { @@ -55,7 +59,29 @@ class Query { return true; } - function getQuery(): string { + function setLimit(int $limit, int $offset = 0): bool { + $this->limit = $limit; + $this->offset = $offset; + return true; + } + + function pushCTE(string $tableSpec, string $join = ''): bool { + // this function takes the query body and converts it to a common table expression, putting it at the bottom of the existing CTE stack + // all WHERE, ORDER BY, and LIMIT parts belong to the new CTE and are removed from the main query + $this->setCTE($tableSpec, $this->buildQueryBody(), [$this->tBody, $this->tWhere], [$this->vBody, $this->vWhere]); + $this->jCTE = []; + $this->tBody = []; + $this->vBody = []; + $this->qWhere = []; + $this->tWhere = []; + $this->vWhere = []; + $this->order = []; + $this->setLimit(0,0); + if(strlen($join)) $this->jCTE[] = $join; + return true; + } + + function __toString(): string { $out = ""; if(sizeof($this->qCTE)) { // start with common table expressions @@ -66,28 +92,16 @@ class Query { return $out; } - function pushCTE(string $tableSpec, $types, $values, string $body, string $where = "", string $order = "", int $limit = 0, int $offset = 0): bool { - // this function takes the query body and converts it to a common table expression, putting it at the bottom of the existing CTE stack - // all WHERE and ORDER BY parts belong to the new CTE and are removed from the main query - $b = $this->buildQueryBody(); - array_push($types, $this->getWhereTypes()); - array_push($values, $this->getWhereValues()); - if($this->limit) { - array_push($types, "strict int"); - array_push($values, $this->limit); - } - if($this->offset) { - array_push($types, "strict int"); - array_push($values, $this->offset); - } - $this->setCTE($tableSpec." as (".$this->buildQueryBody().")", $types, $values); - $this->jCTE = []; - $this->qWhere = []; - $this->tWhere = []; - $this->vWhere = []; - $this->order = []; - $this->__construct($body, $where, $order, $limit, $offset); - return true; + function getQuery(): string { + return $this->__toString(); + } + + function getTypes(): array { + return [$this->tCTE, $this->tBody, $this->tWhere]; + } + + function getValues(): array { + return [$this->vCTE, $this->vBody, $this->vWhere]; } function getWhereTypes(): array { @@ -109,7 +123,7 @@ class Query { protected function buildQueryBody(): string { $out = ""; // add the body - $out .= $this->body; + $out .= $this->qBody; if(sizeof($this->qCTE)) { // add any joins against CTEs $out .= " ".implode(" ", $this->jCTE); diff --git a/sql/SQLite3/0.sql b/sql/SQLite3/0.sql index 555ec560..726a9b85 100644 --- a/sql/SQLite3/0.sql +++ b/sql/SQLite3/0.sql @@ -1,91 +1,90 @@ --- settings -create table arsse_settings( - key varchar(255) primary key not null, -- setting key - value varchar(255) -- setting value, serialized as a string -) without rowid; +-- metadata +create table arsse_meta( + key text primary key not null, -- metadata key + value text -- metadata value, serialized as a string +); -- users create table arsse_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_url TEXT, -- external URL to avatar - avatar_type TEXT, -- internal avatar image's MIME content type - avatar_data BLOB, -- internal avatar image's binary data + 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, -- internal avatar image's MIME content type + avatar_data blob, -- internal avatar image's binary data rights integer not null default 0 -- any administrative rights the user may have -) without rowid; +); + +-- NextCloud folders +create table arsse_folders( + id integer primary key, -- sequence number + owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of folder + parent integer references arsse_folders(id) on delete cascade, -- parent folder id + name text not null, -- folder name + modified datetime not null default CURRENT_TIMESTAMP, -- + unique(owner,name,parent) -- cannot have multiple folders with the same name under the same parent for the same owner +); -- newsfeeds, deduplicated create table arsse_feeds( id integer primary key, -- 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 + 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, -- time at which the feed last actually changed next_fetch datetime, -- time at which the feed should next be fetched - etag TEXT not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes + etag text not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes 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 not null default '', -- HTTP authentication username - password TEXT not null default '', -- HTTP authentication password (this is stored in plain text) + err_msg text, -- last error message + username text not null default '', -- HTTP authentication username + password text not null default '', -- 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 arsse_subscriptions( id integer primary key, -- sequence number - owner TEXT not null references arsse_users(id) on delete cascade on update cascade, -- owner of subscription + owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of subscription feed integer not null references arsse_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 + title text, -- user-supplied title order_type int not null default 0, -- NextCloud sort order pinned boolean not null default 0, -- whether feed is pinned (always sorts at top) folder integer references arsse_folders(id) on delete cascade, -- TT-RSS category (nestable); the first-level category (which acts as NextCloud folder) is joined in when needed unique(owner,feed) -- a given feed should only appear once for a given owner ); --- TT-RSS categories and NextCloud folders -create table arsse_folders( - id integer primary key, -- sequence number - owner TEXT not null references arsse_users(id) on delete cascade on update cascade, -- owner of folder - parent integer references arsse_folders(id) on delete cascade, -- parent folder id - name TEXT not null, -- folder name - modified datetime not null default CURRENT_TIMESTAMP, -- - unique(owner,name,parent) -- cannot have multiple folders with the same name under the same parent for the same owner -); - -- entries in newsfeeds create table arsse_articles( id integer primary key, -- sequence number feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription - url TEXT, -- URL of article - title TEXT, -- article title - author TEXT, -- author's name + url text, -- URL of article + title text, -- article title + author text, -- author's name published datetime, -- time of original publication edited datetime, -- time of last edit modified datetime not null default CURRENT_TIMESTAMP, -- date when article properties were last modified - content TEXT, -- content, as (X)HTML - guid TEXT, -- GUID - url_title_hash TEXT not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid. - url_content_hash TEXT not null, -- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. - title_content_hash TEXT not null -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. + content text, -- content, as (X)HTML + guid text, -- GUID + url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid. + url_content_hash text not null, -- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. + title_content_hash text not null -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid. ); -- enclosures associated with articles create table arsse_enclosures( article integer not null references arsse_articles(id) on delete cascade, - url TEXT, - type varchar(255) + url text, + type text ); -- users' actions on newsfeed entries create table arsse_marks( id integer primary key, article integer not null references arsse_articles(id) on delete cascade, - owner TEXT not null references arsse_users(id) on delete cascade on update cascade, + owner text not null references arsse_users(id) on delete cascade on update cascade, read boolean not null default 0, starred boolean not null default 0, modified datetime not null default CURRENT_TIMESTAMP, @@ -99,19 +98,12 @@ create table arsse_editions( modified datetime not null default CURRENT_TIMESTAMP ); --- user labels associated with newsfeed entries -create table arsse_labels( - sub_article integer not null references arsse_subscription_articles(id) on delete cascade, - name TEXT -); -create index arsse_label_names on arsse_labels(name); - -- author categories associated with newsfeed entries create table arsse_categories( article integer not null references arsse_articles(id) on delete cascade, - name TEXT + name text ); -- set version marker pragma user_version = 1; -insert into arsse_settings(key,value) values('schema_version','1'); \ No newline at end of file +insert into arsse_meta(key,value) values('schema_version','1'); \ No newline at end of file diff --git a/tests/Db/SQLite3/TestDbDriverSQLite3.php b/tests/Db/SQLite3/TestDbDriverSQLite3.php index 1dba8fec..acd31f5f 100644 --- a/tests/Db/SQLite3/TestDbDriverSQLite3.php +++ b/tests/Db/SQLite3/TestDbDriverSQLite3.php @@ -303,7 +303,7 @@ class TestDbDriverSQLite3 extends \PHPUnit\Framework\TestCase { $this->assertFalse($this->drv->isLocked()); $this->assertTrue($this->drv->lock()); $this->assertFalse($this->drv->isLocked()); - $this->drv->exec("CREATE TABLE arsse_settings(key primary key, value, type) without rowid; PRAGMA user_version=1"); + $this->drv->exec("CREATE TABLE arsse_meta(key text primary key, value text); PRAGMA user_version=1"); $this->assertTrue($this->drv->lock()); $this->assertTrue($this->drv->isLocked()); $this->assertFalse($this->drv->lock()); diff --git a/tests/Db/SQLite3/TestDbUpdateSQLite3.php b/tests/Db/SQLite3/TestDbUpdateSQLite3.php index 0aee2020..29f9f3ec 100644 --- a/tests/Db/SQLite3/TestDbUpdateSQLite3.php +++ b/tests/Db/SQLite3/TestDbUpdateSQLite3.php @@ -12,7 +12,7 @@ class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase { protected $vfs; protected $base; - const MINIMAL1 = "create table arsse_settings(key text primary key not null, value text, type text not null); pragma user_version=1"; + const MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1"; const MINIMAL2 = "pragma user_version=2"; function setUp() { @@ -56,7 +56,7 @@ class TestDbUpdateSQLite3 extends \PHPUnit\Framework\TestCase { } function testLoadIncompleteFile() { - file_put_contents($this->base."0.sql", "create table arsse_settings(key text primary key not null, value text, type text not null);"); + file_put_contents($this->base."0.sql", "create table arsse_meta(key text primary key not null, value text);"); $this->assertException("updateFileIncomplete", "Db"); $this->drv->schemaUpdate(1); }