diff --git a/lib/Database.php b/lib/Database.php index 2486907..49f32e8 100644 --- a/lib/Database.php +++ b/lib/Database.php @@ -1323,7 +1323,9 @@ class Database { "markedSince" => ["marked_date", ">=", "datetime", "notMarkedSince"], "notMarkedSince" => ["marked_date", "<=", "datetime", "markedSince"], "folderShallow" => ["folder", "=", "int", ""], + "foldersShallow" => ["folder", "in", "int", ""], "subscription" => ["subscription", "=", "int", ""], + "subscriptions" => ["subscription", "in", "int", ""], "unread" => ["unread", "=", "bool", ""], "starred" => ["starred", "=", "bool", ""], ]; @@ -1374,6 +1376,76 @@ class Database { $q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->not->$m); } } + // handle labels and tags + $options = [ + 'label' => [ + 'match_col' => "arsse_articles.id", + 'cte_name' => "labelled", + 'cte_cols' => ["article", "label_id", "label_name"], + 'cte_body' => "SELECT m.article, l.id, l.name from arsse_label_members as m join arsse_labels as l on l.id = m.label where l.owner = ? and m.assigned = 1", + 'cte_types' => ["str"], + 'cte_values' => [$user], + 'options' => [ + 'label' => ['use_name' => false, 'multi' => false], + 'labels' => ['use_name' => false, 'multi' => true], + 'labelName' => ['use_name' => true, 'multi' => false], + 'labelNames' => ['use_name' => true, 'multi' => true], + ], + ], + 'tag' => [ + 'match_col' => "arsse_subscriptions.id", + 'cte_name' => "tagged", + 'cte_cols' => ["subscription", "tag_id", "tag_name"], + 'cte_body' => "SELECT m.subscription, t.id, t.name from arsse_tag_members as m join arsse_tags as t on t.id = m.tag where t.owner = ? and m.assigned = 1", + 'cte_types' => ["str"], + 'cte_values' => [$user], + 'options' => [ + 'tag' => ['use_name' => false, 'multi' => false], + 'tags' => ['use_name' => false, 'multi' => true], + 'tagName' => ['use_name' => true, 'multi' => false], + 'tagNames' => ['use_name' => true, 'multi' => true], + ], + ], + ]; + foreach ($options as $opt) { + $seen = false; + $match = $opt['match_col']; + $table = $opt['cte_name']; + foreach ($opt['options'] as $m => $props) { + $named = $props['use_name']; + $multi = $props['multi']; + $selection = $opt['cte_cols'][0]; + $col = $opt['cte_cols'][$named ? 2 : 1]; + if ($context->$m()) { + $seen = true; + if ($multi) { + list($test, $types, $values) = $this->generateIn($context->$m, $named ? "str" : "int"); + $test = "in ($test)"; + } else { + $test = "= ?"; + $types = $named ? "str" : "int"; + $values = $context->$m; + } + $q->setWhere("$match in (select $selection from $table where $col $test)", $types, $values); + } + if ($context->not->$m()) { + $seen = true; + if ($multi) { + list($test, $types, $values) = $this->generateIn($context->not->$m, $named ? "str" : "int"); + $test = "in ($test)"; + } else { + $test = "= ?"; + $types = $named ? "str" : "int"; + $values = $context->not->$m; + } + $q->setWhereNot("$match in (select $selection from $table where $col $test)", $types, $values); + } + } + if ($seen) { + $spec = $opt['cte_name']."(".implode(",",$opt['cte_cols']).")"; + $q->setCTE($spec, $opt['cte_body'], $opt['cte_types'], $opt['cte_values']); + } + } // handle complex context options if ($context->annotated()) { $comp = ($context->annotated) ? "<>" : "="; @@ -1384,36 +1456,6 @@ class Database { $op = $context->labelled ? ">" : "="; $q->setWhere("coalesce(label_stats.assigned,0) $op 0"); } - if ($context->label() || $context->not->label() || $context->labelName() || $context->not->labelName()) { - $q->setCTE("labelled(article,label_id,label_name)","SELECT m.article, l.id, l.name from arsse_label_members as m join arsse_labels as l on l.id = m.label where l.owner = ? and m.assigned = 1", "str", $user); - if ($context->label()) { - $q->setWhere("arsse_articles.id in (select article from labelled where label_id = ?)", "int", $context->label); - } - if ($context->not->label()) { - $q->setWhereNot("arsse_articles.id in (select article from labelled where label_id = ?)", "int", $context->not->label); - } - if ($context->labelName()) { - $q->setWhere("arsse_articles.id in (select article from labelled where label_name = ?)", "str", $context->labelName); - } - if ($context->not->labelName()) { - $q->setWhereNot("arsse_articles.id in (select article from labelled where label_name = ?)", "str", $context->not->labelName); - } - } - if ($context->tag() || $context->not->tag() || $context->tagName() || $context->not->tagName()) { - $q->setCTE("tagged(id,name,subscription)","SELECT arsse_tags.id, arsse_tags.name, arsse_tag_members.subscription FROM arsse_tag_members join arsse_tags on arsse_tags.id = arsse_tag_members.tag WHERE arsse_tags.owner = ? and assigned = 1", "str", $user); - if ($context->tag()) { - $q->setWhere("arsse_subscriptions.id in (select subscription from tagged where id = ?)", "int", $context->tag); - } - if ($context->not->tag()) { - $q->setWhereNot("arsse_subscriptions.id in (select subscription from tagged where id = ?)", "int", $context->not->tag); - } - if ($context->tagName()) { - $q->setWhere("arsse_subscriptions.id in (select subscription from tagged where name = ?)", "str", $context->tagName); - } - if ($context->not->tagName()) { - $q->setWhereNot("arsse_subscriptions.id in (select subscription from tagged where name = ?)", "str", $context->not->tagName); - } - } if ($context->folder()) { // add a common table expression to list the folder and its children so that we select from the entire subtree $q->setCTE("folders(folder)", "SELECT ? union select id from arsse_folders join folders on parent = folder", "int", $context->folder); diff --git a/tests/cases/Database/SeriesArticle.php b/tests/cases/Database/SeriesArticle.php index 5340fcc..51d9da5 100644 --- a/tests/cases/Database/SeriesArticle.php +++ b/tests/cases/Database/SeriesArticle.php @@ -427,7 +427,9 @@ trait SeriesArticle { 'Leaf folder' => [(new Context)->folder(6), [7,8]], 'Root folder only' => [(new Context)->folderShallow(0), [1,2,3,4]], 'Shallow folder' => [(new Context)->folderShallow(1), [5,6]], + 'Multiple shallow folders' => [(new Context)->foldersShallow([1,6]), [5,6,7,8]], 'Subscription' => [(new Context)->subscription(5), [19,20]], + 'Multiple subscriptions' => [(new Context)->subscriptions([4,5]), [7,8,19,20]], 'Unread' => [(new Context)->subscription(5)->unread(true), [20]], 'Read' => [(new Context)->subscription(5)->unread(false), [19]], 'Starred' => [(new Context)->starred(true), [1,20]], @@ -458,8 +460,10 @@ trait SeriesArticle { 'Reversed paged results' => [(new Context)->limit(2)->latestEdition(7)->reverse(true), [7,6]], 'With label ID 1' => [(new Context)->label(1), [1,19]], 'With label ID 2' => [(new Context)->label(2), [1,5,20]], + 'With label ID 1 or 2' => [(new Context)->labels([1,2]), [1,5,19,20]], 'With label "Interesting"' => [(new Context)->labelName("Interesting"), [1,19]], 'With label "Fascinating"' => [(new Context)->labelName("Fascinating"), [1,5,20]], + 'With label "Interesting" or "Fascinating"' => [(new Context)->labelNames(["Interesting","Fascinating"]), [1,5,19,20]], 'Article ID 20' => [(new Context)->article(20), [20]], 'Edition ID 1001' => [(new Context)->edition(1001), [20]], 'Multiple articles' => [(new Context)->articles([1,20,50]), [1,20]], @@ -494,8 +498,10 @@ trait SeriesArticle { 'Search 501 terms' => [(new Context)->searchTerms(array_merge(range(1,500),[str_repeat("a", 1000)])), []], 'With tag ID 1' => [(new Context)->tag(1), [5,6,7,8]], 'With tag ID 5' => [(new Context)->tag(5), [7,8,19,20]], + 'With tag ID 1 or 5' => [(new Context)->tags([1,5]), [5,6,7,8,19,20]], 'With tag "Technology"' => [(new Context)->tagName("Technology"), [5,6,7,8]], 'With tag "Politics"' => [(new Context)->tagName("Politics"), [7,8,19,20]], + 'With tag "Technology" or "Politics"' => [(new Context)->tagNames(["Technology","Politics"]), [5,6,7,8,19,20]], 'Excluding tag ID 1' => [(new Context)->not->tag(1), [1,2,3,4,19,20]], 'Excluding tag ID 5' => [(new Context)->not->tag(5), [1,2,3,4,5,6]], 'Excluding tag "Technology"' => [(new Context)->not->tagName("Technology"), [1,2,3,4,19,20]],