Browse Source

Basic substring searching

microsub
J. King 5 years ago
parent
commit
bc3182a961
  1. 42
      lib/Database.php
  2. 19
      tests/cases/Database/SeriesArticle.php

42
lib/Database.php

@ -39,6 +39,8 @@ class Database {
const SCHEMA_VERSION = 4;
/** The maximum number of articles to mark in one query without chunking */
const LIMIT_ARTICLES = 50;
/** The maximum number of search terms allowed; this is a hard limit */
const LIMIT_TERMS = 100;
/** A map database driver short-names and their associated class names */
const DRIVER_NAMES = [
'sqlite3' => \JKingWeb\Arsse\Db\SQLite3\Driver::class,
@ -149,6 +151,35 @@ class Database {
return $out;
}
/** Computes basic LIKE-based text search constraints for use in a WHERE clause
*
* Returns an indexed array containing the clause text, an array of types, and another array of values
*
* The clause is structured such that all terms must be present across any of the columns
*
* @param string[] $terms The terms to search for
* @param string[] $cols The columns to match against; these are -not- sanitized, so much -not- come directly from user input
*/
protected function generateSearch(array $terms, array $cols): array {
$clause = [];
$types = [];
$values = [];
$like = $this->db->sqlToken("like");
foreach($terms as $term) {
$term = str_replace(["%", "_", "^"], ["^%", "^_", "^^"], $term);
$term = "%$term%";
$spec = [];
foreach ($cols as $col) {
$spec[] = "$col $like ? escape '^'";
$types[] = "str";
$values[] = $term;
}
$clause[] = "(".implode(" or ", $spec).")";
}
$clause = "(".implode(" and ", $clause).")";
return [$clause, $types, $values];
}
/** Returns a Transaction object, which is rolled back unless explicitly committed */
public function begin(): Db\Transaction {
return $this->db->begin();
@ -1160,7 +1191,7 @@ class Database {
list($inParams, $inTypes) = $this->generateIn($context->editions, "int");
$q->setWhere("latest_editions.edition in ($inParams)", $inTypes, $context->editions);
} elseif ($context->articles()) {
// if multiple specific articles have been requested, prepare a CTE to list them and their articles
// if multiple specific articles have been requested, filter against the list
if (!$context->articles) {
throw new Db\ExceptionInput("tooShort", ['field' => "articles", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
} elseif (sizeof($context->articles) > self::LIMIT_ARTICLES) {
@ -1221,6 +1252,15 @@ class Database {
$comp = ($context->annotated) ? "<>" : "=";
$q->setWhere("coalesce(arsse_marks.note,'') $comp ''");
}
// filter based on search terms
if ($context->searchTerms()) {
if (!$context->searchTerms) {
throw new Db\ExceptionInput("tooShort", ['field' => "searchTerms", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
} elseif (sizeof($context->searchTerms) > self::LIMIT_TERMS) {
throw new Db\ExceptionInput("tooLong", ['field' => "searchTerms", 'action' => __FUNCTION__, 'max' => self::LIMIT_TERMS]);
}
$q->setWhere(...$this->generateSearch($context->searchTerms, ["arsse_articles.title", "arsse_articles.content"]));
}
// return the query
return $q;
}

19
tests/cases/Database/SeriesArticle.php

@ -111,9 +111,9 @@ trait SeriesArticle {
'modified' => "datetime",
],
'rows' => [
[1,1,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[2,1,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[3,2,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[1,1,null,"Title one", null,null,null,"First article", null,"","","","2000-01-01T00:00:00Z"],
[2,1,null,"Title two", null,null,null,"Second article",null,"","","","2010-01-01T00:00:00Z"],
[3,2,null,"Title three",null,null,null,"Third article", null,"","","","2000-01-01T00:00:00Z"],
[4,2,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
[5,3,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
[6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
@ -494,6 +494,9 @@ trait SeriesArticle {
// get specific starred articles
$compareIds([1], (new Context)->articles([1,2,3])->starred(true));
$compareIds([2,3], (new Context)->articles([1,2,3])->starred(false));
// get items that match search terms
$compareIds([1,2,3], (new Context)->searchTerms(["Article"]));
$compareIds([1], (new Context)->searchTerms(["one", "first"]));
}
public function testListArticlesOfAMissingFolder() {
@ -985,4 +988,14 @@ trait SeriesArticle {
$this->assertException("notAuthorized", "User", "ExceptionAuthz");
Arsse::$db->articleCategoriesGet($this->user, 19);
}
public function testSearchTooFewTerms() {
$this->assertException("tooShort", "Db", "ExceptionInput");
Arsse::$db->articleList($this->user, (new Context)->searchTerms([]));
}
public function testSearchTooManyTerms() {
$this->assertException("tooLong", "Db", "ExceptionInput");
Arsse::$db->articleList($this->user, (new Context)->searchTerms(range(1, 105)));
}
}

Loading…
Cancel
Save