mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 21:22:40 +00:00
Cleanup
- 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
This commit is contained in:
parent
3fad820be4
commit
7e7b204d85
9 changed files with 149 additions and 163 deletions
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
function getQuery(): string {
|
||||
return $this->__toString();
|
||||
}
|
||||
if($this->offset) {
|
||||
array_push($types, "strict int");
|
||||
array_push($values, $this->offset);
|
||||
|
||||
function getTypes(): array {
|
||||
return [$this->tCTE, $this->tBody, $this->tWhere];
|
||||
}
|
||||
$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 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);
|
||||
|
|
|
@ -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');
|
||||
insert into arsse_meta(key,value) values('schema_version','1');
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue