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; const SCHEMA_VERSION = 4;
/** The maximum number of articles to mark in one query without chunking */ /** The maximum number of articles to mark in one query without chunking */
const LIMIT_ARTICLES = 50; 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 */ /** A map database driver short-names and their associated class names */
const DRIVER_NAMES = [ const DRIVER_NAMES = [
'sqlite3' => \JKingWeb\Arsse\Db\SQLite3\Driver::class, 'sqlite3' => \JKingWeb\Arsse\Db\SQLite3\Driver::class,
@ -149,6 +151,35 @@ class Database {
return $out; 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 */ /** Returns a Transaction object, which is rolled back unless explicitly committed */
public function begin(): Db\Transaction { public function begin(): Db\Transaction {
return $this->db->begin(); return $this->db->begin();
@ -1160,7 +1191,7 @@ class Database {
list($inParams, $inTypes) = $this->generateIn($context->editions, "int"); list($inParams, $inTypes) = $this->generateIn($context->editions, "int");
$q->setWhere("latest_editions.edition in ($inParams)", $inTypes, $context->editions); $q->setWhere("latest_editions.edition in ($inParams)", $inTypes, $context->editions);
} elseif ($context->articles()) { } 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) { if (!$context->articles) {
throw new Db\ExceptionInput("tooShort", ['field' => "articles", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element 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) { } elseif (sizeof($context->articles) > self::LIMIT_ARTICLES) {
@ -1221,6 +1252,15 @@ class Database {
$comp = ($context->annotated) ? "<>" : "="; $comp = ($context->annotated) ? "<>" : "=";
$q->setWhere("coalesce(arsse_marks.note,'') $comp ''"); $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 the query
return $q; return $q;
} }

19
tests/cases/Database/SeriesArticle.php

@ -111,9 +111,9 @@ trait SeriesArticle {
'modified' => "datetime", 'modified' => "datetime",
], ],
'rows' => [ 'rows' => [
[1,1,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,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"], [2,1,null,"Title two", null,null,null,"Second article",null,"","","","2010-01-01T00:00:00Z"],
[3,2,null,null,null,null,null,null,null,"","","","2000-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"], [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"], [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"], [6,3,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
@ -494,6 +494,9 @@ trait SeriesArticle {
// get specific starred articles // get specific starred articles
$compareIds([1], (new Context)->articles([1,2,3])->starred(true)); $compareIds([1], (new Context)->articles([1,2,3])->starred(true));
$compareIds([2,3], (new Context)->articles([1,2,3])->starred(false)); $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() { public function testListArticlesOfAMissingFolder() {
@ -985,4 +988,14 @@ trait SeriesArticle {
$this->assertException("notAuthorized", "User", "ExceptionAuthz"); $this->assertException("notAuthorized", "User", "ExceptionAuthz");
Arsse::$db->articleCategoriesGet($this->user, 19); 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