- Revamped design of Query class to be more consistent and predictable, and generally suck less
- Removed special case for Query class in Statement class
- Cleaned up database schema somewhat
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 {
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) {
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;
return (bool) $out;
}
}
public function settingRemove(string $key): bool {
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;
return true;
}
}
@ -358,13 +358,14 @@ class Database {
join user on user is owner
join user on user is owner
join arsse_feeds on feed = arsse_feeds.id
join arsse_feeds on feed = arsse_feeds.id
left join topmost on folder=f_id",
left join topmost on folder=f_id",
"", // where terms
"str", // where terms
"pinned desc, title" // order by terms
$this->dateFormatDefault
);
);
$q->setOrder("pinned desc, title");
// define common table expressions
// 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
// 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)) {
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
// 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
// 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
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $folder);
$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
// 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
// add a suitable WHERE condition
$q->setWhere("folder in (select folder from folders)");
$q->setWhere("folder in (select folder from folders)");
$q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join user on user is owner", [], [], "join feeds on arsse_articles.feed is feeds.feed");
"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
// if it does exist, add a common table expression to list it and its children so that we select from the entire subtree
// 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
// 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 {
} else {
// otherwise add a CTE for all the user's subscriptions
// 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");
// add a basic CTE that will join in only the requested subscription
// 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()) {
} else if($context->folder()) {
// if a folder is specified, make sure it exists
// if a folder is specified, make sure it exists
$this->folderValidateId($user, $context->folder);
$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
// 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
// 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 {
} else {
// otherwise add a CTE for all the user's subscriptions
// 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($context->editions()) {
// if multiple specific editions have been requested, prepare a CTE to list them and their articles
// 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(!$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
if(sizeof($context->editions) > 50) throw new Db\ExceptionInput("tooLong", ['field' => "editions", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements
"requested_articles(id,edition) as (select article,id as edition from arsse_editions where edition in ($inParams))",
"SELECT article,id as edition from arsse_editions where edition in ($inParams)",
$inTypes,
$inTypes,
$context->editions
$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(!$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
if(sizeof($context->articles) > 50) throw new Db\ExceptionInput("tooLong", ['field' => "articles", 'action' => __FUNCTION__, 'max' => 50]); // must not have more than 50 array elements
"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))",
"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,
$inTypes,
$context->articles
$context->articles
);
);
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
} else {
} else {
// if neither list is specified, mock an empty table
// 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");
passwordTEXTnotnulldefault'',-- HTTP authentication password (this is stored in plain text)
passwordtextnotnulldefault'',-- 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
createtablearsse_subscriptions(
createtablearsse_subscriptions(
idintegerprimarykey,-- sequence number
idintegerprimarykey,-- sequence number
ownerTEXTnotnullreferencesarsse_users(id)ondeletecascadeonupdatecascade,-- owner of subscription
ownertextnotnullreferencesarsse_users(id)ondeletecascadeonupdatecascade,-- owner of subscription
feedintegernotnullreferencesarsse_feeds(id)ondeletecascade,-- feed for the subscription
feedintegernotnullreferencesarsse_feeds(id)ondeletecascade,-- feed for the subscription
addeddatetimenotnulldefaultCURRENT_TIMESTAMP,-- time at which feed was added
addeddatetimenotnulldefaultCURRENT_TIMESTAMP,-- time at which feed was added
modifieddatetimenotnulldefaultCURRENT_TIMESTAMP,-- date at which subscription properties were last modified
modifieddatetimenotnulldefaultCURRENT_TIMESTAMP,-- date at which subscription properties were last modified
titleTEXT,-- user-supplied title
titletext,-- user-supplied title
order_typeintnotnulldefault0,-- NextCloud sort order
order_typeintnotnulldefault0,-- NextCloud sort order
pinnedbooleannotnulldefault0,-- whether feed is pinned (always sorts at top)
pinnedbooleannotnulldefault0,-- whether feed is pinned (always sorts at top)
folderintegerreferencesarsse_folders(id)ondeletecascade,-- TT-RSS category (nestable); the first-level category (which acts as NextCloud folder) is joined in when needed
folderintegerreferencesarsse_folders(id)ondeletecascade,-- 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
unique(owner,feed)-- a given feed should only appear once for a given owner
);
);
-- TT-RSS categories and NextCloud folders
createtablearsse_folders(
idintegerprimarykey,-- sequence number
ownerTEXTnotnullreferencesarsse_users(id)ondeletecascadeonupdatecascade,-- owner of folder
parentintegerreferencesarsse_folders(id)ondeletecascade,-- parent folder id
unique(owner,name,parent)-- cannot have multiple folders with the same name under the same parent for the same owner
);
-- entries in newsfeeds
-- entries in newsfeeds
createtablearsse_articles(
createtablearsse_articles(
idintegerprimarykey,-- sequence number
idintegerprimarykey,-- sequence number
feedintegernotnullreferencesarsse_feeds(id)ondeletecascade,-- feed for the subscription
feedintegernotnullreferencesarsse_feeds(id)ondeletecascade,-- feed for the subscription
urlTEXT,-- URL of article
urltext,-- URL of article
titleTEXT,-- article title
titletext,-- article title
authorTEXT,-- author's name
authortext,-- author's name
publisheddatetime,-- time of original publication
publisheddatetime,-- time of original publication
editeddatetime,-- time of last edit
editeddatetime,-- time of last edit
modifieddatetimenotnulldefaultCURRENT_TIMESTAMP,-- date when article properties were last modified
modifieddatetimenotnulldefaultCURRENT_TIMESTAMP,-- date when article properties were last modified
contentTEXT,-- content, as (X)HTML
contenttext,-- content, as (X)HTML
guidTEXT,-- GUID
guidtext,-- GUID
url_title_hashTEXTnotnull,-- hash of URL + title; used when checking for updates and for identification if there is no guid.
url_title_hashtextnotnull,-- hash of URL + title; used when checking for updates and for identification if there is no guid.
url_content_hashTEXTnotnull,-- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid.
url_content_hashtextnotnull,-- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid.
title_content_hashTEXTnotnull-- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid.
title_content_hashtextnotnull-- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid.