diff --git a/lib/Database.php b/lib/Database.php index 119e6bd..020eb1d 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -882,12 +882,18 @@ class Database { // validate inputs $folder = $this->folderValidateId($user, $folder)['id']; // create a complex query - $q = new Query("SELECT count(*) from arsse_subscriptions"); + $q = new Query( + "WITH RECURSIVE + folders(folder) as ( + select ? union all select id from arsse_folders join folders on parent = folder + ) + select count(*) from arsse_subscriptions", + ["int"], + [$folder] + ); $q->setWhere("owner = ?", "str", $user); if ($folder) { - // if the specified folder exists, add a common table expression to list it and its children so that we select from the entire subtree - $q->setCTE("folders(folder)", "SELECT ? union all select id from arsse_folders join folders on parent = folder", "int", $folder); - // add a suitable WHERE condition + // if the specified folder exists, add a suitable WHERE condition $q->setWhere("folder in (select folder from folders)"); } return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue(); @@ -1882,10 +1888,23 @@ class Database { // marking as read is ignored if the edition is not the latest, but the same is not true of the other two marks $this->db->query("UPDATE arsse_marks set touched = 0 where touched <> 0"); // set read marks - $q = $this->articleQuery($user, $context, ["id", "subscription"]); - $q->setWhere("arsse_marks.read <> coalesce(?,arsse_marks.read)", "bool", $data['read']); - $q->pushCTE("target_articles(article,subscription)"); - $q->setBody("UPDATE arsse_marks set \"read\" = ?, touched = 1 where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", "bool", $data['read']); + $subq = $this->articleQuery($user, $context, ["id", "subscription"]); + $subq->setWhere("arsse_marks.read <> coalesce(?,arsse_marks.read)", "bool", $data['read']); + $q = new Query( + "WITH RECURSIVE + target_articles(article, subscription) as ( + {$subq->getQuery()} + ) + update arsse_marks + set + \"read\" = ?, + touched = 1 + where + article in (select article from target_articles) + and subscription in (select distinct subscription from target_articles)", + [$subq->getTypes(), "bool"], + [$subq->getValues(), $data['read']] + ); $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); // get the articles associated with the requested editions if ($context->edition()) { @@ -1895,14 +1914,27 @@ class Database { } // set starred, hidden, and/or note marks (unless all requested editions actually do not exist) if ($context->article || $context->articles) { - $q = $this->articleQuery($user, $context, ["id", "subscription"]); - $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.hidden <> coalesce(?,arsse_marks.hidden))", ["str", "bool", "bool"], [$data['note'], $data['starred'], $data['hidden']]); - $q->pushCTE("target_articles(article,subscription)"); - $data = array_filter($data, function($v) { + $setData = array_filter($data, function($v) { return isset($v); }); - [$set, $setTypes, $setValues] = $this->generateSet($data, ['starred' => "bool", 'hidden' => "bool", 'note' => "str"]); - $q->setBody("UPDATE arsse_marks set touched = 1, $set where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); + [$set, $setTypes, $setValues] = $this->generateSet($setData, ['starred' => "bool", 'hidden' => "bool", 'note' => "str"]); + $subq = $this->articleQuery($user, $context, ["id", "subscription"]); + $subq->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.hidden <> coalesce(?,arsse_marks.hidden))", ["str", "bool", "bool"], [$data['note'], $data['starred'], $data['hidden']]); + $q = new Query( + "WITH RECURSIVE + target_articles(article, subscription) as ( + {$subq->getQuery()} + ) + update arsse_marks + set + touched = 1, + $set + where + article in (select article from target_articles) + and subscription in (select distinct subscription from target_articles)", + [$subq->getTypes(), $setTypes], + [$subq->getValues(), $setValues] + ); $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues()); } // finally set the modification date for all touched marks and return the number of affected marks @@ -1923,17 +1955,29 @@ class Database { return 0; } } - $q = $this->articleQuery($user, $context, ["id", "subscription"]); - $q->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.read <> coalesce(?,arsse_marks.read) or arsse_marks.hidden <> coalesce(?,arsse_marks.hidden))", ["str", "bool", "bool", "bool"], [$data['note'], $data['starred'], $data['read'], $data['hidden']]); - $q->pushCTE("target_articles(article,subscription)"); - $data = array_filter($data, function($v) { + $setData = array_filter($data, function($v) { return isset($v); }); - [$set, $setTypes, $setValues] = $this->generateSet($data, ['read' => "bool", 'starred' => "bool", 'hidden' => "bool", 'note' => "str"]); + [$set, $setTypes, $setValues] = $this->generateSet($setData, ['read' => "bool", 'starred' => "bool", 'hidden' => "bool", 'note' => "str"]); if ($updateTimestamp) { $set .= ", modified = CURRENT_TIMESTAMP"; } - $q->setBody("UPDATE arsse_marks set $set where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues); + $subq = $this->articleQuery($user, $context, ["id", "subscription"]); + $subq->setWhere("(arsse_marks.note <> coalesce(?,arsse_marks.note) or arsse_marks.starred <> coalesce(?,arsse_marks.starred) or arsse_marks.read <> coalesce(?,arsse_marks.read) or arsse_marks.hidden <> coalesce(?,arsse_marks.hidden))", ["str", "bool", "bool", "bool"], [$data['note'], $data['starred'], $data['read'], $data['hidden']]); + $q = new Query( + "WITH RECURSIVE + target_articles(article, subscription) as ( + {$subq->getQuery()} + ) + update arsse_marks + set + $set + where + article in (select article from target_articles) + and subscription in (select distinct subscription from target_articles)", + [$subq->getTypes(), $setTypes], + [$subq->getValues(), $setValues] + ); $out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes(); } $tr->commit(); diff --git a/lib/Misc/Query.php b/lib/Misc/Query.php index a2965dc..941ea78 100644 --- a/lib/Misc/Query.php +++ b/lib/Misc/Query.php @@ -10,9 +10,6 @@ class Query { 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 protected $qWhere = []; // WHERE clause components protected $tWhere = []; // WHERE clause type bindings protected $vWhere = []; // WHERE clause binding values @@ -37,15 +34,6 @@ class Query { return $this; } - public function setCTE(string $tableSpec, string $body, $types = null, $values = null): self { - $this->qCTE[] = "$tableSpec as ($body)"; - if (!is_null($types)) { - $this->tCTE[] = $types; - $this->vCTE[] = $values; - } - return $this; - } - public function setWhere(string $where, $types = null, $values = null): self { $this->qWhere[] = $where; if (!is_null($types)) { @@ -84,33 +72,8 @@ class Query { return $this; } - public function pushCTE(string $tableSpec): self { - // 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->tWhereNot], [$this->vBody, $this->vWhere, $this->vWhereNot]); - $this->tBody = []; - $this->vBody = []; - $this->qWhere = []; - $this->tWhere = []; - $this->vWhere = []; - $this->qWhereNot = []; - $this->tWhereNot = []; - $this->vWhereNot = []; - $this->order = []; - $this->group = []; - $this->setLimit(0, 0); - return $this; - } - public function __toString(): string { - $out = ""; - if (sizeof($this->qCTE)) { - // start with common table expressions - $out .= "WITH RECURSIVE ".implode(", ", $this->qCTE)." "; - } - // add the body - $out .= $this->buildQueryBody(); - return $out; + return $this->buildQueryBody(); } public function getQuery(): string { @@ -118,11 +81,11 @@ class Query { } public function getTypes(): array { - return ValueInfo::flatten([$this->tCTE, $this->tBody, $this->tWhere, $this->tWhereNot]); + return ValueInfo::flatten([$this->tBody, $this->tWhere, $this->tWhereNot]); } public function getValues(): array { - return ValueInfo::flatten([$this->vCTE, $this->vBody, $this->vWhere, $this->vWhereNot]); + return ValueInfo::flatten([$this->vBody, $this->vWhere, $this->vWhereNot]); } protected function buildQueryBody(): string { diff --git a/tests/cases/Misc/TestQuery.php b/tests/cases/Misc/TestQuery.php index db8a629..053d109 100644 --- a/tests/cases/Misc/TestQuery.php +++ b/tests/cases/Misc/TestQuery.php @@ -77,38 +77,15 @@ class TestQuery extends \JKingWeb\Arsse\Test\AbstractTest { $this->assertSame([], $q->getValues()); } - public function testQueryWithCommonTableExpression(): void { - $q = (new Query("select * from table where a in (select * from cte where a = ?)", "int", 1))->setCTE("cte", "select * from other_table where a = ? and b = ?", ["str", "str"], [2, 3]); - $this->assertSame("WITH RECURSIVE cte as (select * from other_table where a = ? and b = ?) select * from table where a in (select * from cte where a = ?)", $q->getQuery()); - $this->assertSame(["str", "str", "int"], $q->getTypes()); - $this->assertSame([2, 3, 1], $q->getValues()); - // multiple CTEs - $q = (new Query("select * from table where a in (select * from cte1 join cte2 using (a) where a = ?)", "int", 1))->setCTE("cte1", "select * from other_table where a = ? and b = ?", ["str", "str"], [2, 3])->setCTE("cte2", "select * from other_table where c between ? and ?", ["datetime", "datetime"], [4, 5]); - $this->assertSame("WITH RECURSIVE cte1 as (select * from other_table where a = ? and b = ?), cte2 as (select * from other_table where c between ? and ?) select * from table where a in (select * from cte1 join cte2 using (a) where a = ?)", $q->getQuery()); - $this->assertSame(["str", "str", "datetime", "datetime", "int"], $q->getTypes()); - $this->assertSame([2, 3, 4, 5, 1], $q->getValues()); - } - - public function testQueryWithPushedCommonTableExpression(): void { - $q = (new Query("select * from table1"))->setWhere("a between ? and ?", ["datetime", "datetime"], [1, 2]) - ->setCTE("cte1", "select * from table2 where a = ? and b = ?", ["str", "str"], [3, 4]) - ->pushCTE("cte2") - ->setBody("select * from table3 join cte1 using (a) join cte2 using (a) where a = ?", "int", 5); - $this->assertSame("WITH RECURSIVE cte1 as (select * from table2 where a = ? and b = ?), cte2 as (select * from table1 WHERE a between ? and ?) select * from table3 join cte1 using (a) join cte2 using (a) where a = ?", $q->getQuery()); - $this->assertSame(["str", "str", "datetime", "datetime", "int"], $q->getTypes()); - $this->assertSame([3, 4, 1, 2, 5], $q->getValues()); - } - public function testComplexQuery(): void { - $q = (new query("select *, ? as const from table", "datetime", 1)) + $q = (new query("SELECT *, ? as const from table", "datetime", 1)) ->setWhereNot("b = ?", "bool", 2) ->setGroup("col1", "col2") ->setWhere("a = ?", "str", 3) ->setLimit(4, 5) - ->setOrder("col3") - ->setCTE("cte", "select ? as const", "int", 6); - $this->assertSame("WITH RECURSIVE cte as (select ? as const) select *, ? as const from table WHERE a = ? AND NOT (b = ?) GROUP BY col1, col2 ORDER BY col3 LIMIT 4 OFFSET 5", $q->getQuery()); - $this->assertSame(["int", "datetime", "str", "bool"], $q->getTypes()); - $this->assertSame([6, 1, 3, 2], $q->getValues()); + ->setOrder("col3"); + $this->assertSame("SELECT *, ? as const from table WHERE a = ? AND NOT (b = ?) GROUP BY col1, col2 ORDER BY col3 LIMIT 4 OFFSET 5", $q->getQuery()); + $this->assertSame(["datetime", "str", "bool"], $q->getTypes()); + $this->assertSame([1, 3, 2], $q->getValues()); } }