Browse Source

Allow ranges in exclusion contexts

microsub
J. King 5 years ago
parent
commit
0dc82f64d5
  1. 46
      lib/Context/Context.php
  2. 45
      lib/Context/ExclusionContext.php
  3. 83
      lib/Database.php
  4. 3
      lib/Misc/Query.php
  5. 80
      tests/cases/Database/SeriesArticle.php

46
lib/Context/Context.php

@ -6,8 +6,6 @@
declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
use JKingWeb\Arsse\Misc\Date;
class Context extends ExclusionContext {
/** @var ExclusionContext */
public $not;
@ -18,14 +16,6 @@ class Context extends ExclusionContext {
public $starred;
public $labelled;
public $annotated;
public $oldestArticle;
public $latestArticle;
public $oldestEdition;
public $latestEdition;
public $modifiedSince;
public $notModifiedSince;
public $markedSince;
public $notMarkedSince;
public function __construct() {
$this->not = new ExclusionContext($this);
@ -67,40 +57,4 @@ class Context extends ExclusionContext {
public function annotated(bool $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function latestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function latestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function modifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notModifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function markedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notMarkedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
}

45
lib/Context/ExclusionContext.php

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace JKingWeb\Arsse\Context;
use JKingWeb\Arsse\Misc\ValueInfo;
use JKingWeb\Arsse\Misc\Date;
class ExclusionContext {
public $folder;
@ -22,6 +23,14 @@ class ExclusionContext {
public $searchTerms;
public $titleTerms;
public $authorTerms;
public $oldestArticle;
public $latestArticle;
public $oldestEdition;
public $latestEdition;
public $modifiedSince;
public $notModifiedSince;
public $markedSince;
public $notMarkedSince;
protected $props = [];
protected $parent;
@ -152,4 +161,40 @@ class ExclusionContext {
}
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function latestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestArticle(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function latestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function oldestEdition(int $spec = null) {
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function modifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notModifiedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function markedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
public function notMarkedSince($spec = null) {
$spec = Date::normalize($spec);
return $this->act(__FUNCTION__, func_num_args(), $spec);
}
}

83
lib/Database.php

@ -1179,32 +1179,34 @@ class Database {
// if there are no output columns requested we're getting a count and should not group, but otherwise we should
$q->setGroup("arsse_articles.id", "arsse_marks.note", "arsse_enclosures.url", "arsse_enclosures.type", "arsse_subscriptions.title", "arsse_feeds.title", "arsse_subscriptions.id", "arsse_marks.modified", "arsse_label_members.modified", "arsse_marks.read", "arsse_marks.starred", "latest_editions.edition");
}
$excContext = new ExclusionContext;
// handle the simple context options
$options = [
// each context array consists of a column identifier (see $colDefs above), a comparison operator, a data type, and an upper bound if the value is an array
"edition" => ["edition", "=", "int", 1],
"editions" => ["edition", "in", "int", self::LIMIT_ARTICLES],
"article" => ["id", "=", "int", 1],
"articles" => ["id", "in", "int", self::LIMIT_ARTICLES],
"oldestArticle" => ["id", ">=", "int", 1],
"latestArticle" => ["id", "<=", "int", 1],
"oldestEdition" => ["edition", ">=", "int", 1],
"latestEdition" => ["edition", "<=", "int", 1],
"modifiedSince" => ["modified_date", ">=", "datetime", 1],
"notModifiedSince" => ["modified_date", "<=", "datetime", 1],
"markedSince" => ["marked_date", ">=", "datetime", 1],
"notMarkedSince" => ["marked_date", "<=", "datetime", 1],
"folderShallow" => ["folder", "=", "int", 1],
"subscription" => ["subscription", "=", "int", 1],
"unread" => ["unread", "=", "bool", 1],
"starred" => ["starred", "=", "bool", 1],
// each context array consists of a column identifier (see $colDefs above), a comparison operator, a data type, an option to pair with for BETWEEN evaluation, and an upper bound if the value is an array
"edition" => ["edition", "=", "int", "", 1],
"editions" => ["edition", "in", "int", "", self::LIMIT_ARTICLES],
"article" => ["id", "=", "int", "", 1],
"articles" => ["id", "in", "int", "", self::LIMIT_ARTICLES],
"oldestArticle" => ["id", ">=", "int", "latestArticle", 1],
"latestArticle" => ["id", "<=", "int", "oldestArticle", 1],
"oldestEdition" => ["edition", ">=", "int", "latestEdition", 1],
"latestEdition" => ["edition", "<=", "int", "oldestEdition", 1],
"modifiedSince" => ["modified_date", ">=", "datetime", "notModifiedSince", 1],
"notModifiedSince" => ["modified_date", "<=", "datetime", "modifiedSince", 1],
"markedSince" => ["marked_date", ">=", "datetime", "notMarkedSince", 1],
"notMarkedSince" => ["marked_date", "<=", "datetime", "markedSince", 1],
"folderShallow" => ["folder", "=", "int", "", 1],
"subscription" => ["subscription", "=", "int", "", 1],
"unread" => ["unread", "=", "bool", "", 1],
"starred" => ["starred", "=", "bool", "", 1],
];
foreach ($options as $m => list($col, $op, $type, $max)) {
$optionsSeen = [];
foreach ($options as $m => list($col, $op, $type, $pair, $max)) {
if (!$context->$m()) {
// context is not being used
continue;
} elseif (is_array($context->$m)) {
// context option is an array of values
if (!$context->$m) {
throw new Db\ExceptionInput("tooShort", ['field' => $m, 'action' => $this->caller(), 'min' => 1]); // must have at least one array element
} elseif (sizeof($context->$m) > $max) {
@ -1212,27 +1214,42 @@ class Database {
}
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
$q->setWhere("{$colDefs[$col]} $op ($clause)", $types, $values);
} elseif ($pair && $context->$pair()) {
// option is paired with another which is also being used
if ($op === ">=") {
$q->setWhere("{$colDefs[$col]} BETWEEN ? AND ?", [$type, $type], [$context->$m, $context->$pair]);
} else {
// option has already been paired
continue;
}
} else {
$q->setWhere("{$colDefs[$col]} $op ?", $type, $context->$m);
}
}
if ($context->not != $excContext) {
// further handle exclusionary options if specified
foreach ($options as $m => list($col, $op, $type, $max)) {
if (!method_exists($context->not, $m) || !$context->not->$m()) {
// context option is not being used
// further handle exclusionary options if specified
foreach ($options as $m => list($col, $op, $type, $pair, $max)) {
if (!method_exists($context->not, $m) || !$context->not->$m()) {
// context option is not being used
continue;
} elseif (is_array($context->not->$m)) {
if (!$context->not->$m) {
// for exclusions we don't care if the array is empty
continue;
} elseif (is_array($context->not->$m)) {
if (!$context->not->$m) {
// for exclusions we don't care if the array is empty
} elseif (sizeof($context->not->$m) > $max) {
throw new Db\ExceptionInput("tooLong", ['field' => $m, 'action' => $this->caller(), 'max' => $max]); // @codeCoverageIgnore
}
list($clause, $types, $values) = $this->generateIn($context->$m, $type);
$q->setWhereNot("{$colDefs[$col]} $op ($clause)", $types, $values);
} elseif (sizeof($context->not->$m) > $max) {
throw new Db\ExceptionInput("tooLong", ['field' => "$m (not)", 'action' => $this->caller(), 'max' => $max]);
}
list($clause, $types, $values) = $this->generateIn($context->not->$m, $type);
$q->setWhereNot("{$colDefs[$col]} $op ($clause)", $types, $values);
} elseif ($pair && $context->not->$pair()) {
// option is paired with another which is also being used
if ($op === ">=") {
$q->setWhereNot("{$colDefs[$col]} BETWEEN ? AND ?", [$type, $type], [$context->not->$m, $context->not->$pair]);
} else {
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->$m);
// option has already been paired
continue;
}
} else {
$q->setWhereNot("{$colDefs[$col]} $op ?", $type, $context->not->$m);
}
}
// handle complex context options

3
lib/Misc/Query.php

@ -113,6 +113,9 @@ class Query {
$this->qWhere = [];
$this->tWhere = [];
$this->vWhere = [];
$this->qWhereNot = [];
$this->tWhereNot = [];
$this->vWhereNot = [];
$this->qJoin = [];
$this->tJoin = [];
$this->vJoin = [];

80
tests/cases/Database/SeriesArticle.php

@ -377,43 +377,6 @@ trait SeriesArticle {
unset($this->data, $this->matches, $this->fields, $this->checkTables, $this->user);
}
public function testRetrieveArticleIdsForEditions() {
$exp = [
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
6 => 6,
7 => 7,
8 => 8,
9 => 9,
10 => 10,
11 => 11,
12 => 12,
13 => 13,
14 => 14,
15 => 15,
16 => 16,
17 => 17,
18 => 18,
19 => 19,
20 => 20,
101 => 101,
102 => 102,
103 => 103,
104 => 104,
105 => 105,
202 => 102,
203 => 103,
204 => 104,
205 => 105,
305 => 105,
1001 => 20,
];
$this->assertEquals($exp, Arsse::$db->editionArticle(...range(1, 1001)));
}
/** @dataProvider provideContextMatches */
public function testListArticlesCheckingContext(Context $c, array $exp) {
$ids = array_column($ids = Arsse::$db->articleList("john.doe@example.com", $c)->getAll(), "id");
@ -454,6 +417,8 @@ trait SeriesArticle {
"Marked or labelled since 2010" => [(new Context)->markedSince("2010-01-01T00:00:00Z"), [2,4,6,8,19,20]],
"Not marked or labelled since 2014" => [(new Context)->notMarkedSince("2014-01-01T00:00:00Z"), [1,2,3,4,5,6,7,20]],
"Not marked or labelled since 2005" => [(new Context)->notMarkedSince("2005-01-01T00:00:00Z"), [1,3,5,7]],
"Marked or labelled between 2000 and 2015" => [(new Context)->markedSince("2000-01-01T00:00:00Z")->notMarkedSince("2015-12-31T23:59:59Z"), [1,2,3,4,5,6,7,8,20]],
"Marked or labelled in 2010" => [(new Context)->markedSince("2010-01-01T00:00:00Z")->notMarkedSince("2010-12-31T23:59:59Z"), [2,4,6,20]],
"Paged results" => [(new Context)->limit(2)->oldestEdition(4), [4,5]],
"Reversed paged results" => [(new Context)->limit(2)->latestEdition(7)->reverse(true), [7,6]],
"With label ID 1" => [(new Context)->label(1), [1,19]],
@ -483,9 +448,50 @@ trait SeriesArticle {
"Search author 2" => [(new Context)->authorTerms(["jane doe"]), [6,7]],
"Search author 3" => [(new Context)->authorTerms(["doe", "jane"]), [6,7]],
"Search author 4" => [(new Context)->authorTerms(["doe jane"]), []],
"Folder tree 1 excluding subscription 4" => [(new Context)->not->subscription(4)->folder(1), [5,6]],
"Folder tree 1 excluding articles 7 and 8" => [(new Context)->folder(1)->not->articles([7,8]), [5,6]],
"Folder tree 1 excluding no articles" => [(new Context)->folder(1)->not->articles([]), [5,6,7,8]],
"Marked or labelled between 2000 and 2015 excluding in 2010" => [(new Context)->markedSince("2000-01-01T00:00:00Z")->notMarkedSince("2015-12-31T23:59:59")->not->markedSince("2010-01-01T00:00:00Z")->not->notMarkedSince("2010-12-31T23:59:59Z"), [1,3,5,7,8]],
];
}
public function testRetrieveArticleIdsForEditions() {
$exp = [
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
6 => 6,
7 => 7,
8 => 8,
9 => 9,
10 => 10,
11 => 11,
12 => 12,
13 => 13,
14 => 14,
15 => 15,
16 => 16,
17 => 17,
18 => 18,
19 => 19,
20 => 20,
101 => 101,
102 => 102,
103 => 103,
104 => 104,
105 => 105,
202 => 102,
203 => 103,
204 => 104,
205 => 105,
305 => 105,
1001 => 20,
];
$this->assertEquals($exp, Arsse::$db->editionArticle(...range(1, 1001)));
}
public function testListArticlesOfAMissingFolder() {
$this->assertException("idMissing", "Db", "ExceptionInput");
Arsse::$db->articleList($this->user, (new Context)->folder(1));

Loading…
Cancel
Save