mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Merge branch 'master' into mysql
This commit is contained in:
commit
86c16d3cb3
129 changed files with 4532 additions and 3253 deletions
|
@ -17,6 +17,7 @@ $paths = [
|
|||
$rules = [
|
||||
'@PSR2' => true,
|
||||
'braces' => ['position_after_functions_and_oop_constructs' => "same"],
|
||||
'function_declaration' => ['closure_function_spacing' => "none"],
|
||||
];
|
||||
|
||||
$finder = \PhpCsFixer\Finder::create();
|
||||
|
|
12
CHANGELOG
12
CHANGELOG
|
@ -1,3 +1,15 @@
|
|||
Version 0.6.0 (????-??-??)
|
||||
==========================
|
||||
|
||||
New features:
|
||||
- Support for PostgreSQL databases
|
||||
|
||||
Bug fixes:
|
||||
- Use a general-purpose Unicode collation with SQLite databases
|
||||
|
||||
Changes:
|
||||
- Improve performance of common database queries by 80-90%
|
||||
|
||||
Version 0.5.1 (2018-11-10)
|
||||
==========================
|
||||
|
||||
|
|
10
README.md
10
README.md
|
@ -4,7 +4,7 @@ The Arsse is a news aggregator server which implements multiple synchronization
|
|||
|
||||
At present the software should be considered in an "alpha" state: though its core subsystems are covered by unit tests and should be free of major bugs, not everything has been rigorously tested. Additionally, many features one would expect from other similar software have yet to be implemented. Areas of future work include:
|
||||
|
||||
- Support for more database engines (PostgreSQL, MySQL, MariaDB)
|
||||
- Support for more database engines (MySQL, MariaDB)
|
||||
- Providing more sync protocols (Google Reader, Fever, others)
|
||||
- Better packaging and configuration samples
|
||||
|
||||
|
@ -16,7 +16,9 @@ The Arsse has the following requirements:
|
|||
- PHP 7.0.7 or later with the following extensions:
|
||||
- [intl](http://php.net/manual/en/book.intl.php), [json](http://php.net/manual/en/book.json.php), [hash](http://php.net/manual/en/book.hash.php), and [pcre](http://php.net/manual/en/book.pcre.php)
|
||||
- [dom](http://php.net/manual/en/book.dom.php), [simplexml](http://php.net/manual/en/book.simplexml.php), and [iconv](http://php.net/manual/en/book.iconv.php) (for picoFeed)
|
||||
- [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://ca1.php.net/manual/en/ref.pdo-sqlite.php)
|
||||
- Either of:
|
||||
- [sqlite3](http://php.net/manual/en/book.sqlite3.php) or [pdo_sqlite](http://ca1.php.net/manual/en/ref.pdo-sqlite.php) for SQLite databases
|
||||
- [pgsql](http://php.net/manual/en/book.pgsql.php) or [pdo_pgsql](http://ca1.php.net/manual/en/ref.pdo-pgsql.php) for PostgreSQL 9.1 or later databases
|
||||
- Privileges to create and run daemon processes on the server
|
||||
|
||||
## Installation
|
||||
|
@ -69,6 +71,10 @@ The Arsse is made available under the permissive MIT license. See the `LICENSE`
|
|||
|
||||
Please refer to `CONTRIBUTING.md` for guidelines on contributing code to The Arsse.
|
||||
|
||||
## Database compatibility notes
|
||||
|
||||
Functionally there is no reason to prefer either SQLite or PostgreSQL over the other. SQLite, however, is significantly simpler to set up in most cases, requiring only read and write access to a containing directory in order to function. On the other hand PostgreSQL may perform better than SQLite when serving hundreds of users or more, but this has not been tested.
|
||||
|
||||
## Protocol compatibility notes
|
||||
|
||||
### General
|
||||
|
|
36
RoboFile.php
36
RoboFile.php
|
@ -50,6 +50,21 @@ class RoboFile extends \Robo\Tasks {
|
|||
* recommended if debugging facilities are not otherwise needed.
|
||||
*/
|
||||
public function coverage(array $args): Result {
|
||||
// run tests with code coverage reporting enabled
|
||||
$exec = $this->findCoverageEngine();
|
||||
return $this->runTests($exec, "coverage", array_merge(["--coverage-html", self::BASE_TEST."coverage"], $args));
|
||||
}
|
||||
|
||||
/** Produces a code coverage report, with redundant tests
|
||||
*
|
||||
* Depending on the environment, some tests that normally provide
|
||||
* coverage may be skipped, while working alternatives are normally
|
||||
* suppressed for reasons of time. This coverage report will try to
|
||||
* run all tests which may cover code.
|
||||
*
|
||||
* See also help for the "coverage" task for more details.
|
||||
*/
|
||||
public function coverageFull(array $args): Result {
|
||||
// run tests with code coverage reporting enabled
|
||||
$exec = $this->findCoverageEngine();
|
||||
return $this->runTests($exec, "typical", array_merge(["--coverage-html", self::BASE_TEST."coverage"], $args));
|
||||
|
@ -66,13 +81,16 @@ class RoboFile extends \Robo\Tasks {
|
|||
}
|
||||
|
||||
protected function findCoverageEngine(): string {
|
||||
$null = null;
|
||||
$code = 0;
|
||||
exec("phpdbg --version", $null, $code);
|
||||
if (!$code) {
|
||||
return "phpdbg -qrr";
|
||||
if ($this->isWindows()) {
|
||||
$dbg = dirname(\PHP_BINARY)."\\phpdbg.exe";
|
||||
$dbg = file_exists($dbg) ? $dbg : "";
|
||||
} else {
|
||||
return "php";
|
||||
$dbg = `which phpdbg`;
|
||||
}
|
||||
if ($dbg) {
|
||||
return escapeshellarg($dbg)." -qrr";
|
||||
} else {
|
||||
return escapeshellarg(\PHP_BINARY);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,6 +106,9 @@ class RoboFile extends \Robo\Tasks {
|
|||
case "quick":
|
||||
$set = ["--exclude-group", "optional,slow"];
|
||||
break;
|
||||
case "coverage":
|
||||
$set = ["--exclude-group", "optional,coverageOptional"];
|
||||
break;
|
||||
case "full":
|
||||
$set = [];
|
||||
break;
|
||||
|
@ -96,9 +117,8 @@ class RoboFile extends \Robo\Tasks {
|
|||
}
|
||||
$execpath = realpath(self::BASE."vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit");
|
||||
$confpath = realpath(self::BASE_TEST."phpunit.xml");
|
||||
$blackhole = $this->isWindows() ? "nul" : "/dev/null";
|
||||
$this->taskServer(8000)->host("localhost")->dir(self::BASE_TEST."docroot")->rawArg("-n")->arg(self::BASE_TEST."server.php")->background()->run();
|
||||
return $this->taskExec($executor)->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->rawArg("2>$blackhole")->run();
|
||||
return $this->taskExec($executor)->arg($execpath)->option("-c", $confpath)->args(array_merge($set, $args))->run();
|
||||
}
|
||||
|
||||
/** Packages a given commit of the software into a release tarball
|
||||
|
|
|
@ -9,6 +9,12 @@ When upgrading between any two versions of The Arsse, the following are usually
|
|||
- If installing from source, update dependencies with `composer install -o --no-dev`
|
||||
|
||||
|
||||
Upgrading from 0.5.1 to 0.6.0
|
||||
=============================
|
||||
|
||||
- The database schema has changed from rev3 to rev4; if upgrading the database manually, apply the 3.sql file
|
||||
|
||||
|
||||
Upgrading from 0.2.1 to 0.3.0
|
||||
=============================
|
||||
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"JKingWeb\\Arsse\\Test\\": "tests/lib/"
|
||||
"JKingWeb\\Arsse\\Test\\": "tests/lib/",
|
||||
"JKingWeb\\Arsse\\TestCase\\": "tests/cases/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ abstract class AbstractException extends \Exception {
|
|||
"Db/Exception.fileUnwritable" => 10205,
|
||||
"Db/Exception.fileUncreatable" => 10206,
|
||||
"Db/Exception.fileCorrupt" => 10207,
|
||||
"Db/Exception.connectionFailure" => 10208,
|
||||
"Db/Exception.updateTooNew" => 10211,
|
||||
"Db/Exception.updateManual" => 10212,
|
||||
"Db/Exception.updateManualOnly" => 10213,
|
||||
|
|
|
@ -81,11 +81,16 @@ USAGE_TEXT;
|
|||
return $this->userManage($args);
|
||||
}
|
||||
} catch (AbstractException $e) {
|
||||
fwrite(STDERR, $e->getMessage().\PHP_EOL);
|
||||
$this->logError($e->getMessage());
|
||||
return $e->getCode();
|
||||
}
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
protected function logError(string $msg) {
|
||||
fwrite(STDERR,$msg.\PHP_EOL);
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
protected function getService(): Service {
|
||||
return new Service;
|
||||
|
|
22
lib/Conf.php
22
lib/Conf.php
|
@ -19,12 +19,30 @@ class Conf {
|
|||
public $dbDriver = Db\SQLite3\Driver::class;
|
||||
/** @var boolean Whether to attempt to automatically update the database when updated to a new version with schema changes */
|
||||
public $dbAutoUpdate = true;
|
||||
/** @var float Number of seconds to wait before returning a timeout error when connecting to a database (zero waits forever; not applicable to SQLite) */
|
||||
public $dbTimeoutConnect = 5.0;
|
||||
/** @var float Number of seconds to wait before returning a timeout error when executing a database operation (zero waits forever; not applicable to SQLite) */
|
||||
public $dbTimeoutExec = 0.0;
|
||||
/** @var string|null Full path and file name of SQLite database (if using SQLite) */
|
||||
public $dbSQLite3File = null;
|
||||
/** @var string Encryption key to use for SQLite database (if using a version of SQLite with SEE) */
|
||||
public $dbSQLite3Key = "";
|
||||
/** @var integer Number of seconds for SQLite to wait before returning a timeout error when writing to the database */
|
||||
public $dbSQLite3Timeout = 60;
|
||||
/** @var float Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */
|
||||
public $dbSQLite3Timeout = 60.0;
|
||||
/** @var string Host name, address, or socket path of PostgreSQL database server (if using PostgreSQL) */
|
||||
public $dbPostgreSQLHost = "";
|
||||
/** @var string Log-in user name for PostgreSQL database server (if using PostgreSQL) */
|
||||
public $dbPostgreSQLUser = "arsse";
|
||||
/** @var string Log-in password for PostgreSQL database server (if using PostgreSQL) */
|
||||
public $dbPostgreSQLPass = "";
|
||||
/** @var integer Listening port for PostgreSQL database server (if using PostgreSQL over TCP) */
|
||||
public $dbPostgreSQLPort = 5432;
|
||||
/** @var string Database name on PostgreSQL database server (if using PostgreSQL) */
|
||||
public $dbPostgreSQLDb = "arsse";
|
||||
/** @var string Schema name in PostgreSQL database (if using PostgreSQL) */
|
||||
public $dbPostgreSQLSchema = "";
|
||||
/** @var string Service file entry to use (if using PostgreSQL); if using a service entry all above parameters except schema are ignored */
|
||||
public $dbPostgreSQLService = "";
|
||||
|
||||
/** @var string Class of the user management driver in use (Internal by default) */
|
||||
public $userDriver = User\Internal\Driver::class;
|
||||
|
|
463
lib/Database.php
463
lib/Database.php
|
@ -7,19 +7,15 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\Arsse;
|
||||
|
||||
use JKingWeb\DrUUID\UUID;
|
||||
use JKingWeb\Arsse\Db\Statement;
|
||||
use JKingWeb\Arsse\Misc\Query;
|
||||
use JKingWeb\Arsse\Misc\Context;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||
|
||||
class Database {
|
||||
const SCHEMA_VERSION = 3;
|
||||
const SCHEMA_VERSION = 4;
|
||||
const LIMIT_ARTICLES = 50;
|
||||
// articleList verbosity levels
|
||||
const LIST_MINIMAL = 0; // only that metadata which is required for context matching
|
||||
const LIST_CONSERVATIVE = 1; // base metadata plus anything that is not potentially large text
|
||||
const LIST_TYPICAL = 2; // conservative, with the addition of content
|
||||
const LIST_FULL = 3; // all possible fields
|
||||
|
||||
/** @var Db\Driver */
|
||||
public $db;
|
||||
|
@ -84,13 +80,26 @@ class Database {
|
|||
|
||||
protected function generateIn(array $values, string $type): array {
|
||||
$out = [
|
||||
[], // query clause
|
||||
"", // query clause
|
||||
[], // binding types
|
||||
];
|
||||
// the query clause is just a series of question marks separated by commas
|
||||
$out[0] = implode(",", array_fill(0, sizeof($values), "?"));
|
||||
// the binding types are just a repetition of the supplied type
|
||||
$out[1] = array_fill(0, sizeof($values), $type);
|
||||
if (sizeof($values)) {
|
||||
// the query clause is just a series of question marks separated by commas
|
||||
$out[0] = implode(",", array_fill(0, sizeof($values), "?"));
|
||||
// the binding types are just a repetition of the supplied type
|
||||
$out[1] = array_fill(0, sizeof($values), $type);
|
||||
} else {
|
||||
// if the set is empty, some databases require a query which returns an empty set
|
||||
$standin = [
|
||||
'string' => "''",
|
||||
'binary' => "''",
|
||||
'datetime' => "''",
|
||||
'integer' => "1",
|
||||
'boolean' => "1",
|
||||
'float' => "1.0",
|
||||
][Statement::TYPES[$type] ?? "string"];
|
||||
$out[0] = "select $standin where 1 = 0";
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
@ -182,7 +191,7 @@ class Database {
|
|||
$id = UUID::mint()->hex;
|
||||
$expires = Date::add(Arsse::$conf->userSessionTimeout);
|
||||
// save the session to the database
|
||||
$this->db->prepare("INSERT INTO arsse_sessions(id,expires,user) values(?,?,?)", "str", "datetime", "str")->run($id, $expires, $user);
|
||||
$this->db->prepare("INSERT INTO arsse_sessions(id,expires,\"user\") values(?,?,?)", "str", "datetime", "str")->run($id, $expires, $user);
|
||||
// return the ID
|
||||
return $id;
|
||||
}
|
||||
|
@ -193,12 +202,12 @@ class Database {
|
|||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
// delete the session and report success.
|
||||
return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id = ? and user = ?", "str", "str")->run($id, $user)->changes();
|
||||
return (bool) $this->db->prepare("DELETE FROM arsse_sessions where id = ? and \"user\" = ?", "str", "str")->run($id, $user)->changes();
|
||||
}
|
||||
|
||||
public function sessionResume(string $id): array {
|
||||
$maxAge = Date::sub(Arsse::$conf->userSessionLifetime);
|
||||
$out = $this->db->prepare("SELECT id,created,expires,user from arsse_sessions where id = ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow();
|
||||
$out = $this->db->prepare("SELECT id,created,expires,\"user\" from arsse_sessions where id = ? and expires > CURRENT_TIMESTAMP and created > ?", "str", "datetime")->run($id, $maxAge)->getRow();
|
||||
// if the session does not exist or is expired, throw an exception
|
||||
if (!$out) {
|
||||
throw new User\ExceptionSession("invalid", $id);
|
||||
|
@ -371,13 +380,13 @@ class Database {
|
|||
// SQL will happily accept duplicates (null is not unique), so we must do this check ourselves
|
||||
$p = $this->db->prepare(
|
||||
"WITH RECURSIVE
|
||||
target as (select ? as user, ? as source, ? as dest, ? as rename),
|
||||
folders as (SELECT id from arsse_folders join target on owner = user and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id)
|
||||
target as (select ? as userid, ? as source, ? as dest, ? as rename),
|
||||
folders as (SELECT id from arsse_folders join target on owner = userid and coalesce(parent,0) = source union select arsse_folders.id as id from arsse_folders join folders on arsse_folders.parent=folders.id)
|
||||
".
|
||||
"SELECT
|
||||
((select dest from target) is null or exists(select id from arsse_folders join target on owner = user and coalesce(id,0) = coalesce(dest,0))) as extant,
|
||||
not exists(select id from folders where id = coalesce((select dest from target),0)) as valid,
|
||||
not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) as available
|
||||
case when ((select dest from target) is null or exists(select id from arsse_folders join target on owner = userid and coalesce(id,0) = coalesce(dest,0))) then 1 else 0 end as extant,
|
||||
case when not exists(select id from folders where id = coalesce((select dest from target),0)) then 1 else 0 end as valid,
|
||||
case when not exists(select id from arsse_folders join target on coalesce(parent,0) = coalesce(dest,0) and name = coalesce((select rename from target),(select name from arsse_folders join target on id = source))) then 1 else 0 end as available
|
||||
",
|
||||
"str",
|
||||
"strict int",
|
||||
|
@ -409,7 +418,7 @@ class Database {
|
|||
// make sure that a folder with the same prospective name and parent does not already exist: if the parent is null,
|
||||
// SQL will happily accept duplicates (null is not unique), so we must do this check ourselves
|
||||
$parent = $parent ? $parent : null;
|
||||
if ($this->db->prepare("SELECT exists(select id from arsse_folders where coalesce(parent,0) = ? and name = ?)", "strict int", "str")->run($parent, $name)->getValue()) {
|
||||
if ($this->db->prepare("SELECT count(*) from arsse_folders where coalesce(parent,0) = ? and name = ?", "strict int", "str")->run($parent, $name)->getValue()) {
|
||||
throw new Db\ExceptionInput("constraintViolation", ["action" => $this->caller(), "field" => "name"]);
|
||||
}
|
||||
return true;
|
||||
|
@ -462,15 +471,16 @@ class Database {
|
|||
coalesce(arsse_subscriptions.title, arsse_feeds.title) as title,
|
||||
(SELECT count(*) from arsse_articles where feed = arsse_subscriptions.feed) - (SELECT count(*) from arsse_marks where subscription = arsse_subscriptions.id and read = 1) as unread
|
||||
from arsse_subscriptions
|
||||
join user on user = owner
|
||||
join userdata on userid = owner
|
||||
join arsse_feeds on feed = arsse_feeds.id
|
||||
left join topmost on folder=f_id"
|
||||
);
|
||||
$q->setOrder("pinned desc, title collate nocase");
|
||||
$nocase = $this->db->sqlToken("nocase");
|
||||
$q->setOrder("pinned desc, coalesce(arsse_subscriptions.title, arsse_feeds.title) collate $nocase");
|
||||
// define common table expressions
|
||||
$q->setCTE("user(user)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once
|
||||
$q->setCTE("userdata(userid)", "SELECT ?", "str", $user); // the subject user; this way we only have to pass it to prepare() once
|
||||
// topmost folders belonging to the user
|
||||
$q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join user on owner = user where parent is null union select id,top from arsse_folders join topmost on parent=f_id");
|
||||
$q->setCTE("topmost(f_id,top)", "SELECT id,id from arsse_folders join userdata on owner = userid where parent is null union select id,top from arsse_folders join topmost on parent=f_id");
|
||||
if ($id) {
|
||||
// this condition facilitates the implementation of subscriptionPropertiesGet, which would otherwise have to duplicate the complex query; it takes precedence over a specified folder
|
||||
// if an ID is specified, add a suitable WHERE condition and bindings
|
||||
|
@ -795,73 +805,102 @@ class Database {
|
|||
)->run($feedID, $ids, $hashesUT, $hashesUC, $hashesTC);
|
||||
}
|
||||
|
||||
protected function articleQuery(string $user, Context $context, array $extraColumns = []): Query {
|
||||
$extraColumns = implode(",", $extraColumns);
|
||||
if (strlen($extraColumns)) {
|
||||
$extraColumns .= ",";
|
||||
protected function articleQuery(string $user, Context $context, array $cols = ["id"]): Query {
|
||||
$greatest = $this->db->sqlToken("greatest");
|
||||
// prepare the output column list
|
||||
$colDefs = [
|
||||
'id' => "arsse_articles.id",
|
||||
'edition' => "latest_editions.edition",
|
||||
'url' => "arsse_articles.url",
|
||||
'title' => "arsse_articles.title",
|
||||
'author' => "arsse_articles.author",
|
||||
'content' => "arsse_articles.content",
|
||||
'guid' => "arsse_articles.guid",
|
||||
'fingerprint' => "arsse_articles.url_title_hash || ':' || arsse_articles.url_content_hash || ':' || arsse_articles.title_content_hash",
|
||||
'subscription' => "arsse_subscriptions.id",
|
||||
'feed' => "arsse_subscriptions.feed",
|
||||
'starred' => "coalesce(arsse_marks.starred,0)",
|
||||
'unread' => "abs(coalesce(arsse_marks.read,0) - 1)",
|
||||
'note' => "coalesce(arsse_marks.note,'')",
|
||||
'published_date' => "arsse_articles.published",
|
||||
'edited_date' => "arsse_articles.edited",
|
||||
'modified_date' => "arsse_articles.modified",
|
||||
'marked_date' => "$greatest(arsse_articles.modified, coalesce(arsse_marks.modified, '0001-01-01 00:00:00'), coalesce(arsse_label_members.modified, '0001-01-01 00:00:00'))",
|
||||
'subscription_title' => "coalesce(arsse_subscriptions.title, arsse_feeds.title)",
|
||||
'media_url' => "arsse_enclosures.url",
|
||||
'media_type' => "arsse_enclosures.type",
|
||||
|
||||
];
|
||||
if (!$cols) {
|
||||
// if no columns are specified return a count
|
||||
$columns = "count(distinct arsse_articles.id) as count";
|
||||
} else {
|
||||
$columns = [];
|
||||
foreach ($cols as $col) {
|
||||
$col = trim(strtolower($col));
|
||||
if (!isset($colDefs[$col])) {
|
||||
continue;
|
||||
}
|
||||
$columns[] = $colDefs[$col]." as ".$col;
|
||||
}
|
||||
$columns = implode(",", $columns);
|
||||
}
|
||||
// define the basic query, to which we add lots of stuff where necessary
|
||||
$q = new Query(
|
||||
"SELECT
|
||||
$extraColumns
|
||||
arsse_articles.id as id,
|
||||
arsse_articles.feed as feed,
|
||||
arsse_articles.modified as modified_date,
|
||||
max(
|
||||
arsse_articles.modified,
|
||||
coalesce((select modified from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),''),
|
||||
coalesce((select modified from arsse_label_members where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),'')
|
||||
) as marked_date,
|
||||
NOT (select count(*) from arsse_marks where article = arsse_articles.id and read = 1 and subscription in (select sub from subscribed_feeds)) as unread,
|
||||
(select count(*) from arsse_marks where article = arsse_articles.id and starred = 1 and subscription in (select sub from subscribed_feeds)) as starred,
|
||||
(select max(id) from arsse_editions where article = arsse_articles.id) as edition,
|
||||
subscribed_feeds.sub as subscription
|
||||
FROM arsse_articles"
|
||||
$columns
|
||||
from arsse_articles
|
||||
join arsse_subscriptions on arsse_subscriptions.feed = arsse_articles.feed and arsse_subscriptions.owner = ?
|
||||
join arsse_feeds on arsse_subscriptions.feed = arsse_feeds.id
|
||||
left join arsse_marks on arsse_marks.subscription = arsse_subscriptions.id and arsse_marks.article = arsse_articles.id
|
||||
left join arsse_enclosures on arsse_enclosures.article = arsse_articles.id
|
||||
left join arsse_label_members on arsse_label_members.subscription = arsse_subscriptions.id and arsse_label_members.article = arsse_articles.id and arsse_label_members.assigned = 1
|
||||
left join arsse_labels on arsse_labels.owner = arsse_subscriptions.owner and arsse_label_members.label = arsse_labels.id",
|
||||
["str"],
|
||||
[$user]
|
||||
);
|
||||
$q->setCTE("latest_editions(article,edition)", "SELECT article,max(id) from arsse_editions group by article", [], [], "join latest_editions on arsse_articles.id = latest_editions.article");
|
||||
if ($cols) {
|
||||
// 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");
|
||||
}
|
||||
$q->setLimit($context->limit, $context->offset);
|
||||
$q->setCTE("user(user)", "SELECT ?", "str", $user);
|
||||
if ($context->subscription()) {
|
||||
// if a subscription is specified, make sure it exists
|
||||
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
||||
// add a basic CTE that will join in only the requested subscription
|
||||
$q->setCTE("subscribed_feeds(id,sub)", "SELECT ?,?", ["int","int"], [$id,$context->subscription], "join subscribed_feeds on feed = subscribed_feeds.id");
|
||||
$this->subscriptionValidateId($user, $context->subscription);
|
||||
// filter for the subscription
|
||||
$q->setWhere("arsse_subscriptions.id = ?", "int", $context->subscription);
|
||||
} elseif ($context->folder()) {
|
||||
// if a folder is specified, make sure it exists
|
||||
$this->folderValidateId($user, $context->folder);
|
||||
// if it does exist, 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 select id from arsse_folders join folders on parent = folder", "int", $context->folder);
|
||||
// add another CTE for the subscriptions within the folder
|
||||
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner join folders on arsse_subscriptions.folder = folders.folder", [], [], "join subscribed_feeds on feed = subscribed_feeds.id");
|
||||
// limit subscriptions to the listed folders
|
||||
$q->setWhere("arsse_subscriptions.folder in (select folder from folders)");
|
||||
} elseif ($context->folderShallow()) {
|
||||
// if a shallow folder is specified, make sure it exists
|
||||
$this->folderValidateId($user, $context->folderShallow);
|
||||
// if it does exist, add a CTE with only its subscriptions (and not those of its descendents)
|
||||
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner and coalesce(folder,0) = ?", "strict int", $context->folderShallow, "join subscribed_feeds on feed = subscribed_feeds.id");
|
||||
} else {
|
||||
// otherwise add a CTE for all the user's subscriptions
|
||||
$q->setCTE("subscribed_feeds(id,sub)", "SELECT feed,id from arsse_subscriptions join user on user = owner", [], [], "join subscribed_feeds on feed = subscribed_feeds.id");
|
||||
// if it does exist, filter for that folder only
|
||||
$q->setWhere("coalesce(arsse_subscriptions.folder,0) = ?", "int", $context->folderShallow);
|
||||
}
|
||||
if ($context->edition()) {
|
||||
// if an edition is specified, filter for its previously identified article
|
||||
$q->setWhere("arsse_articles.id = (select article from arsse_editions where id = ?)", "int", $context->edition);
|
||||
// if an edition is specified, first validate it, then filter for it
|
||||
$this->articleValidateEdition($user, $context->edition);
|
||||
$q->setWhere("latest_editions.edition = ?", "int", $context->edition);
|
||||
} elseif ($context->article()) {
|
||||
// if an article is specified, filter for it (it has already been validated above)
|
||||
// if an article is specified, first validate it, then filter for it
|
||||
$this->articleValidateId($user, $context->article);
|
||||
$q->setWhere("arsse_articles.id = ?", "int", $context->article);
|
||||
}
|
||||
if ($context->editions()) {
|
||||
// if multiple specific editions have been requested, prepare a CTE to list them and their articles
|
||||
// if multiple specific editions have been requested, filter against the list
|
||||
if (!$context->editions) {
|
||||
throw new Db\ExceptionInput("tooShort", ['field' => "editions", 'action' => __FUNCTION__, 'min' => 1]); // must have at least one array element
|
||||
} elseif (sizeof($context->editions) > self::LIMIT_ARTICLES) {
|
||||
throw new Db\ExceptionInput("tooLong", ['field' => "editions", 'action' => __FUNCTION__, 'max' => self::LIMIT_ARTICLES]); // @codeCoverageIgnore
|
||||
}
|
||||
list($inParams, $inTypes) = $this->generateIn($context->editions, "int");
|
||||
$q->setCTE(
|
||||
"requested_articles(id,edition)",
|
||||
"SELECT article,id as edition from arsse_editions where edition in ($inParams)",
|
||||
$inTypes,
|
||||
$context->editions
|
||||
);
|
||||
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
|
||||
$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 (!$context->articles) {
|
||||
|
@ -870,21 +909,13 @@ class Database {
|
|||
throw new Db\ExceptionInput("tooLong", ['field' => "articles", 'action' => __FUNCTION__, 'max' => self::LIMIT_ARTICLES]); // @codeCoverageIgnore
|
||||
}
|
||||
list($inParams, $inTypes) = $this->generateIn($context->articles, "int");
|
||||
$q->setCTE(
|
||||
"requested_articles(id,edition)",
|
||||
"SELECT id,(select max(id) from arsse_editions where article = arsse_articles.id) as edition from arsse_articles where arsse_articles.id in ($inParams)",
|
||||
$inTypes,
|
||||
$context->articles
|
||||
);
|
||||
$q->setWhere("arsse_articles.id in (select id from requested_articles)");
|
||||
} else {
|
||||
// if neither list is specified, mock an empty table
|
||||
$q->setCTE("requested_articles(id,edition)", "SELECT 'empty','table' where 1 = 0");
|
||||
$q->setWhere("arsse_articles.id in ($inParams)", $inTypes, $context->articles);
|
||||
}
|
||||
// filter based on label by ID or name
|
||||
if ($context->labelled()) {
|
||||
// any label (true) or no label (false)
|
||||
$q->setWhere((!$context->labelled ? "not " : "")."exists(select article from arsse_label_members where assigned = 1 and article = arsse_articles.id and subscription in (select sub from subscribed_feeds))");
|
||||
$isOrIsNot = (!$context->labelled ? "is" : "is not");
|
||||
$q->setWhere("arsse_labels.id $isOrIsNot null");
|
||||
} elseif ($context->label() || $context->labelName()) {
|
||||
// specific label ID or name
|
||||
if ($context->label()) {
|
||||
|
@ -892,7 +923,7 @@ class Database {
|
|||
} else {
|
||||
$id = $this->labelValidateId($user, $context->labelName, true)['id'];
|
||||
}
|
||||
$q->setWhere("exists(select article from arsse_label_members where assigned = 1 and article = arsse_articles.id and label = ?)", "int", $id);
|
||||
$q->setWhere("arsse_labels.id = ?", "int", $id);
|
||||
}
|
||||
// filter based on article or edition offset
|
||||
if ($context->oldestArticle()) {
|
||||
|
@ -902,40 +933,41 @@ class Database {
|
|||
$q->setWhere("arsse_articles.id <= ?", "int", $context->latestArticle);
|
||||
}
|
||||
if ($context->oldestEdition()) {
|
||||
$q->setWhere("edition >= ?", "int", $context->oldestEdition);
|
||||
$q->setWhere("latest_editions.edition >= ?", "int", $context->oldestEdition);
|
||||
}
|
||||
if ($context->latestEdition()) {
|
||||
$q->setWhere("edition <= ?", "int", $context->latestEdition);
|
||||
$q->setWhere("latest_editions.edition <= ?", "int", $context->latestEdition);
|
||||
}
|
||||
// filter based on time at which an article was changed by feed updates (modified), or by user action (marked)
|
||||
if ($context->modifiedSince()) {
|
||||
$q->setWhere("modified_date >= ?", "datetime", $context->modifiedSince);
|
||||
$q->setWhere("arsse_articles.modified >= ?", "datetime", $context->modifiedSince);
|
||||
}
|
||||
if ($context->notModifiedSince()) {
|
||||
$q->setWhere("modified_date <= ?", "datetime", $context->notModifiedSince);
|
||||
$q->setWhere("arsse_articles.modified <= ?", "datetime", $context->notModifiedSince);
|
||||
}
|
||||
if ($context->markedSince()) {
|
||||
$q->setWhere("marked_date >= ?", "datetime", $context->markedSince);
|
||||
$q->setWhere($colDefs['marked_date']." >= ?", "datetime", $context->markedSince);
|
||||
}
|
||||
if ($context->notMarkedSince()) {
|
||||
$q->setWhere("marked_date <= ?", "datetime", $context->notMarkedSince);
|
||||
$q->setWhere($colDefs['marked_date']." <= ?", "datetime", $context->notMarkedSince);
|
||||
}
|
||||
// filter for un/read and un/starred status if specified
|
||||
if ($context->unread()) {
|
||||
$q->setWhere("unread = ?", "bool", $context->unread);
|
||||
$q->setWhere("coalesce(arsse_marks.read,0) = ?", "bool", !$context->unread);
|
||||
}
|
||||
if ($context->starred()) {
|
||||
$q->setWhere("starred = ?", "bool", $context->starred);
|
||||
$q->setWhere("coalesce(arsse_marks.starred,0) = ?", "bool", $context->starred);
|
||||
}
|
||||
// filter based on whether the article has a note
|
||||
if ($context->annotated()) {
|
||||
$q->setWhere((!$context->annotated ? "not " : "")."exists(select modified from arsse_marks where article = arsse_articles.id and note <> '' and subscription in (select sub from subscribed_feeds))");
|
||||
$comp = ($context->annotated) ? "<>" : "=";
|
||||
$q->setWhere("coalesce(arsse_marks.note,'') $comp ''");
|
||||
}
|
||||
// return the query
|
||||
return $q;
|
||||
}
|
||||
|
||||
protected function articleChunk(Context $context): array {
|
||||
protected function contextChunk(Context $context): array {
|
||||
$exception = "";
|
||||
if ($context->editions()) {
|
||||
// editions take precedence over articles
|
||||
|
@ -959,13 +991,13 @@ class Database {
|
|||
}
|
||||
}
|
||||
|
||||
public function articleList(string $user, Context $context = null, int $fields = self::LIST_FULL): Db\Result {
|
||||
public function articleList(string $user, Context $context = null, array $fields = ["id"]): Db\Result {
|
||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
$context = $context ?? new Context;
|
||||
// if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result
|
||||
if ($contexts = $this->articleChunk($context)) {
|
||||
if ($contexts = $this->contextChunk($context)) {
|
||||
$out = [];
|
||||
$tr = $this->begin();
|
||||
foreach ($contexts as $context) {
|
||||
|
@ -974,46 +1006,9 @@ class Database {
|
|||
$tr->commit();
|
||||
return new Db\ResultAggregate(...$out);
|
||||
} else {
|
||||
$columns = [];
|
||||
switch ($fields) {
|
||||
// NOTE: the cases all cascade into each other: a given verbosity level is always a superset of the previous one
|
||||
case self::LIST_FULL: // everything
|
||||
$columns = array_merge($columns, [
|
||||
"(select note from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)) as note",
|
||||
]);
|
||||
// no break
|
||||
case self::LIST_TYPICAL: // conservative, plus content
|
||||
$columns = array_merge($columns, [
|
||||
"content",
|
||||
"arsse_enclosures.url as media_url", // enclosures are potentially large due to data: URLs
|
||||
"arsse_enclosures.type as media_type", // FIXME: enclosures should eventually have their own fetch method
|
||||
]);
|
||||
// no break
|
||||
case self::LIST_CONSERVATIVE: // base metadata, plus anything that is not likely to be large text
|
||||
$columns = array_merge($columns, [
|
||||
"arsse_articles.url as url",
|
||||
"arsse_articles.title as title",
|
||||
"(select coalesce(arsse_subscriptions.title,arsse_feeds.title) from arsse_feeds join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id where arsse_feeds.id = arsse_articles.feed) as subscription_title",
|
||||
"author",
|
||||
"guid",
|
||||
"published as published_date",
|
||||
"edited as edited_date",
|
||||
"url_title_hash||':'||url_content_hash||':'||title_content_hash as fingerprint",
|
||||
]);
|
||||
// no break
|
||||
case self::LIST_MINIMAL: // base metadata (always included: required for context matching)
|
||||
$columns = array_merge($columns, [
|
||||
// id, subscription, feed, modified_date, marked_date, unread, starred, edition
|
||||
"edited as edited_date",
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("constantUnknown", $fields);
|
||||
}
|
||||
$q = $this->articleQuery($user, $context, $columns);
|
||||
$q->setOrder("edited_date".($context->reverse ? " desc" : ""));
|
||||
$q->setOrder("edition".($context->reverse ? " desc" : ""));
|
||||
$q->setJoin("left join arsse_enclosures on arsse_enclosures.article = arsse_articles.id");
|
||||
$q = $this->articleQuery($user, $context, $fields);
|
||||
$q->setOrder("arsse_articles.edited".($context->reverse ? " desc" : ""));
|
||||
$q->setOrder("latest_editions.edition".($context->reverse ? " desc" : ""));
|
||||
// perform the query and return results
|
||||
return $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
||||
}
|
||||
|
@ -1025,7 +1020,7 @@ class Database {
|
|||
}
|
||||
$context = $context ?? new Context;
|
||||
// if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result
|
||||
if ($contexts = $this->articleChunk($context)) {
|
||||
if ($contexts = $this->contextChunk($context)) {
|
||||
$out = 0;
|
||||
$tr = $this->begin();
|
||||
foreach ($contexts as $context) {
|
||||
|
@ -1034,9 +1029,7 @@ class Database {
|
|||
$tr->commit();
|
||||
return $out;
|
||||
} else {
|
||||
$q = $this->articleQuery($user, $context);
|
||||
$q->pushCTE("selected_articles");
|
||||
$q->setBody("SELECT count(*) from selected_articles");
|
||||
$q = $this->articleQuery($user, $context, []);
|
||||
return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
|
||||
}
|
||||
}
|
||||
|
@ -1045,9 +1038,17 @@ class Database {
|
|||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
$data = [
|
||||
'read' => $data['read'] ?? null,
|
||||
'starred' => $data['starred'] ?? null,
|
||||
'note' => $data['note'] ?? null,
|
||||
];
|
||||
if (!isset($data['read']) && !isset($data['starred']) && !isset($data['note'])) {
|
||||
return 0;
|
||||
}
|
||||
$context = $context ?? new Context;
|
||||
// if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result
|
||||
if ($contexts = $this->articleChunk($context)) {
|
||||
if ($contexts = $this->contextChunk($context)) {
|
||||
$out = 0;
|
||||
$tr = $this->begin();
|
||||
foreach ($contexts as $context) {
|
||||
|
@ -1056,63 +1057,69 @@ class Database {
|
|||
$tr->commit();
|
||||
return $out;
|
||||
} else {
|
||||
// sanitize input
|
||||
$values = [
|
||||
isset($data['read']) ? $data['read'] : null,
|
||||
isset($data['starred']) ? $data['starred'] : null,
|
||||
isset($data['note']) ? $data['note'] : null,
|
||||
];
|
||||
// the two queries we want to execute to make the requested changes
|
||||
$queries = [
|
||||
"UPDATE arsse_marks
|
||||
set
|
||||
read = case when (select honour_read from target_articles where target_articles.id = article) = 1 then (select read from target_values) else read end,
|
||||
starred = coalesce((select starred from target_values),starred),
|
||||
note = coalesce((select note from target_values),note),
|
||||
modified = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
subscription in (select sub from subscribed_feeds)
|
||||
and article in (select id from target_articles where to_insert = 0 and (honour_read = 1 or honour_star = 1 or (select note from target_values) is not null))",
|
||||
"INSERT INTO arsse_marks(subscription,article,read,starred,note)
|
||||
select
|
||||
(select id from arsse_subscriptions join user on user = owner where arsse_subscriptions.feed = target_articles.feed),
|
||||
id,
|
||||
coalesce((select read from target_values) * honour_read,0),
|
||||
coalesce((select starred from target_values),0),
|
||||
coalesce((select note from target_values),'')
|
||||
from target_articles where to_insert = 1 and (honour_read = 1 or honour_star = 1 or coalesce((select note from target_values),'') <> '')"
|
||||
];
|
||||
$out = 0;
|
||||
// wrap this UPDATE and INSERT together into a transaction
|
||||
$tr = $this->begin();
|
||||
// if an edition context is specified, make sure it's valid
|
||||
if ($context->edition()) {
|
||||
// make sure the edition exists
|
||||
$edition = $this->articleValidateEdition($user, $context->edition);
|
||||
// if the edition is not the latest, do not mark the read flag
|
||||
if (!$edition['current']) {
|
||||
$values[0] = null;
|
||||
$out = 0;
|
||||
if ($data['read'] || $data['starred'] || strlen($data['note'] ?? "")) {
|
||||
// first prepare a query to insert any missing marks rows for the articles we want to mark
|
||||
// but only insert new mark records if we're setting at least one "positive" mark
|
||||
$q = $this->articleQuery($user, $context, ["id", "subscription"]);
|
||||
$q->setWhere("arsse_marks.starred is null"); // null means there is no marks row for the article
|
||||
$q->pushCTE("missing_marks(article,subscription)");
|
||||
$q->setBody("INSERT INTO arsse_marks(article,subscription) SELECT article,subscription from missing_marks");
|
||||
$this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
||||
}
|
||||
if (isset($data['read']) && (isset($data['starred']) || isset($data['note'])) && ($context->edition() || $context->editions())) {
|
||||
// if marking by edition both read and something else, do separate marks for starred and note than for read
|
||||
// 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']);
|
||||
$this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues());
|
||||
// get the articles associated with the requested editions
|
||||
if ($context->edition()) {
|
||||
$context->article($this->articleValidateEdition($user, $context->edition)['article'])->edition(null);
|
||||
} else {
|
||||
$context->articles($this->editionArticle(...$context->editions))->editions(null);
|
||||
}
|
||||
} elseif ($context->article()) {
|
||||
// otherwise if an article context is specified, make sure it's valid
|
||||
$this->articleValidateId($user, $context->article);
|
||||
// set starred 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))", ["str", "bool"], [$data['note'], $data['starred']]);
|
||||
$q->pushCTE("target_articles(article,subscription)");
|
||||
$data = array_filter($data, function($v) {
|
||||
return isset($v);
|
||||
});
|
||||
list($set, $setTypes, $setValues) = $this->generateSet($data, ['starred' => "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);
|
||||
$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
|
||||
$out = $this->db->query("UPDATE arsse_marks set modified = CURRENT_TIMESTAMP, touched = 0 where touched = 1")->changes();
|
||||
} else {
|
||||
if (!isset($data['read']) && ($context->edition() || $context->editions())) {
|
||||
// get the articles associated with the requested editions
|
||||
if ($context->edition()) {
|
||||
$context->article($this->articleValidateEdition($user, $context->edition)['article'])->edition(null);
|
||||
} else {
|
||||
$context->articles($this->editionArticle(...$context->editions))->editions(null);
|
||||
}
|
||||
if (!$context->article && !$context->articles) {
|
||||
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))", ["str", "bool", "bool"], [$data['note'], $data['starred'], $data['read']]);
|
||||
$q->pushCTE("target_articles(article,subscription)");
|
||||
$data = array_filter($data, function($v) {
|
||||
return isset($v);
|
||||
});
|
||||
list($set, $setTypes, $setValues) = $this->generateSet($data, ['read' => "bool", 'starred' => "bool", 'note' => "str"]);
|
||||
$q->setBody("UPDATE arsse_marks set $set, modified = CURRENT_TIMESTAMP where article in(select article from target_articles) and subscription in(select distinct subscription from target_articles)", $setTypes, $setValues);
|
||||
$out = $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes();
|
||||
}
|
||||
// execute each query in sequence
|
||||
foreach ($queries as $query) {
|
||||
// first build the query which will select the target articles; we will later turn this into a CTE for the actual query that manipulates the articles
|
||||
$q = $this->articleQuery($user, $context, [
|
||||
"(not exists(select article from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds))) as to_insert",
|
||||
"((select read from target_values) is not null and (select read from target_values) <> (coalesce((select read from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),0)) and (not exists(select * from requested_articles) or (select max(id) from arsse_editions where article = arsse_articles.id) in (select edition from requested_articles))) as honour_read",
|
||||
"((select starred from target_values) is not null and (select starred from target_values) <> (coalesce((select starred from arsse_marks where article = arsse_articles.id and subscription in (select sub from subscribed_feeds)),0))) as honour_star",
|
||||
]);
|
||||
// common table expression with the values to set
|
||||
$q->setCTE("target_values(read,starred,note)", "SELECT ?,?,?", ["bool","bool","str"], $values);
|
||||
// push the current query onto the CTE stack and execute the query we're actually interested in
|
||||
$q->pushCTE("target_articles");
|
||||
$q->setBody($query);
|
||||
$out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes();
|
||||
}
|
||||
// commit the transaction
|
||||
$tr->commit();
|
||||
return $out;
|
||||
}
|
||||
|
@ -1125,11 +1132,11 @@ class Database {
|
|||
return $this->db->prepare(
|
||||
"SELECT
|
||||
count(*) as total,
|
||||
coalesce(sum(not read),0) as unread,
|
||||
coalesce(sum(abs(read - 1)),0) as unread,
|
||||
coalesce(sum(read),0) as read
|
||||
FROM (
|
||||
select read from arsse_marks where starred = 1 and subscription in (select id from arsse_subscriptions where owner = ?)
|
||||
)",
|
||||
) as starred_data",
|
||||
"str"
|
||||
)->run($user)->getRow();
|
||||
}
|
||||
|
@ -1140,12 +1147,10 @@ class Database {
|
|||
}
|
||||
$id = $this->articleValidateId($user, $id)['article'];
|
||||
$out = $this->db->prepare("SELECT id,name from arsse_labels where owner = ? and exists(select id from arsse_label_members where article = ? and label = arsse_labels.id and assigned = 1)", "str", "int")->run($user, $id)->getAll();
|
||||
if (!$out) {
|
||||
return $out;
|
||||
} else {
|
||||
// flatten the result to return just the label ID or name
|
||||
return array_column($out, !$byName ? "id" : "name");
|
||||
}
|
||||
// flatten the result to return just the label ID or name, sorted
|
||||
$out = $out ? array_column($out, !$byName ? "id" : "name") : [];
|
||||
sort($out);
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function articleCategoriesGet(string $user, $id): array {
|
||||
|
@ -1168,11 +1173,15 @@ class Database {
|
|||
"SELECT
|
||||
id, (select count(*) from arsse_subscriptions where feed = arsse_feeds.id) as subs
|
||||
from arsse_feeds where id = ?".
|
||||
"), latest_editions(article,edition) as (".
|
||||
"SELECT article,max(id) from arsse_editions group by article".
|
||||
"), excepted_articles(id,edition) as (".
|
||||
"SELECT
|
||||
arsse_articles.id, (select max(id) from arsse_editions where article = arsse_articles.id) as edition
|
||||
arsse_articles.id as id,
|
||||
latest_editions.edition as edition
|
||||
from arsse_articles
|
||||
join target_feed on arsse_articles.feed = target_feed.id
|
||||
join latest_editions on arsse_articles.id = latest_editions.article
|
||||
order by edition desc limit ?".
|
||||
") ".
|
||||
"DELETE from arsse_articles where
|
||||
|
@ -1240,14 +1249,14 @@ class Database {
|
|||
join arsse_feeds on arsse_feeds.id = arsse_articles.feed
|
||||
join arsse_subscriptions on arsse_subscriptions.feed = arsse_feeds.id
|
||||
WHERE
|
||||
edition = ? and arsse_subscriptions.owner = ?",
|
||||
arsse_editions.id = ? and arsse_subscriptions.owner = ?",
|
||||
"int",
|
||||
"str"
|
||||
)->run($id, $user)->getRow();
|
||||
if (!$out) {
|
||||
throw new Db\ExceptionInput("subjectMissing", ["action" => $this->caller(), "field" => "edition", 'id' => $id]);
|
||||
}
|
||||
return $out;
|
||||
return array_map("intval", $out);
|
||||
}
|
||||
|
||||
public function editionLatest(string $user, Context $context = null): int {
|
||||
|
@ -1255,19 +1264,35 @@ class Database {
|
|||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
$context = $context ?? new Context;
|
||||
$q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article = arsse_articles.id left join arsse_feeds on arsse_articles.feed = arsse_feeds.id");
|
||||
$q = new Query("SELECT max(arsse_editions.id) from arsse_editions left join arsse_articles on article = arsse_articles.id join arsse_subscriptions on arsse_articles.feed = arsse_subscriptions.feed and arsse_subscriptions.owner = ?", "str", $user);
|
||||
if ($context->subscription()) {
|
||||
// if a subscription is specified, make sure it exists
|
||||
$id = $this->subscriptionValidateId($user, $context->subscription)['feed'];
|
||||
$this->subscriptionValidateId($user, $context->subscription);
|
||||
// a simple WHERE clause is required here
|
||||
$q->setWhere("arsse_feeds.id = ?", "int", $id);
|
||||
} else {
|
||||
$q->setCTE("user(user)", "SELECT ?", "str", $user);
|
||||
$q->setCTE("feeds(feed)", "SELECT feed from arsse_subscriptions join user on user = owner", [], [], "join feeds on arsse_articles.feed = feeds.feed");
|
||||
$q->setWhere("arsse_subscriptions.id = ?", "int", $context->subscription);
|
||||
}
|
||||
return (int) $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->getValue();
|
||||
}
|
||||
|
||||
public function editionArticle(int ...$edition): array {
|
||||
$out = [];
|
||||
$context = (new Context)->editions($edition);
|
||||
// if the context has more articles or editions than we can process in one query, perform a series of queries and return an aggregate result
|
||||
if ($contexts = $this->contextChunk($context)) {
|
||||
$articles = $editions = [];
|
||||
foreach ($contexts as $context) {
|
||||
$out = $this->editionArticle(...$context->editions);
|
||||
$editions = array_merge($editions, array_map("intval", array_keys($out)));
|
||||
$articles = array_merge($articles, array_map("intval", array_values($out)));
|
||||
}
|
||||
return array_combine($editions, $articles);
|
||||
} else {
|
||||
list($in, $inTypes) = $this->generateIn($context->editions, "int");
|
||||
$out = $this->db->prepare("SELECT id as edition, article from arsse_editions where id in($in)", $inTypes)->run($context->editions)->getAll();
|
||||
return $out ? array_combine(array_column($out, "edition"), array_column($out, "article")) : [];
|
||||
}
|
||||
}
|
||||
|
||||
public function labelAdd(string $user, array $data): int {
|
||||
// if the user isn't authorized to perform this action then throw an exception.
|
||||
if (!Arsse::$user->authorize($user, __FUNCTION__)) {
|
||||
|
@ -1286,14 +1311,16 @@ class Database {
|
|||
throw new User\ExceptionAuthz("notAuthorized", ["action" => __FUNCTION__, "user" => $user]);
|
||||
}
|
||||
return $this->db->prepare(
|
||||
"SELECT
|
||||
id,name,
|
||||
(select count(*) from arsse_label_members where label = id and assigned = 1) as articles,
|
||||
(select count(*) from arsse_label_members
|
||||
join arsse_marks on arsse_label_members.article = arsse_marks.article and arsse_label_members.subscription = arsse_marks.subscription
|
||||
where label = id and assigned = 1 and read = 1
|
||||
) as read
|
||||
FROM arsse_labels where owner = ? and articles >= ? order by name
|
||||
"SELECT * FROM (
|
||||
SELECT
|
||||
id,name,
|
||||
(select count(*) from arsse_label_members where label = id and assigned = 1) as articles,
|
||||
(select count(*) from arsse_label_members
|
||||
join arsse_marks on arsse_label_members.article = arsse_marks.article and arsse_label_members.subscription = arsse_marks.subscription
|
||||
where label = id and assigned = 1 and read = 1
|
||||
) as read
|
||||
FROM arsse_labels where owner = ?) as label_data
|
||||
where articles >= ? order by name
|
||||
",
|
||||
"str",
|
||||
"int"
|
||||
|
@ -1373,7 +1400,7 @@ class Database {
|
|||
$this->labelValidateId($user, $id, $byName, false);
|
||||
$field = !$byName ? "id" : "name";
|
||||
$type = !$byName ? "int" : "str";
|
||||
$out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label = id where assigned = 1 and $field = ? and owner = ?", $type, "str")->run($id, $user)->getAll();
|
||||
$out = $this->db->prepare("SELECT article from arsse_label_members join arsse_labels on label = id where assigned = 1 and $field = ? and owner = ? order by article", $type, "str")->run($id, $user)->getAll();
|
||||
if (!$out) {
|
||||
// if no results were returned, do a full validation on the label ID
|
||||
$this->labelValidateId($user, $id, $byName, true, true);
|
||||
|
@ -1400,14 +1427,14 @@ class Database {
|
|||
$q->setWhere("exists(select article from arsse_label_members where label = ? and article = arsse_articles.id)", "int", $id);
|
||||
$q->pushCTE("target_articles");
|
||||
$q->setBody(
|
||||
"UPDATE arsse_label_members set assigned = ?, modified = CURRENT_TIMESTAMP where label = ? and assigned = not ? and article in (select id from target_articles)",
|
||||
"UPDATE arsse_label_members set assigned = ?, modified = CURRENT_TIMESTAMP where label = ? and assigned <> ? and article in (select id from target_articles)",
|
||||
["bool","int","bool"],
|
||||
[!$remove, $id, !$remove]
|
||||
);
|
||||
$out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes();
|
||||
// next, if we're not removing, add any new entries that need to be added
|
||||
if (!$remove) {
|
||||
$q = $this->articleQuery($user, $context);
|
||||
$q = $this->articleQuery($user, $context, ["id", "feed"]);
|
||||
$q->setWhere("not exists(select article from arsse_label_members where label = ? and article = arsse_articles.id)", "int", $id);
|
||||
$q->pushCTE("target_articles");
|
||||
$q->setBody(
|
||||
|
@ -1415,10 +1442,10 @@ class Database {
|
|||
arsse_label_members(label,article,subscription)
|
||||
SELECT
|
||||
?,id,
|
||||
(select id from arsse_subscriptions join user on user = owner where arsse_subscriptions.feed = target_articles.feed)
|
||||
(select id from arsse_subscriptions where owner = ? and arsse_subscriptions.feed = target_articles.feed)
|
||||
FROM target_articles",
|
||||
"int",
|
||||
$id
|
||||
["int", "str"],
|
||||
[$id, $user]
|
||||
);
|
||||
$out += $this->db->prepare($q->getQuery(), $q->getTypes())->run($q->getValues())->changes();
|
||||
}
|
||||
|
|
|
@ -13,18 +13,10 @@ abstract class AbstractDriver implements Driver {
|
|||
protected $transDepth = 0;
|
||||
protected $transStatus = [];
|
||||
|
||||
abstract protected function lock(): bool;
|
||||
abstract protected function unlock(bool $rollback = false): bool;
|
||||
abstract protected function getError(): string;
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public function schemaVersion(): int {
|
||||
// FIXME: generic schemaVersion() will need to be covered for database engines other than SQLite
|
||||
try {
|
||||
return (int) $this->query("SELECT value from arsse_meta where key is schema_version")->getValue();
|
||||
} catch (Exception $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function schemaUpdate(int $to, string $basePath = null): bool {
|
||||
$ver = $this->schemaVersion();
|
||||
if (!Arsse::$conf->dbAutoUpdate) {
|
||||
|
@ -78,50 +70,63 @@ abstract class AbstractDriver implements Driver {
|
|||
}
|
||||
|
||||
public function savepointCreate(bool $lock = false): int {
|
||||
// if no transaction is active and a lock was requested, lock the database using a backend-specific routine
|
||||
if ($lock && !$this->transDepth) {
|
||||
$this->lock();
|
||||
$this->locked = true;
|
||||
}
|
||||
// create a savepoint, incrementing the transaction depth
|
||||
$this->exec("SAVEPOINT arsse_".(++$this->transDepth));
|
||||
// set the state of the newly created savepoint to pending
|
||||
$this->transStatus[$this->transDepth] = self::TR_PEND;
|
||||
// return the depth number
|
||||
return $this->transDepth;
|
||||
}
|
||||
|
||||
public function savepointRelease(int $index = null): bool {
|
||||
// assume the most recent savepoint if none was specified
|
||||
$index = $index ?? $this->transDepth;
|
||||
if (array_key_exists($index, $this->transStatus)) {
|
||||
switch ($this->transStatus[$index]) {
|
||||
case self::TR_PEND:
|
||||
// release the requested savepoint and set its state to committed
|
||||
$this->exec("RELEASE SAVEPOINT arsse_".$index);
|
||||
$this->transStatus[$index] = self::TR_COMMIT;
|
||||
// for any later pending savepoints, set their state to implicitly committed
|
||||
$a = $index;
|
||||
while (++$a && $a <= $this->transDepth) {
|
||||
if ($this->transStatus[$a] <= self::TR_PEND) {
|
||||
$this->transStatus[$a] = self::TR_PEND_COMMIT;
|
||||
}
|
||||
}
|
||||
// return success
|
||||
$out = true;
|
||||
break;
|
||||
case self::TR_PEND_COMMIT:
|
||||
// set the state to explicitly committed
|
||||
$this->transStatus[$index] = self::TR_COMMIT;
|
||||
$out = true;
|
||||
break;
|
||||
case self::TR_PEND_ROLLBACK:
|
||||
// set the state to explicitly committed
|
||||
$this->transStatus[$index] = self::TR_COMMIT;
|
||||
$out = false;
|
||||
break;
|
||||
case self::TR_COMMIT:
|
||||
case self::TR_ROLLBACK: //@codeCoverageIgnore
|
||||
// savepoint has already been released or rolled back; this is an error
|
||||
throw new Exception("savepointStale", ['action' => "commit", 'index' => $index]);
|
||||
default:
|
||||
throw new Exception("savepointStatusUnknown", $this->transStatus[$index]); // @codeCoverageIgnore
|
||||
}
|
||||
if ($index==$this->transDepth) {
|
||||
// if we've released the topmost savepoint, clean up all prior savepoints which have already been explicitly committed (or rolled back), if any
|
||||
while ($this->transDepth > 0 && $this->transStatus[$this->transDepth] > self::TR_PEND) {
|
||||
array_pop($this->transStatus);
|
||||
$this->transDepth--;
|
||||
}
|
||||
}
|
||||
// if no savepoints are pending and the database was locked, unlock it
|
||||
if (!$this->transDepth && $this->locked) {
|
||||
$this->unlock();
|
||||
$this->locked = false;
|
||||
|
|
|
@ -74,20 +74,26 @@ abstract class AbstractStatement implements Statement {
|
|||
}
|
||||
}
|
||||
|
||||
protected function bindValues(array $values, int $offset = 0): int {
|
||||
$a = $offset;
|
||||
protected function bindValues(array $values, int $offset = null): int {
|
||||
$a = (int) $offset;
|
||||
foreach ($values as $value) {
|
||||
if (is_array($value)) {
|
||||
// recursively flatten any arrays, which may be provided for SET or IN() clauses
|
||||
$a += $this->bindValues($value, $a);
|
||||
} elseif (array_key_exists($a, $this->types)) {
|
||||
$value = $this->cast($value, $this->types[$a], $this->isNullable[$a]);
|
||||
$this->bindValue($value, $this->types[$a], $a+1);
|
||||
$a++;
|
||||
$this->bindValue($value, $this->types[$a], ++$a);
|
||||
} else {
|
||||
throw new Exception("paramTypeMissing", $a+1);
|
||||
}
|
||||
}
|
||||
// once the last value is bound, check that all parameters have been supplied values and bind null for any missing ones
|
||||
// SQLite will happily substitute null for a missing value, but other engines (viz. PostgreSQL) produce an error
|
||||
if (is_null($offset)) {
|
||||
while ($a < sizeof($this->types)) {
|
||||
$this->bindValue(null, $this->types[$a], ++$a);
|
||||
}
|
||||
}
|
||||
return $a - $offset;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,4 +39,6 @@ interface Driver {
|
|||
public function prepareArray(string $query, array $paramTypes): Statement;
|
||||
// report whether the database character set is correct/acceptable
|
||||
public function charsetAcceptable(): bool;
|
||||
// return an implementation-dependent form of a reference SQL function or operator
|
||||
public function sqlToken(string $token): string;
|
||||
}
|
||||
|
|
|
@ -26,13 +26,7 @@ trait PDODriver {
|
|||
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
||||
throw new $excClass($excMsg, $excData);
|
||||
}
|
||||
$changes = $r->rowCount();
|
||||
try {
|
||||
$lastId = 0;
|
||||
$lastId = $this->db->lastInsertId();
|
||||
} catch (\PDOException $e) { // @codeCoverageIgnore
|
||||
}
|
||||
return new PDOResult($r, [$changes, $lastId]);
|
||||
return new PDOResult($this->db, $r);
|
||||
}
|
||||
|
||||
public function prepareArray(string $query, array $paramTypes): Statement {
|
||||
|
|
|
@ -7,15 +7,23 @@ declare(strict_types=1);
|
|||
namespace JKingWeb\Arsse\Db;
|
||||
|
||||
trait PDOError {
|
||||
public function exceptionBuild(): array {
|
||||
if ($this instanceof Statement) {
|
||||
public function exceptionBuild(bool $statementError = null): array {
|
||||
if ($statementError ?? ($this instanceof Statement)) {
|
||||
$err = $this->st->errorInfo();
|
||||
} else {
|
||||
$err = $this->db->errorInfo();
|
||||
}
|
||||
switch ($err[0]) {
|
||||
case "22P02":
|
||||
case "42804":
|
||||
return [ExceptionInput::class, 'engineTypeViolation', $err[2]];
|
||||
case "23000":
|
||||
case "23502":
|
||||
case "23505":
|
||||
return [ExceptionInput::class, "constraintViolation", $err[2]];
|
||||
case "55P03":
|
||||
case "57014":
|
||||
return [ExceptionTimeout::class, 'general', $err[2]];
|
||||
case "HY000":
|
||||
// engine-specific errors
|
||||
switch ($this->db->getAttribute(\PDO::ATTR_DRIVER_NAME)) {
|
||||
|
|
|
@ -10,26 +10,28 @@ use JKingWeb\Arsse\Db\Exception;
|
|||
|
||||
class PDOResult extends AbstractResult {
|
||||
protected $set;
|
||||
protected $db;
|
||||
protected $cur = null;
|
||||
protected $rows = 0;
|
||||
protected $id = 0;
|
||||
|
||||
// actual public methods
|
||||
|
||||
public function changes(): int {
|
||||
return $this->rows;
|
||||
return $this->set->rowCount();
|
||||
}
|
||||
|
||||
public function lastId(): int {
|
||||
return $this->id;
|
||||
try {
|
||||
return (int) $this->db->lastInsertId();
|
||||
} catch (\PDOException $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// constructor/destructor
|
||||
|
||||
public function __construct(\PDOStatement $result, array $changes = [0,0]) {
|
||||
public function __construct(\PDO $db, \PDOStatement $result) {
|
||||
$this->set = $result;
|
||||
$this->rows = (int) $changes[0];
|
||||
$this->id = (int) $changes[1];
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
|
@ -38,6 +40,7 @@ class PDOResult extends AbstractResult {
|
|||
} catch (\PDOException $e) { // @codeCoverageIgnore
|
||||
}
|
||||
unset($this->set);
|
||||
unset($this->db);
|
||||
}
|
||||
|
||||
// PHP iterator methods
|
||||
|
|
|
@ -15,7 +15,7 @@ class PDOStatement extends AbstractStatement {
|
|||
"datetime" => \PDO::PARAM_STR,
|
||||
"binary" => \PDO::PARAM_LOB,
|
||||
"string" => \PDO::PARAM_STR,
|
||||
"boolean" => \PDO::PARAM_BOOL,
|
||||
"boolean" => \PDO::PARAM_INT, // FIXME: using \PDO::PARAM_BOOL leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
|
||||
];
|
||||
|
||||
protected $st;
|
||||
|
@ -28,10 +28,10 @@ class PDOStatement extends AbstractStatement {
|
|||
}
|
||||
|
||||
public function __destruct() {
|
||||
unset($this->st);
|
||||
unset($this->st, $this->db);
|
||||
}
|
||||
|
||||
public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
|
||||
public function runArray(array $values = []): Result {
|
||||
$this->st->closeCursor();
|
||||
$this->bindValues($values);
|
||||
try {
|
||||
|
@ -40,13 +40,7 @@ class PDOStatement extends AbstractStatement {
|
|||
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
||||
throw new $excClass($excMsg, $excData);
|
||||
}
|
||||
$changes = $this->st->rowCount();
|
||||
try {
|
||||
$lastId = 0;
|
||||
$lastId = $this->db->lastInsertId();
|
||||
} catch (\PDOException $e) { // @codeCoverageIgnore
|
||||
}
|
||||
return new PDOResult($this->st, [$changes, $lastId]);
|
||||
return new PDOResult($this->db, $this->st);
|
||||
}
|
||||
|
||||
protected function bindValue($value, string $type, int $position): bool {
|
||||
|
|
42
lib/Db/PostgreSQL/Dispatch.php
Normal file
42
lib/Db/PostgreSQL/Dispatch.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
trait Dispatch {
|
||||
protected function dispatchQuery(string $query, array $params = []) {
|
||||
pg_send_query_params($this->db, $query, $params);
|
||||
$result = pg_get_result($this->db);
|
||||
if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) {
|
||||
return $this->buildException($code, pg_result_error($result));
|
||||
} else {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildException(string $code, string $msg): array {
|
||||
switch ($code) {
|
||||
case "22P02":
|
||||
case "42804":
|
||||
return [ExceptionInput::class, 'engineTypeViolation', $msg];
|
||||
case "23000":
|
||||
case "23502":
|
||||
case "23505":
|
||||
return [ExceptionInput::class, "engineConstraintViolation", $msg];
|
||||
case "55P03":
|
||||
case "57014":
|
||||
return [ExceptionTimeout::class, 'general', $msg];
|
||||
default:
|
||||
return [Exception::class, "engineErrorGeneral", $code.": ".$msg]; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
}
|
223
lib/Db/PostgreSQL/Driver.php
Normal file
223
lib/Db/PostgreSQL/Driver.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
||||
use Dispatch;
|
||||
|
||||
protected $db;
|
||||
protected $transStart = 0;
|
||||
|
||||
public function __construct(string $user = null, string $pass = null, string $db = null, string $host = null, int $port = null, string $schema = null, string $service = null) {
|
||||
// check to make sure required extension is loaded
|
||||
if (!static::requirementsMet()) {
|
||||
throw new Exception("extMissing", static::driverName()); // @codeCoverageIgnore
|
||||
}
|
||||
$user = $user ?? Arsse::$conf->dbPostgreSQLUser;
|
||||
$pass = $pass ?? Arsse::$conf->dbPostgreSQLPass;
|
||||
$db = $db ?? Arsse::$conf->dbPostgreSQLDb;
|
||||
$host = $host ?? Arsse::$conf->dbPostgreSQLHost;
|
||||
$port = $port ?? Arsse::$conf->dbPostgreSQLPort;
|
||||
$schema = $schema ?? Arsse::$conf->dbPostgreSQLSchema;
|
||||
$service = $service ?? Arsse::$conf->dbPostgreSQLService;
|
||||
$this->makeConnection($user, $pass, $db, $host, $port, $service);
|
||||
foreach (static::makeSetupQueries($schema) as $q) {
|
||||
$this->exec($q);
|
||||
}
|
||||
}
|
||||
|
||||
public static function makeConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service): string {
|
||||
$base = [
|
||||
'client_encoding' => "UTF8",
|
||||
'application_name' => "arsse",
|
||||
'connect_timeout' => (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0),
|
||||
];
|
||||
$out = [];
|
||||
if ($service != "") {
|
||||
$out['service'] = $service;
|
||||
} else {
|
||||
if ($host != "") {
|
||||
$out['host'] = $host;
|
||||
}
|
||||
if ($port != 5432 && !($host != "" && $host[0] == "/")) {
|
||||
$out['port'] = (string) $port;
|
||||
}
|
||||
if ($db != "") {
|
||||
$out['dbname'] = $db;
|
||||
}
|
||||
if (!$pdo) {
|
||||
$out['user'] = $user;
|
||||
if ($pass != "") {
|
||||
$out['password'] = $pass;
|
||||
}
|
||||
}
|
||||
}
|
||||
ksort($out);
|
||||
ksort($base);
|
||||
$out = array_merge($out, $base);
|
||||
$out = array_map(function($v, $k) {
|
||||
return "$k='".str_replace("'", "\\'", str_replace("\\", "\\\\", $v))."'";
|
||||
}, $out, array_keys($out));
|
||||
return implode(" ", $out);
|
||||
}
|
||||
|
||||
public static function makeSetupQueries(string $schema = ""): array {
|
||||
$timeout = ceil(Arsse::$conf->dbTimeoutExec * 1000);
|
||||
$out = [
|
||||
"SET TIME ZONE UTC",
|
||||
"SET DateStyle = 'ISO, MDY'",
|
||||
"SET statement_timeout = '$timeout'",
|
||||
];
|
||||
if (strlen($schema) > 0) {
|
||||
$schema = '"'.str_replace('"', '""', $schema).'"';
|
||||
$out[] = "SET search_path = $schema, public";
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public static function create(): \JKingWeb\Arsse\Db\Driver {
|
||||
if (self::requirementsMet()) {
|
||||
return new self;
|
||||
} elseif (PDODriver::requirementsMet()) {
|
||||
return new PDODriver;
|
||||
} else {
|
||||
throw new Exception("extMissing", self::driverName());
|
||||
}
|
||||
}
|
||||
|
||||
public static function schemaID(): string {
|
||||
return "PostgreSQL";
|
||||
}
|
||||
|
||||
public function charsetAcceptable(): bool {
|
||||
return $this->query("SELECT pg_encoding_to_char(encoding) from pg_database where datname = current_database()")->getValue() == "UTF8";
|
||||
}
|
||||
|
||||
public function schemaVersion(): int {
|
||||
if ($this->query("SELECT count(*) from information_schema.tables where table_name = 'arsse_meta' and table_schema = current_schema()")->getValue()) {
|
||||
return (int) $this->query("SELECT value from arsse_meta where key = 'schema_version'")->getValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public function sqlToken(string $token): string {
|
||||
switch (strtolower($token)) {
|
||||
case "nocase":
|
||||
return '"und-x-icu"';
|
||||
default:
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
public function savepointCreate(bool $lock = false): int {
|
||||
if (!$this->transStart) {
|
||||
$this->exec("BEGIN TRANSACTION");
|
||||
$this->transStart = parent::savepointCreate($lock);
|
||||
return $this->transStart;
|
||||
} else {
|
||||
return parent::savepointCreate($lock);
|
||||
}
|
||||
}
|
||||
|
||||
public function savepointRelease(int $index = null): bool {
|
||||
$index = $index ?? $this->transDepth;
|
||||
$out = parent::savepointRelease($index);
|
||||
if ($index == $this->transStart) {
|
||||
$this->exec("COMMIT");
|
||||
$this->transStart = 0;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function savepointUndo(int $index = null): bool {
|
||||
$index = $index ?? $this->transDepth;
|
||||
$out = parent::savepointUndo($index);
|
||||
if ($index == $this->transStart) {
|
||||
$this->exec("ROLLBACK");
|
||||
$this->transStart = 0;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected function lock(): bool {
|
||||
if ($this->query("SELECT count(*) from information_schema.tables where table_schema = current_schema() and table_name = 'arsse_meta'")->getValue()) {
|
||||
$this->exec("LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function unlock(bool $rollback = false): bool {
|
||||
// do nothing; transaction is committed or rolled back later
|
||||
return true;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
if (isset($this->db)) {
|
||||
pg_close($this->db);
|
||||
unset($this->db);
|
||||
}
|
||||
}
|
||||
|
||||
public static function driverName(): string {
|
||||
return Arsse::$lang->msg("Driver.Db.PostgreSQL.Name");
|
||||
}
|
||||
|
||||
public static function requirementsMet(): bool {
|
||||
return \extension_loaded("pgsql");
|
||||
}
|
||||
|
||||
protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) {
|
||||
$dsn = $this->makeconnectionString(false, $user, $pass, $db, $host, $port, $service);
|
||||
set_error_handler(function(int $code, string $msg) {
|
||||
$msg = substr($msg, 62);
|
||||
throw new Exception("connectionFailure", ["PostgreSQL", $msg]);
|
||||
});
|
||||
try {
|
||||
$this->db = pg_connect($dsn, \PGSQL_CONNECT_FORCE_NEW);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
|
||||
protected function getError(): string {
|
||||
// stub
|
||||
return "";
|
||||
}
|
||||
|
||||
public function exec(string $query): bool {
|
||||
pg_send_query($this->db, $query);
|
||||
while ($result = pg_get_result($this->db)) {
|
||||
if (($code = pg_result_error_field($result, \PGSQL_DIAG_SQLSTATE)) && isset($code) && $code) {
|
||||
list($excClass, $excMsg, $excData) = $this->buildException($code, pg_result_error($result));
|
||||
throw new $excClass($excMsg, $excData);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function query(string $query): \JKingWeb\Arsse\Db\Result {
|
||||
$r = $this->dispatchQuery($query);
|
||||
if (is_resource($r)) {
|
||||
return new Result($this->db, $r);
|
||||
} else {
|
||||
list($excClass, $excMsg, $excData) = $r;
|
||||
throw new $excClass($excMsg, $excData);
|
||||
}
|
||||
}
|
||||
|
||||
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
|
||||
return new Statement($this->db, $query, $paramTypes);
|
||||
}
|
||||
}
|
66
lib/Db/PostgreSQL/PDODriver.php
Normal file
66
lib/Db/PostgreSQL/PDODriver.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
class PDODriver extends Driver {
|
||||
use \JKingWeb\Arsse\Db\PDODriver;
|
||||
|
||||
protected $db;
|
||||
|
||||
public static function requirementsMet(): bool {
|
||||
return class_exists("PDO") && in_array("pgsql", \PDO::getAvailableDrivers());
|
||||
}
|
||||
|
||||
protected function makeConnection(string $user, string $pass, string $db, string $host, int $port, string $service) {
|
||||
$dsn = $this->makeconnectionString(true, $user, $pass, $db, $host, $port, $service);
|
||||
try {
|
||||
$this->db = new \PDO("pgsql:$dsn", $user, $pass, [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::ATTR_PERSISTENT => true,
|
||||
]);
|
||||
} catch (\PDOException $e) {
|
||||
if ($e->getCode() == 7) {
|
||||
switch (substr($e->getMessage(), 9, 5)) {
|
||||
case "08006":
|
||||
throw new Exception("connectionFailure", ["PostgreSQL", substr($e->getMessage(), 28)]);
|
||||
default:
|
||||
throw $e; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
throw $e; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
unset($this->db);
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
public static function create(): \JKingWeb\Arsse\Db\Driver {
|
||||
if (self::requirementsMet()) {
|
||||
return new self;
|
||||
} elseif (Driver::requirementsMet()) {
|
||||
return new Driver;
|
||||
} else {
|
||||
throw new Exception("extMissing", self::driverName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function driverName(): string {
|
||||
return Arsse::$lang->msg("Driver.Db.PostgreSQLPDO.Name");
|
||||
}
|
||||
|
||||
public function prepareArray(string $query, array $paramTypes): \JKingWeb\Arsse\Db\Statement {
|
||||
return new PDOStatement($this->db, $query, $paramTypes);
|
||||
}
|
||||
}
|
56
lib/Db/PostgreSQL/PDOStatement.php
Normal file
56
lib/Db/PostgreSQL/PDOStatement.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
||||
|
||||
class PDOStatement extends Statement {
|
||||
use \JKingWeb\Arsse\Db\PDOError;
|
||||
|
||||
protected $db;
|
||||
protected $st;
|
||||
protected $qOriginal;
|
||||
protected $qMunged;
|
||||
protected $bindings;
|
||||
|
||||
public function __construct(\PDO $db, string $query, array $bindings = []) {
|
||||
$this->db = $db;
|
||||
$this->qOriginal = $query;
|
||||
$this->retypeArray($bindings);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
unset($this->db, $this->st);
|
||||
}
|
||||
|
||||
public function retypeArray(array $bindings, bool $append = false): bool {
|
||||
if ($append) {
|
||||
return parent::retypeArray($bindings, $append);
|
||||
} else {
|
||||
$this->bindings = $bindings;
|
||||
parent::retypeArray($bindings, $append);
|
||||
$this->qMunged = self::mungeQuery($this->qOriginal, $this->types, false);
|
||||
try {
|
||||
// statement creation with PostgreSQL should never fail (it is not evaluated at creation time)
|
||||
$s = $this->db->prepare($this->qMunged);
|
||||
} catch (\PDOException $e) { // @codeCoverageIgnore
|
||||
list($excClass, $excMsg, $excData) = $this->exceptionBuild(true); // @codeCoverageIgnore
|
||||
throw new $excClass($excMsg, $excData); // @codeCoverageIgnore
|
||||
}
|
||||
$this->st = new \JKingWeb\Arsse\Db\PDOStatement($this->db, $s, $this->bindings);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
|
||||
return $this->st->runArray($values);
|
||||
}
|
||||
|
||||
/** @codeCoverageIgnore */
|
||||
protected function bindValue($value, string $type, int $position): bool {
|
||||
// stub required by abstract parent, but never used
|
||||
return true;
|
||||
}
|
||||
}
|
48
lib/Db/PostgreSQL/Result.php
Normal file
48
lib/Db/PostgreSQL/Result.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
||||
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
|
||||
class Result extends \JKingWeb\Arsse\Db\AbstractResult {
|
||||
protected $db;
|
||||
protected $r;
|
||||
protected $cur;
|
||||
|
||||
// actual public methods
|
||||
|
||||
public function changes(): int {
|
||||
return pg_affected_rows($this->r);
|
||||
}
|
||||
|
||||
public function lastId(): int {
|
||||
if ($r = @pg_query($this->db, "SELECT lastval()")) {
|
||||
return (int) pg_fetch_result($r, 0, 0);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// constructor/destructor
|
||||
|
||||
public function __construct($db, $result) {
|
||||
$this->db = $db;
|
||||
$this->r = $result;
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
pg_free_result($this->r);
|
||||
unset($this->r, $this->db);
|
||||
}
|
||||
|
||||
// PHP iterator methods
|
||||
|
||||
public function valid() {
|
||||
$this->cur = pg_fetch_row($this->r, null, \PGSQL_ASSOC);
|
||||
return ($this->cur !== false);
|
||||
}
|
||||
}
|
77
lib/Db/PostgreSQL/Statement.php
Normal file
77
lib/Db/PostgreSQL/Statement.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Db\PostgreSQL;
|
||||
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Db\ExceptionInput;
|
||||
use JKingWeb\Arsse\Db\ExceptionTimeout;
|
||||
|
||||
class Statement extends \JKingWeb\Arsse\Db\AbstractStatement {
|
||||
use Dispatch;
|
||||
|
||||
const BINDINGS = [
|
||||
"integer" => "bigint",
|
||||
"float" => "decimal",
|
||||
"datetime" => "timestamp(0) without time zone",
|
||||
"binary" => "bytea",
|
||||
"string" => "text",
|
||||
"boolean" => "smallint", // FIXME: using boolean leads to incompatibilities with versions of SQLite bundled prior to PHP 7.3
|
||||
];
|
||||
|
||||
protected $db;
|
||||
protected $in = [];
|
||||
protected $qOriginal;
|
||||
protected $qMunged;
|
||||
protected $bindings;
|
||||
|
||||
public function __construct($db, string $query, array $bindings = []) {
|
||||
$this->db = $db;
|
||||
$this->qOriginal = $query;
|
||||
$this->retypeArray($bindings);
|
||||
}
|
||||
|
||||
public function retypeArray(array $bindings, bool $append = false): bool {
|
||||
if ($append) {
|
||||
return parent::retypeArray($bindings, $append);
|
||||
} else {
|
||||
$this->bindings = $bindings;
|
||||
parent::retypeArray($bindings, $append);
|
||||
$this->qMunged = self::mungeQuery($this->qOriginal, $this->types, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function runArray(array $values = []): \JKingWeb\Arsse\Db\Result {
|
||||
$this->in = [];
|
||||
$this->bindValues($values);
|
||||
$r = $this->dispatchQuery($this->qMunged, $this->in);
|
||||
if (is_resource($r)) {
|
||||
return new Result($this->db, $r);
|
||||
} else {
|
||||
list($excClass, $excMsg, $excData) = $r;
|
||||
throw new $excClass($excMsg, $excData);
|
||||
}
|
||||
}
|
||||
|
||||
protected function bindValue($value, string $type, int $position): bool {
|
||||
$this->in[] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function mungeQuery(string $q, array $types, bool $mungeParamMarkers = true): string {
|
||||
$q = explode("?", $q);
|
||||
$out = "";
|
||||
for ($b = 1; $b < sizeof($q); $b++) {
|
||||
$a = $b - 1;
|
||||
$mark = $mungeParamMarkers ? "\$$b" : "?";
|
||||
$type = isset($types[$a]) ? "::".self::BINDINGS[$types[$a]] : "";
|
||||
$out .= $q[$a].$mark.$type;
|
||||
}
|
||||
$out .= array_pop($q);
|
||||
return $out;
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ class ResultAggregate extends AbstractResult {
|
|||
// actual public methods
|
||||
|
||||
public function changes(): int {
|
||||
return array_reduce($this->data, function ($sum, $value) {
|
||||
return array_reduce($this->data, function($sum, $value) {
|
||||
return $sum + $value->changes();
|
||||
}, 0);
|
||||
}
|
||||
|
|
|
@ -31,10 +31,6 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
$timeout = Arsse::$conf->dbSQLite3Timeout * 1000;
|
||||
try {
|
||||
$this->makeConnection($dbFile, $dbKey);
|
||||
// set the timeout; parameters are not allowed for pragmas, but this usage should be safe
|
||||
$this->exec("PRAGMA busy_timeout = $timeout");
|
||||
// set other initial options
|
||||
$this->exec("PRAGMA foreign_keys = yes");
|
||||
} catch (\Throwable $e) {
|
||||
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
|
||||
$files = [
|
||||
|
@ -56,6 +52,15 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
// otherwise the database is probably corrupt
|
||||
throw new Exception("fileCorrupt", $dbFile);
|
||||
}
|
||||
// set the timeout
|
||||
$timeout = (int) ceil((Arsse::$conf->dbSQLite3Timeout ?? 0) * 1000);
|
||||
$this->setTimeout($timeout);
|
||||
// set other initial options
|
||||
$this->exec("PRAGMA foreign_keys = yes");
|
||||
// use a case-insensitive Unicode collation sequence
|
||||
$this->collator = new \Collator("@kf=false");
|
||||
$m = ($this->db instanceof \PDO) ? "sqliteCreateCollation" : "createCollation";
|
||||
$this->db->$m("nocase", [$this->collator, "compare"]);
|
||||
}
|
||||
|
||||
public static function requirementsMet(): bool {
|
||||
|
@ -68,6 +73,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
$this->db->enableExceptions(true);
|
||||
}
|
||||
|
||||
protected function setTimeout(int $msec) {
|
||||
$this->exec("PRAGMA busy_timeout = $msec");
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
try {
|
||||
$this->db->close();
|
||||
|
@ -100,20 +109,27 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
return (int) $this->query("PRAGMA user_version")->getValue();
|
||||
}
|
||||
|
||||
public function sqlToken(string $token): string {
|
||||
switch (strtolower($token)) {
|
||||
case "greatest":
|
||||
return "max";
|
||||
default:
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
public function schemaUpdate(int $to, string $basePath = null): bool {
|
||||
// turn off foreign keys
|
||||
$this->exec("PRAGMA foreign_keys = no");
|
||||
$this->exec("PRAGMA legacy_alter_table = yes");
|
||||
// run the generic updater
|
||||
try {
|
||||
parent::schemaUpdate($to, $basePath);
|
||||
} catch (\Throwable $e) {
|
||||
} finally {
|
||||
// turn foreign keys back on
|
||||
$this->exec("PRAGMA foreign_keys = yes");
|
||||
// pass the exception up
|
||||
throw $e;
|
||||
$this->exec("PRAGMA legacy_alter_table = no");
|
||||
}
|
||||
// turn foreign keys back on
|
||||
$this->exec("PRAGMA foreign_keys = yes");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -158,7 +174,13 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
}
|
||||
|
||||
protected function lock(): bool {
|
||||
$this->exec("BEGIN EXCLUSIVE TRANSACTION");
|
||||
$timeout = (int) $this->query("PRAGMA busy_timeout")->getValue();
|
||||
$this->setTimeout(0);
|
||||
try {
|
||||
$this->exec("BEGIN EXCLUSIVE TRANSACTION");
|
||||
} finally {
|
||||
$this->setTimeout($timeout);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,9 @@ class PDODriver extends Driver {
|
|||
}
|
||||
|
||||
protected function makeConnection(string $file, string $key) {
|
||||
$this->db = new \PDO("sqlite:".$file, "", "", [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
|
||||
$this->db = new \PDO("sqlite:".$file, "", "", [
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
}
|
||||
|
||||
public function __destruct() {
|
||||
|
|
|
@ -33,14 +33,14 @@ class Feed {
|
|||
} else {
|
||||
$links = $f->reader->find($f->getUrl(), $f->getContent());
|
||||
if (!$links) {
|
||||
// work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory
|
||||
// work around a PicoFeed memory leak
|
||||
libxml_use_internal_errors(false);
|
||||
throw new Feed\Exception($url, new \PicoFeed\Reader\SubscriptionNotFoundException('Unable to find a subscription'));
|
||||
} else {
|
||||
$out = $links[0];
|
||||
}
|
||||
}
|
||||
// work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory
|
||||
// work around a PicoFeed memory leak
|
||||
libxml_use_internal_errors(false);
|
||||
return $out;
|
||||
}
|
||||
|
@ -115,10 +115,10 @@ class Feed {
|
|||
// Some feeds might use a different domain (eg: feedburner), so the site url is
|
||||
// used instead of the feed's url.
|
||||
$this->favicon = (new Favicon)->find($feed->siteUrl);
|
||||
// work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory
|
||||
// work around a PicoFeed memory leak
|
||||
libxml_use_internal_errors(false);
|
||||
} catch (PicoFeedException $e) {
|
||||
// work around a PicoFeed memory leak FIXME: remove this hack (or not) once PicoFeed stops leaking memory
|
||||
// work around a PicoFeed memory leak
|
||||
libxml_use_internal_errors(false);
|
||||
throw new Feed\Exception($this->resource->getUrl(), $e);
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ class Lang {
|
|||
protected function listFiles(): array {
|
||||
$out = $this->globFiles($this->path."*.php");
|
||||
// trim the returned file paths to return just the language tag
|
||||
$out = array_map(function ($file) {
|
||||
$out = array_map(function($file) {
|
||||
$file = str_replace(DIRECTORY_SEPARATOR, "/", $file); // we replace the directory separator because we don't use native paths in testing
|
||||
$file = substr($file, strrpos($file, "/")+1);
|
||||
return strtolower(substr($file, 0, strrpos($file, ".")));
|
||||
|
|
|
@ -39,8 +39,13 @@ class Context {
|
|||
|
||||
protected function act(string $prop, int $set, $value) {
|
||||
if ($set) {
|
||||
$this->props[$prop] = true;
|
||||
$this->$prop = $value;
|
||||
if (is_null($value)) {
|
||||
unset($this->props[$prop]);
|
||||
$this->$prop = (new \ReflectionClass($this))->getDefaultProperties()[$prop];
|
||||
} else {
|
||||
$this->props[$prop] = true;
|
||||
$this->$prop = $value;
|
||||
}
|
||||
return $this;
|
||||
} else {
|
||||
return isset($this->props[$prop]);
|
||||
|
@ -136,14 +141,14 @@ class Context {
|
|||
}
|
||||
|
||||
public function editions(array $spec = null) {
|
||||
if ($spec) {
|
||||
if (isset($spec)) {
|
||||
$spec = $this->cleanArray($spec);
|
||||
}
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
}
|
||||
|
||||
public function articles(array $spec = null) {
|
||||
if ($spec) {
|
||||
if (isset($spec)) {
|
||||
$spec = $this->cleanArray($spec);
|
||||
}
|
||||
return $this->act(__FUNCTION__, func_num_args(), $spec);
|
||||
|
|
|
@ -20,6 +20,7 @@ class Query {
|
|||
protected $qWhere = []; // WHERE clause components
|
||||
protected $tWhere = []; // WHERE clause type bindings
|
||||
protected $vWhere = []; // WHERE clause binding values
|
||||
protected $group = []; // GROUP BY clause components
|
||||
protected $order = []; // ORDER BY clause components
|
||||
protected $limit = 0;
|
||||
protected $offset = 0;
|
||||
|
@ -68,6 +69,13 @@ class Query {
|
|||
return true;
|
||||
}
|
||||
|
||||
public function setGroup(string ...$column): bool {
|
||||
foreach ($column as $col) {
|
||||
$this->group[] = $col;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function setOrder(string $order, bool $prepend = false): bool {
|
||||
if ($prepend) {
|
||||
array_unshift($this->order, $order);
|
||||
|
@ -97,6 +105,7 @@ class Query {
|
|||
$this->tJoin = [];
|
||||
$this->vJoin = [];
|
||||
$this->order = [];
|
||||
$this->group = [];
|
||||
$this->setLimit(0, 0);
|
||||
if (strlen($join)) {
|
||||
$this->jCTE[] = $join;
|
||||
|
@ -167,6 +176,10 @@ class Query {
|
|||
if (sizeof($this->qWhere)) {
|
||||
$out .= " WHERE ".implode(" AND ", $this->qWhere);
|
||||
}
|
||||
// add any GROUP BY terms
|
||||
if (sizeof($this->group)) {
|
||||
$out .= " GROUP BY ".implode(", ", $this->group);
|
||||
}
|
||||
// add any ORDER BY terms
|
||||
if (sizeof($this->order)) {
|
||||
$out .= " ORDER BY ".implode(", ", $this->order);
|
||||
|
|
|
@ -97,7 +97,7 @@ class REST {
|
|||
public function apiMatch(string $url): array {
|
||||
$map = $this->apis;
|
||||
// sort the API list so the longest URL prefixes come first
|
||||
uasort($map, function ($a, $b) {
|
||||
uasort($map, function($a, $b) {
|
||||
return (strlen($a['match']) <=> strlen($b['match'])) * -1;
|
||||
});
|
||||
// normalize the target URL
|
||||
|
@ -270,7 +270,7 @@ class REST {
|
|||
} else {
|
||||
// if the host is a domain name or IP address, split it along dots and just perform URL decoding
|
||||
$host = explode(".", $host);
|
||||
$host = array_map(function ($segment) {
|
||||
$host = array_map(function($segment) {
|
||||
return str_replace(".", "%2E", rawurlencode(strtolower(rawurldecode($segment))));
|
||||
}, $host);
|
||||
$host = implode(".", $host);
|
||||
|
|
|
@ -563,7 +563,23 @@ class V1_2 extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
}
|
||||
// perform the fetch
|
||||
try {
|
||||
$items = Arsse::$db->articleList(Arsse::$user->id, $c, Database::LIST_TYPICAL);
|
||||
$items = Arsse::$db->articleList(Arsse::$user->id, $c, [
|
||||
"edition",
|
||||
"guid",
|
||||
"id",
|
||||
"url",
|
||||
"title",
|
||||
"author",
|
||||
"edited_date",
|
||||
"content",
|
||||
"media_type",
|
||||
"media_url",
|
||||
"subscription",
|
||||
"unread",
|
||||
"starred",
|
||||
"modified_date",
|
||||
"fingerprint",
|
||||
]);
|
||||
} catch (ExceptionInput $e) {
|
||||
// ID of subscription or folder is not valid
|
||||
return new EmptyResponse(422);
|
||||
|
|
|
@ -330,7 +330,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
'id' => "FEED:".self::FEED_ALL,
|
||||
'bare_id' => self::FEED_ALL,
|
||||
'icon' => "images/folder.png",
|
||||
'unread' => array_reduce($subs, function ($sum, $value) {
|
||||
'unread' => array_reduce($subs, function($sum, $value) {
|
||||
return $sum + $value['unread'];
|
||||
}, 0), // the sum of all feeds' unread is the total unread
|
||||
], $tSpecial),
|
||||
|
@ -1113,8 +1113,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
$out += Arsse::$db->articleMark(Arsse::$user->id, ['starred' => (bool) $data['mode']], (new Context)->articles($articles));
|
||||
break;
|
||||
case 2: //toggle
|
||||
$on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(true), Database::LIST_MINIMAL)->getAll(), "id");
|
||||
$off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(false), Database::LIST_MINIMAL)->getAll(), "id");
|
||||
$on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(true), ["id"])->getAll(), "id");
|
||||
$off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->starred(false), ["id"])->getAll(), "id");
|
||||
if ($off) {
|
||||
$out += Arsse::$db->articleMark(Arsse::$user->id, ['starred' => true], (new Context)->articles($off));
|
||||
}
|
||||
|
@ -1145,8 +1145,8 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
$out += Arsse::$db->articleMark(Arsse::$user->id, ['read' => !$data['mode']], (new Context)->articles($articles));
|
||||
break;
|
||||
case 2: //toggle
|
||||
$on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(true), Database::LIST_MINIMAL)->getAll(), "id");
|
||||
$off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(false), Database::LIST_MINIMAL)->getAll(), "id");
|
||||
$on = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(true), ["id"])->getAll(), "id");
|
||||
$off = array_column(Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)->unread(false), ["id"])->getAll(), "id");
|
||||
if ($off) {
|
||||
$out += Arsse::$db->articleMark(Arsse::$user->id, ['read' => false], (new Context)->articles($off));
|
||||
}
|
||||
|
@ -1183,7 +1183,22 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
}
|
||||
// retrieve the requested articles
|
||||
$out = [];
|
||||
foreach (Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles)) as $article) {
|
||||
$columns = [
|
||||
"id",
|
||||
"guid",
|
||||
"title",
|
||||
"url",
|
||||
"unread",
|
||||
"starred",
|
||||
"edited_date",
|
||||
"subscription",
|
||||
"subscription_title",
|
||||
"note",
|
||||
"content",
|
||||
"media_url",
|
||||
"media_type",
|
||||
];
|
||||
foreach (Arsse::$db->articleList(Arsse::$user->id, (new Context)->articles($articles), $columns) as $article) {
|
||||
$out[] = [
|
||||
'id' => (string) $article['id'], // string cast to be consistent with TTRSS
|
||||
'guid' => $article['guid'] ? "SHA256:".$article['guid'] : null,
|
||||
|
@ -1246,7 +1261,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
// fetch the list of IDs
|
||||
$out = [];
|
||||
try {
|
||||
foreach ($this->fetchArticles($data, Database::LIST_MINIMAL) as $row) {
|
||||
foreach ($this->fetchArticles($data, ["id"]) as $row) {
|
||||
$out[] = ['id' => (int) $row['id']];
|
||||
}
|
||||
} catch (ExceptionInput $e) {
|
||||
|
@ -1267,7 +1282,23 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
// retrieve the requested articles
|
||||
$out = [];
|
||||
try {
|
||||
foreach ($this->fetchArticles($data, Database::LIST_FULL) as $article) {
|
||||
$columns = [
|
||||
"id",
|
||||
"guid",
|
||||
"title",
|
||||
"url",
|
||||
"unread",
|
||||
"starred",
|
||||
"edited_date",
|
||||
"published_date",
|
||||
"subscription",
|
||||
"subscription_title",
|
||||
"note",
|
||||
($data['show_content'] || $data['show_excerpt']) ? "content" : "",
|
||||
($data['include_attachments']) ? "media_url": "",
|
||||
($data['include_attachments']) ? "media_type": "",
|
||||
];
|
||||
foreach ($this->fetchArticles($data, $columns) as $article) {
|
||||
$row = [
|
||||
'id' => (int) $article['id'],
|
||||
'guid' => $article['guid'] ? "SHA256:".$article['guid'] : "",
|
||||
|
@ -1325,7 +1356,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
// when paginating the header returns the latest ("first") item ID in the full list; we get this ID here
|
||||
$data['skip'] = 0;
|
||||
$data['limit'] = 1;
|
||||
$firstID = ($this->fetchArticles($data, Database::LIST_MINIMAL)->getRow() ?? ['id' => 0])['id'];
|
||||
$firstID = ($this->fetchArticles($data, ["id"])->getRow() ?? ['id' => 0])['id'];
|
||||
} elseif ($data['order_by']=="date_reverse") {
|
||||
// the "date_reverse" sort order doesn't get a first ID because it's meaningless for ascending-order pagination (pages doesn't go stale)
|
||||
$firstID = 0;
|
||||
|
@ -1346,7 +1377,7 @@ class API extends \JKingWeb\Arsse\REST\AbstractHandler {
|
|||
return $out;
|
||||
}
|
||||
|
||||
protected function fetchArticles(array $data, int $fields): \JKingWeb\Arsse\Db\Result {
|
||||
protected function fetchArticles(array $data, array $fields): \JKingWeb\Arsse\Db\Result {
|
||||
// normalize input
|
||||
if (is_null($data['feed_id'])) {
|
||||
throw new Exception("INCORRECT_USAGE");
|
||||
|
|
|
@ -9,7 +9,6 @@ namespace JKingWeb\Arsse;
|
|||
use PasswordGenerator\Generator as PassGen;
|
||||
|
||||
class User {
|
||||
|
||||
public $id = null;
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,6 +20,8 @@ return [
|
|||
|
||||
'Driver.Db.SQLite3.Name' => 'SQLite 3',
|
||||
'Driver.Db.SQLite3PDO.Name' => 'SQLite 3 (PDO)',
|
||||
'Driver.Db.PostgreSQL.Name' => 'PostgreSQL',
|
||||
'Driver.Db.PostgreSQLPDO.Name' => 'PostgreSQL (PDO)',
|
||||
'Driver.Service.Curl.Name' => 'HTTP (curl)',
|
||||
'Driver.Service.Internal.Name' => 'Internal',
|
||||
'Driver.User.Internal.Name' => 'Internal',
|
||||
|
@ -120,6 +122,7 @@ return [
|
|||
'Exception.JKingWeb/Arsse/Db/Exception.fileUnusable' => 'Insufficient permissions to open database file "{0}" for reading or writing',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.fileUncreatable' => 'Insufficient permissions to create new database file "{0}"',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.fileCorrupt' => 'Database file "{0}" is corrupt or not a valid database',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.connectionFailure' => 'Could not connect to {0} database: {1}',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeInvalid' => 'Prepared statement parameter type "{0}" is invalid',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeUnknown' => 'Prepared statement parameter type "{0}" is valid, but not implemented',
|
||||
'Exception.JKingWeb/Arsse/Db/Exception.paramTypeMissing' => 'Prepared statement parameter type for parameter #{0} was not specified',
|
||||
|
@ -156,7 +159,7 @@ return [
|
|||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.subjectMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.idMissing' => 'Referenced ID ({id}) in field "{field}" does not exist',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.circularDependence' => 'Referenced ID ({id}) in field "{field}" creates a circular dependence',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => 'Specified value in field "{0}" already exists',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.constraintViolation' => 'Specified value in field "{field}" already exists',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineConstraintViolation' => '{0}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionInput.engineTypeViolation' => '{0}',
|
||||
'Exception.JKingWeb/Arsse/Db/ExceptionTimeout.general' => '{0}',
|
||||
|
|
113
sql/PostgreSQL/0.sql
Normal file
113
sql/PostgreSQL/0.sql
Normal file
|
@ -0,0 +1,113 @@
|
|||
-- SPDX-License-Identifier: MIT
|
||||
-- Copyright 2017 J. King, Dustin Wilson et al.
|
||||
-- See LICENSE and AUTHORS files for details
|
||||
|
||||
-- Please consult the SQLite 3 schemata for commented version
|
||||
|
||||
create table arsse_meta(
|
||||
key text primary key,
|
||||
value text
|
||||
);
|
||||
|
||||
create table arsse_users(
|
||||
id text primary key,
|
||||
password text,
|
||||
name text,
|
||||
avatar_type text,
|
||||
avatar_data bytea,
|
||||
admin smallint default 0,
|
||||
rights bigint not null default 0
|
||||
);
|
||||
|
||||
create table arsse_users_meta(
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
key text not null,
|
||||
value text,
|
||||
primary key(owner,key)
|
||||
);
|
||||
|
||||
create table arsse_folders(
|
||||
id bigserial primary key,
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
parent bigint references arsse_folders(id) on delete cascade,
|
||||
name text not null,
|
||||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP, --
|
||||
unique(owner,name,parent)
|
||||
);
|
||||
|
||||
create table arsse_feeds(
|
||||
id bigserial primary key,
|
||||
url text not null,
|
||||
title text,
|
||||
favicon text,
|
||||
source text,
|
||||
updated timestamp(0) without time zone,
|
||||
modified timestamp(0) without time zone,
|
||||
next_fetch timestamp(0) without time zone,
|
||||
orphaned timestamp(0) without time zone,
|
||||
etag text not null default '',
|
||||
err_count bigint not null default 0,
|
||||
err_msg text,
|
||||
username text not null default '',
|
||||
password text not null default '',
|
||||
size bigint not null default 0,
|
||||
scrape smallint not null default 0,
|
||||
unique(url,username,password)
|
||||
);
|
||||
|
||||
create table arsse_subscriptions(
|
||||
id bigserial primary key,
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
feed bigint not null references arsse_feeds(id) on delete cascade,
|
||||
added timestamp(0) without time zone not null default CURRENT_TIMESTAMP,
|
||||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP,
|
||||
title text,
|
||||
order_type smallint not null default 0,
|
||||
pinned smallint not null default 0,
|
||||
folder bigint references arsse_folders(id) on delete cascade,
|
||||
unique(owner,feed)
|
||||
);
|
||||
|
||||
create table arsse_articles(
|
||||
id bigserial primary key,
|
||||
feed bigint not null references arsse_feeds(id) on delete cascade,
|
||||
url text,
|
||||
title text,
|
||||
author text,
|
||||
published timestamp(0) without time zone,
|
||||
edited timestamp(0) without time zone,
|
||||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP,
|
||||
content text,
|
||||
guid text,
|
||||
url_title_hash text not null,
|
||||
url_content_hash text not null,
|
||||
title_content_hash text not null
|
||||
);
|
||||
|
||||
create table arsse_enclosures(
|
||||
article bigint not null references arsse_articles(id) on delete cascade,
|
||||
url text,
|
||||
type text
|
||||
);
|
||||
|
||||
create table arsse_marks(
|
||||
article bigint not null references arsse_articles(id) on delete cascade,
|
||||
subscription bigint not null references arsse_subscriptions(id) on delete cascade on update cascade,
|
||||
read smallint not null default 0,
|
||||
starred smallint not null default 0,
|
||||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP,
|
||||
primary key(article,subscription)
|
||||
);
|
||||
|
||||
create table arsse_editions(
|
||||
id bigserial primary key,
|
||||
article bigint not null references arsse_articles(id) on delete cascade,
|
||||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
create table arsse_categories(
|
||||
article bigint not null references arsse_articles(id) on delete cascade,
|
||||
name text
|
||||
);
|
||||
|
||||
insert into arsse_meta(key,value) values('schema_version','1');
|
33
sql/PostgreSQL/1.sql
Normal file
33
sql/PostgreSQL/1.sql
Normal file
|
@ -0,0 +1,33 @@
|
|||
-- SPDX-License-Identifier: MIT
|
||||
-- Copyright 2017 J. King, Dustin Wilson et al.
|
||||
-- See LICENSE and AUTHORS files for details
|
||||
|
||||
-- Please consult the SQLite 3 schemata for commented version
|
||||
|
||||
create table arsse_sessions (
|
||||
id text primary key,
|
||||
created timestamp(0) without time zone not null default CURRENT_TIMESTAMP,
|
||||
expires timestamp(0) without time zone not null,
|
||||
"user" text not null references arsse_users(id) on delete cascade on update cascade
|
||||
);
|
||||
|
||||
create table arsse_labels (
|
||||
id bigserial primary key,
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
name text not null,
|
||||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP,
|
||||
unique(owner,name)
|
||||
);
|
||||
|
||||
create table arsse_label_members (
|
||||
label bigint not null references arsse_labels(id) on delete cascade,
|
||||
article bigint not null references arsse_articles(id) on delete cascade,
|
||||
subscription bigint not null references arsse_subscriptions(id) on delete cascade,
|
||||
assigned smallint not null default 1,
|
||||
modified timestamp(0) without time zone not null default CURRENT_TIMESTAMP,
|
||||
primary key(label,article)
|
||||
);
|
||||
|
||||
alter table arsse_marks add column note text not null default '';
|
||||
|
||||
update arsse_meta set value = '2' where key = 'schema_version';
|
16
sql/PostgreSQL/2.sql
Normal file
16
sql/PostgreSQL/2.sql
Normal file
|
@ -0,0 +1,16 @@
|
|||
-- SPDX-License-Identifier: MIT
|
||||
-- Copyright 2017 J. King, Dustin Wilson et al.
|
||||
-- See LICENSE and AUTHORS files for details
|
||||
|
||||
-- Please consult the SQLite 3 schemata for commented version
|
||||
|
||||
alter table arsse_users alter column id type text collate "und-x-icu";
|
||||
alter table arsse_folders alter column name type text collate "und-x-icu";
|
||||
alter table arsse_feeds alter column title type text collate "und-x-icu";
|
||||
alter table arsse_subscriptions alter column title type text collate "und-x-icu";
|
||||
alter table arsse_articles alter column title type text collate "und-x-icu";
|
||||
alter table arsse_articles alter column author type text collate "und-x-icu";
|
||||
alter table arsse_categories alter column name type text collate "und-x-icu";
|
||||
alter table arsse_labels alter column name type text collate "und-x-icu";
|
||||
|
||||
update arsse_meta set value = '3' where key = 'schema_version';
|
11
sql/PostgreSQL/3.sql
Normal file
11
sql/PostgreSQL/3.sql
Normal file
|
@ -0,0 +1,11 @@
|
|||
-- SPDX-License-Identifier: MIT
|
||||
-- Copyright 2017 J. King, Dustin Wilson et al.
|
||||
-- See LICENSE and AUTHORS files for details
|
||||
|
||||
-- Please consult the SQLite 3 schemata for commented version
|
||||
|
||||
alter table arsse_marks alter column modified drop default;
|
||||
alter table arsse_marks alter column modified drop not null;
|
||||
alter table arsse_marks add column touched smallint not null default 0;
|
||||
|
||||
update arsse_meta set value = '4' where key = 'schema_version';
|
|
@ -5,14 +5,14 @@
|
|||
-- Make the database WAL-journalled; this is persitent
|
||||
PRAGMA journal_mode = wal;
|
||||
|
||||
-- metadata
|
||||
create table arsse_meta(
|
||||
-- application metadata
|
||||
key text primary key not null, -- metadata key
|
||||
value text -- metadata value, serialized as a string
|
||||
);
|
||||
|
||||
-- users
|
||||
create table arsse_users(
|
||||
-- users
|
||||
id text primary key not null, -- user id
|
||||
password text, -- password, salted and hashed; if using external authentication this would be blank
|
||||
name text, -- display name
|
||||
|
@ -22,35 +22,38 @@ create table arsse_users(
|
|||
rights integer not null default 0 -- temporary admin-rights marker FIXME: remove reliance on this
|
||||
);
|
||||
|
||||
-- extra user metadata
|
||||
create table arsse_users_meta(
|
||||
-- extra user metadata (not currently used and will be removed)
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
key text not null,
|
||||
value text,
|
||||
primary key(owner,key)
|
||||
);
|
||||
|
||||
-- NextCloud News folders
|
||||
create table arsse_folders(
|
||||
-- folders, used by NextCloud News and Tiny Tiny RSS
|
||||
-- feed subscriptions may belong to at most one folder;
|
||||
-- in Tiny Tiny RSS folders may nest
|
||||
id integer primary key, -- sequence number
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of folder
|
||||
parent integer references arsse_folders(id) on delete cascade, -- parent folder id
|
||||
name text not null, -- folder name
|
||||
modified text not null default CURRENT_TIMESTAMP, --
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which the folder itself (not its contents) was changed; not currently used
|
||||
unique(owner,name,parent) -- cannot have multiple folders with the same name under the same parent for the same owner
|
||||
);
|
||||
|
||||
-- newsfeeds, deduplicated
|
||||
create table arsse_feeds(
|
||||
-- newsfeeds, deduplicated
|
||||
-- users have subscriptions to these feeds in another table
|
||||
id integer primary key, -- sequence number
|
||||
url text not null, -- URL of feed
|
||||
title text, -- default title of feed
|
||||
title text, -- default title of feed (users can set the title of their subscription to the feed)
|
||||
favicon text, -- URL of favicon
|
||||
source text, -- URL of site to which the feed belongs
|
||||
updated text, -- time at which the feed was last fetched
|
||||
modified text, -- time at which the feed last actually changed
|
||||
next_fetch text, -- time at which the feed should next be fetched
|
||||
orphaned text, -- time at which the feed last had no subscriptions
|
||||
orphaned text, -- time at which the feed last had no subscriptions
|
||||
etag text not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes
|
||||
err_count integer not null default 0, -- count of successive times update resulted in error since last successful update
|
||||
err_msg text, -- last error message
|
||||
|
@ -61,13 +64,13 @@ create table arsse_feeds(
|
|||
unique(url,username,password) -- a URL with particular credentials should only appear once
|
||||
);
|
||||
|
||||
-- users' subscriptions to newsfeeds, with settings
|
||||
create table arsse_subscriptions(
|
||||
-- users' subscriptions to newsfeeds, with settings
|
||||
id integer primary key, -- sequence number
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of subscription
|
||||
feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription
|
||||
added text not null default CURRENT_TIMESTAMP, -- time at which feed was added
|
||||
modified text not null default CURRENT_TIMESTAMP, -- date at which subscription properties were last modified
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which subscription properties were last modified
|
||||
title text, -- user-supplied title
|
||||
order_type int not null default 0, -- NextCloud sort order
|
||||
pinned boolean not null default 0, -- whether feed is pinned (always sorts at top)
|
||||
|
@ -75,16 +78,16 @@ create table arsse_subscriptions(
|
|||
unique(owner,feed) -- a given feed should only appear once for a given owner
|
||||
);
|
||||
|
||||
-- entries in newsfeeds
|
||||
create table arsse_articles(
|
||||
-- entries in newsfeeds
|
||||
id integer primary key, -- sequence number
|
||||
feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription
|
||||
url text, -- URL of article
|
||||
title text, -- article title
|
||||
author text, -- author's name
|
||||
published text, -- time of original publication
|
||||
edited text, -- time of last edit
|
||||
modified text not null default CURRENT_TIMESTAMP, -- date when article properties were last modified
|
||||
edited text, -- time of last edit by author
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database
|
||||
content text, -- content, as (X)HTML
|
||||
guid text, -- GUID
|
||||
url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid.
|
||||
|
@ -92,34 +95,37 @@ create table arsse_articles(
|
|||
title_content_hash text not null -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid.
|
||||
);
|
||||
|
||||
-- enclosures associated with articles
|
||||
create table arsse_enclosures(
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
url text,
|
||||
type text
|
||||
-- enclosures (attachments) associated with articles
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- article to which the enclosure belongs
|
||||
url text, -- URL of the enclosure
|
||||
type text -- content-type (MIME type) of the enclosure
|
||||
);
|
||||
|
||||
-- users' actions on newsfeed entries
|
||||
create table arsse_marks(
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade,
|
||||
read boolean not null default 0,
|
||||
starred boolean not null default 0,
|
||||
modified text not null default CURRENT_TIMESTAMP,
|
||||
primary key(article,subscription)
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- article associated with the marks
|
||||
subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- subscription associated with the marks; the subscription in turn belongs to a user
|
||||
read boolean not null default 0, -- whether the article has been read
|
||||
starred boolean not null default 0, -- whether the article is starred
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which an article was last modified by a given user
|
||||
primary key(article,subscription) -- no more than one mark-set per article per user
|
||||
);
|
||||
|
||||
-- IDs for specific editions of articles (required for at least NextCloud News)
|
||||
create table arsse_editions(
|
||||
id integer primary key,
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
modified datetime not null default CURRENT_TIMESTAMP
|
||||
-- IDs for specific editions of articles (required for at least NextCloud News)
|
||||
-- every time an article is updated by its author, a new unique edition number is assigned
|
||||
-- with NextCloud News this prevents users from marking as read an article which has been
|
||||
-- updated since the client state was last refreshed
|
||||
id integer primary key, -- sequence number
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- the article of which this is an edition
|
||||
modified datetime not null default CURRENT_TIMESTAMP -- tiem at which the edition was modified (practically, when it was created)
|
||||
);
|
||||
|
||||
-- author categories associated with newsfeed entries
|
||||
create table arsse_categories(
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
name text
|
||||
-- author categories associated with newsfeed entries
|
||||
-- these are not user-modifiable
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- article associated with the category
|
||||
name text -- freeform name of the category
|
||||
);
|
||||
|
||||
-- set version marker
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
-- Copyright 2017 J. King, Dustin Wilson et al.
|
||||
-- See LICENSE and AUTHORS files for details
|
||||
|
||||
-- Sessions for Tiny Tiny RSS (and possibly others)
|
||||
create table arsse_sessions (
|
||||
-- sessions for Tiny Tiny RSS (and possibly others)
|
||||
id text primary key, -- UUID of session
|
||||
created text not null default CURRENT_TIMESTAMP, -- Session start timestamp
|
||||
expires text not null, -- Time at which session is no longer valid
|
||||
user text not null references arsse_users(id) on delete cascade on update cascade -- user associated with the session
|
||||
) without rowid;
|
||||
|
||||
-- User-defined article labels for Tiny Tiny RSS
|
||||
create table arsse_labels (
|
||||
-- user-defined article labels for Tiny Tiny RSS
|
||||
id integer primary key, -- numeric ID
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade, -- owning user
|
||||
name text not null, -- label text
|
||||
|
@ -19,26 +19,29 @@ create table arsse_labels (
|
|||
unique(owner,name)
|
||||
);
|
||||
|
||||
-- Labels assignments for articles
|
||||
create table arsse_label_members (
|
||||
label integer not null references arsse_labels(id) on delete cascade,
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
-- uabels assignments for articles
|
||||
label integer not null references arsse_labels(id) on delete cascade, -- label ID associated to an article; label IDs belong to a user
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- article associated to a label
|
||||
subscription integer not null references arsse_subscriptions(id) on delete cascade, -- Subscription is included so that records are deleted when a subscription is removed
|
||||
assigned boolean not null default 1,
|
||||
modified text not null default CURRENT_TIMESTAMP,
|
||||
primary key(label,article)
|
||||
assigned boolean not null default 1, -- whether the association is current, to support soft deletion
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which the association was last made or unmade
|
||||
primary key(label,article) -- only one association of a given label to a given article
|
||||
) without rowid;
|
||||
|
||||
-- alter marks table to add Tiny Tiny RSS' notes
|
||||
-- SQLite has limited ALTER TABLE support, so the table must be re-created
|
||||
-- and its data re-entered; other database systems have a much simpler prodecure
|
||||
alter table arsse_marks rename to arsse_marks_old;
|
||||
create table arsse_marks(
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade,
|
||||
read boolean not null default 0,
|
||||
starred boolean not null default 0,
|
||||
modified text not null default CURRENT_TIMESTAMP,
|
||||
note text not null default '',
|
||||
primary key(article,subscription)
|
||||
-- users' actions on newsfeed entries
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- article associated with the marks
|
||||
subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- subscription associated with the marks; the subscription in turn belongs to a user
|
||||
read boolean not null default 0, -- whether the article has been read
|
||||
starred boolean not null default 0, -- whether the article is starred
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which an article was last modified by a given user
|
||||
note text not null default '', -- Tiny Tiny RSS freeform user note
|
||||
primary key(article,subscription) -- no more than one mark-set per article per user
|
||||
);
|
||||
insert into arsse_marks(article,subscription,read,starred,modified) select article,subscription,read,starred,modified from arsse_marks_old;
|
||||
drop table arsse_marks_old;
|
||||
|
|
|
@ -2,94 +2,106 @@
|
|||
-- Copyright 2017 J. King, Dustin Wilson et al.
|
||||
-- See LICENSE and AUTHORS files for details
|
||||
|
||||
-- Correct collation sequences
|
||||
-- Correct collation sequences in order for various things to sort case-insensitively
|
||||
-- SQLite has limited ALTER TABLE support, so the tables must be re-created
|
||||
-- and their data re-entered; other database systems have a much simpler prodecure
|
||||
alter table arsse_users rename to arsse_users_old;
|
||||
create table arsse_users(
|
||||
id text primary key not null collate nocase,
|
||||
password text,
|
||||
name text collate nocase,
|
||||
avatar_type text,
|
||||
avatar_data blob,
|
||||
admin boolean default 0,
|
||||
rights integer not null default 0
|
||||
-- users
|
||||
id text primary key not null collate nocase, -- user id
|
||||
password text, -- password, salted and hashed; if using external authentication this would be blank
|
||||
name text collate nocase, -- display name
|
||||
avatar_type text, -- internal avatar image's MIME content type
|
||||
avatar_data blob, -- internal avatar image's binary data
|
||||
admin boolean default 0, -- whether the user is a member of the special "admin" group
|
||||
rights integer not null default 0 -- temporary admin-rights marker FIXME: remove reliance on this
|
||||
);
|
||||
insert into arsse_users(id,password,name,avatar_type,avatar_data,admin,rights) select id,password,name,avatar_type,avatar_data,admin,rights from arsse_users_old;
|
||||
drop table arsse_users_old;
|
||||
|
||||
alter table arsse_folders rename to arsse_folders_old;
|
||||
create table arsse_folders(
|
||||
id integer primary key,
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
parent integer references arsse_folders(id) on delete cascade,
|
||||
name text not null collate nocase,
|
||||
modified text not null default CURRENT_TIMESTAMP, --
|
||||
unique(owner,name,parent)
|
||||
-- folders, used by NextCloud News and Tiny Tiny RSS
|
||||
-- feed subscriptions may belong to at most one folder;
|
||||
-- in Tiny Tiny RSS folders may nest
|
||||
id integer primary key, -- sequence number
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of folder
|
||||
parent integer references arsse_folders(id) on delete cascade, -- parent folder id
|
||||
name text not null collate nocase, -- folder name
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which the folder itself (not its contents) was changed; not currently used
|
||||
unique(owner,name,parent) -- cannot have multiple folders with the same name under the same parent for the same owner
|
||||
);
|
||||
insert into arsse_folders select * from arsse_folders_old;
|
||||
drop table arsse_folders_old;
|
||||
|
||||
alter table arsse_feeds rename to arsse_feeds_old;
|
||||
create table arsse_feeds(
|
||||
id integer primary key,
|
||||
url text not null,
|
||||
title text collate nocase,
|
||||
favicon text,
|
||||
source text,
|
||||
updated text,
|
||||
modified text,
|
||||
next_fetch text,
|
||||
orphaned text,
|
||||
etag text not null default '',
|
||||
err_count integer not null default 0,
|
||||
err_msg text,
|
||||
username text not null default '',
|
||||
password text not null default '',
|
||||
size integer not null default 0,
|
||||
scrape boolean not null default 0,
|
||||
unique(url,username,password)
|
||||
-- newsfeeds, deduplicated
|
||||
-- users have subscriptions to these feeds in another table
|
||||
id integer primary key, -- sequence number
|
||||
url text not null, -- URL of feed
|
||||
title text collate nocase, -- default title of feed (users can set the title of their subscription to the feed)
|
||||
favicon text, -- URL of favicon
|
||||
source text, -- URL of site to which the feed belongs
|
||||
updated text, -- time at which the feed was last fetched
|
||||
modified text, -- time at which the feed last actually changed
|
||||
next_fetch text, -- time at which the feed should next be fetched
|
||||
orphaned text, -- time at which the feed last had no subscriptions
|
||||
etag text not null default '', -- HTTP ETag hash used for cache validation, changes each time the content changes
|
||||
err_count integer not null default 0, -- count of successive times update resulted in error since last successful update
|
||||
err_msg text, -- last error message
|
||||
username text not null default '', -- HTTP authentication username
|
||||
password text not null default '', -- HTTP authentication password (this is stored in plain text)
|
||||
size integer not null default 0, -- number of articles in the feed at last fetch
|
||||
scrape boolean not null default 0, -- whether to use picoFeed's content scraper with this feed
|
||||
unique(url,username,password) -- a URL with particular credentials should only appear once
|
||||
);
|
||||
insert into arsse_feeds select * from arsse_feeds_old;
|
||||
drop table arsse_feeds_old;
|
||||
|
||||
alter table arsse_subscriptions rename to arsse_subscriptions_old;
|
||||
create table arsse_subscriptions(
|
||||
id integer primary key,
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
feed integer not null references arsse_feeds(id) on delete cascade,
|
||||
added text not null default CURRENT_TIMESTAMP,
|
||||
modified text not null default CURRENT_TIMESTAMP,
|
||||
title text collate nocase,
|
||||
order_type int not null default 0,
|
||||
pinned boolean not null default 0,
|
||||
folder integer references arsse_folders(id) on delete cascade,
|
||||
unique(owner,feed)
|
||||
-- users' subscriptions to newsfeeds, with settings
|
||||
id integer primary key, -- sequence number
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade, -- owner of subscription
|
||||
feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription
|
||||
added text not null default CURRENT_TIMESTAMP, -- time at which feed was added
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which subscription properties were last modified
|
||||
title text collate nocase, -- user-supplied title
|
||||
order_type int not null default 0, -- NextCloud sort order
|
||||
pinned boolean not null default 0, -- whether feed is pinned (always sorts at top)
|
||||
folder integer references arsse_folders(id) on delete cascade, -- TT-RSS category (nestable); the first-level category (which acts as NextCloud folder) is joined in when needed
|
||||
unique(owner,feed) -- a given feed should only appear once for a given owner
|
||||
);
|
||||
insert into arsse_subscriptions select * from arsse_subscriptions_old;
|
||||
drop table arsse_subscriptions_old;
|
||||
|
||||
alter table arsse_articles rename to arsse_articles_old;
|
||||
create table arsse_articles(
|
||||
id integer primary key,
|
||||
feed integer not null references arsse_feeds(id) on delete cascade,
|
||||
url text,
|
||||
title text collate nocase,
|
||||
author text collate nocase,
|
||||
published text,
|
||||
edited text,
|
||||
modified text not null default CURRENT_TIMESTAMP,
|
||||
content text,
|
||||
guid text,
|
||||
url_title_hash text not null,
|
||||
url_content_hash text not null,
|
||||
title_content_hash text not null
|
||||
-- entries in newsfeeds
|
||||
id integer primary key, -- sequence number
|
||||
feed integer not null references arsse_feeds(id) on delete cascade, -- feed for the subscription
|
||||
url text, -- URL of article
|
||||
title text collate nocase, -- article title
|
||||
author text collate nocase, -- author's name
|
||||
published text, -- time of original publication
|
||||
edited text, -- time of last edit by author
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time when article was last modified in database
|
||||
content text, -- content, as (X)HTML
|
||||
guid text, -- GUID
|
||||
url_title_hash text not null, -- hash of URL + title; used when checking for updates and for identification if there is no guid.
|
||||
url_content_hash text not null, -- hash of URL + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid.
|
||||
title_content_hash text not null -- hash of title + content, enclosure URL, & content type; used when checking for updates and for identification if there is no guid.
|
||||
);
|
||||
insert into arsse_articles select * from arsse_articles_old;
|
||||
drop table arsse_articles_old;
|
||||
|
||||
alter table arsse_categories rename to arsse_categories_old;
|
||||
create table arsse_categories(
|
||||
article integer not null references arsse_articles(id) on delete cascade,
|
||||
name text collate nocase
|
||||
-- author categories associated with newsfeed entries
|
||||
-- these are not user-modifiable
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- article associated with the category
|
||||
name text collate nocase -- freeform name of the category
|
||||
);
|
||||
insert into arsse_categories select * from arsse_categories_old;
|
||||
drop table arsse_categories_old;
|
||||
|
@ -97,10 +109,11 @@ drop table arsse_categories_old;
|
|||
|
||||
alter table arsse_labels rename to arsse_labels_old;
|
||||
create table arsse_labels (
|
||||
id integer primary key,
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade,
|
||||
name text not null collate nocase,
|
||||
modified text not null default CURRENT_TIMESTAMP,
|
||||
-- user-defined article labels for Tiny Tiny RSS
|
||||
id integer primary key, -- numeric ID
|
||||
owner text not null references arsse_users(id) on delete cascade on update cascade, -- owning user
|
||||
name text not null collate nocase, -- label text
|
||||
modified text not null default CURRENT_TIMESTAMP, -- time at which the label was last modified
|
||||
unique(owner,name)
|
||||
);
|
||||
insert into arsse_labels select * from arsse_labels_old;
|
||||
|
|
27
sql/SQLite3/3.sql
Normal file
27
sql/SQLite3/3.sql
Normal file
|
@ -0,0 +1,27 @@
|
|||
-- SPDX-License-Identifier: MIT
|
||||
-- Copyright 2017 J. King, Dustin Wilson et al.
|
||||
-- See LICENSE and AUTHORS files for details
|
||||
|
||||
-- allow marks to initially have a null date due to changes in how marks are first created
|
||||
-- and also add a "touched" column to aid in tracking changes during the course of some transactions
|
||||
alter table arsse_marks rename to arsse_marks_old;
|
||||
create table arsse_marks(
|
||||
-- users' actions on newsfeed entries
|
||||
article integer not null references arsse_articles(id) on delete cascade, -- article associated with the marks
|
||||
subscription integer not null references arsse_subscriptions(id) on delete cascade on update cascade, -- subscription associated with the marks; the subscription in turn belongs to a user
|
||||
read boolean not null default 0, -- whether the article has been read
|
||||
starred boolean not null default 0, -- whether the article is starred
|
||||
modified text, -- time at which an article was last modified by a given user
|
||||
note text not null default '', -- Tiny Tiny RSS freeform user note
|
||||
touched boolean not null default 0, -- used to indicate a record has been modified during the course of some transactions
|
||||
primary key(article,subscription) -- no more than one mark-set per article per user
|
||||
);
|
||||
insert into arsse_marks select article,subscription,read,starred,modified,note,0 from arsse_marks_old;
|
||||
drop table arsse_marks_old;
|
||||
|
||||
-- reindex anything which uses the nocase collation sequence; it has been replaced with a Unicode collation
|
||||
reindex nocase;
|
||||
|
||||
-- set version marker
|
||||
pragma user_version = 4;
|
||||
update arsse_meta set value = '4' where key = 'schema_version';
|
|
@ -16,9 +16,10 @@ use Phake;
|
|||
|
||||
/** @covers \JKingWeb\Arsse\CLI */
|
||||
class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
|
||||
public function setUp() {
|
||||
$this->clearData(false);
|
||||
self::clearData(false);
|
||||
$this->cli = Phake::partialMock(CLI::class);
|
||||
Phake::when($this->cli)->logError->thenReturn(null);
|
||||
}
|
||||
|
||||
public function assertConsole(CLI $cli, string $command, int $exitStatus, string $output = "", bool $pattern = false) {
|
||||
|
@ -45,13 +46,13 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
|
||||
public function testPrintVersion() {
|
||||
$this->assertConsole(new CLI, "arsse.php --version", 0, Arsse::VERSION);
|
||||
$this->assertConsole($this->cli, "arsse.php --version", 0, Arsse::VERSION);
|
||||
$this->assertLoaded(false);
|
||||
}
|
||||
|
||||
/** @dataProvider provideHelpText */
|
||||
public function testPrintHelp(string $cmd, string $name) {
|
||||
$this->assertConsole(new CLI, $cmd, 0, str_replace("arsse.php", $name, CLI::USAGE));
|
||||
$this->assertConsole($this->cli, $cmd, 0, str_replace("arsse.php", $name, CLI::USAGE));
|
||||
$this->assertLoaded(false);
|
||||
}
|
||||
|
||||
|
@ -65,13 +66,12 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
public function testStartTheDaemon() {
|
||||
$srv = Phake::mock(Service::class);
|
||||
$cli = Phake::partialMock(CLI::class);
|
||||
Phake::when($srv)->watch->thenReturn(new \DateTimeImmutable);
|
||||
Phake::when($cli)->getService->thenReturn($srv);
|
||||
$this->assertConsole($cli, "arsse.php daemon", 0);
|
||||
Phake::when($this->cli)->getService->thenReturn($srv);
|
||||
$this->assertConsole($this->cli, "arsse.php daemon", 0);
|
||||
$this->assertLoaded(true);
|
||||
Phake::verify($srv)->watch(true);
|
||||
Phake::verify($cli)->getService;
|
||||
Phake::verify($this->cli)->getService;
|
||||
}
|
||||
|
||||
/** @dataProvider provideFeedUpdates */
|
||||
|
@ -79,7 +79,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
Arsse::$db = Phake::mock(Database::class);
|
||||
Phake::when(Arsse::$db)->feedUpdate(1, true)->thenReturn(true);
|
||||
Phake::when(Arsse::$db)->feedUpdate(2, true)->thenThrow(new \JKingWeb\Arsse\Feed\Exception("http://example.com/", new \PicoFeed\Client\InvalidUrlException));
|
||||
$this->assertConsole(new CLI, $cmd, $exitStatus, $output);
|
||||
$this->assertConsole($this->cli, $cmd, $exitStatus, $output);
|
||||
$this->assertLoaded(true);
|
||||
Phake::verify(Arsse::$db)->feedUpdate;
|
||||
}
|
||||
|
@ -94,12 +94,11 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
/** @dataProvider provideDefaultConfigurationSaves */
|
||||
public function testSaveTheDefaultConfiguration(string $cmd, int $exitStatus, string $file) {
|
||||
$conf = Phake::mock(Conf::class);
|
||||
$cli = Phake::partialMock(CLI::class);
|
||||
Phake::when($conf)->exportFile("php://output", true)->thenReturn(true);
|
||||
Phake::when($conf)->exportFile("good.conf", true)->thenReturn(true);
|
||||
Phake::when($conf)->exportFile("bad.conf", true)->thenThrow(new \JKingWeb\Arsse\Conf\Exception("fileUnwritable"));
|
||||
Phake::when($cli)->getConf->thenReturn($conf);
|
||||
$this->assertConsole($cli, $cmd, $exitStatus);
|
||||
Phake::when($this->cli)->getConf->thenReturn($conf);
|
||||
$this->assertConsole($this->cli, $cmd, $exitStatus);
|
||||
$this->assertLoaded(false);
|
||||
Phake::verify($conf)->exportFile($file, true);
|
||||
}
|
||||
|
@ -115,10 +114,10 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
/** @dataProvider provideUserList */
|
||||
public function testListUsers(string $cmd, array $list, int $exitStatus, string $output) {
|
||||
// Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
// FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
Arsse::$user = $this->createMock(User::class);
|
||||
Arsse::$user->method("list")->willReturn($list);
|
||||
$this->assertConsole(new CLI, $cmd, $exitStatus, $output);
|
||||
$this->assertConsole($this->cli, $cmd, $exitStatus, $output);
|
||||
}
|
||||
|
||||
public function provideUserList() {
|
||||
|
@ -134,7 +133,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
/** @dataProvider provideUserAdditions */
|
||||
public function testAddAUser(string $cmd, int $exitStatus, string $output) {
|
||||
// Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
// FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
Arsse::$user = $this->createMock(User::class);
|
||||
Arsse::$user->method("add")->will($this->returnCallback(function($user, $pass = null) {
|
||||
switch ($user) {
|
||||
|
@ -144,7 +143,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
return is_null($pass) ? "random password" : $pass;
|
||||
}
|
||||
}));
|
||||
$this->assertConsole(new CLI, $cmd, $exitStatus, $output);
|
||||
$this->assertConsole($this->cli, $cmd, $exitStatus, $output);
|
||||
}
|
||||
|
||||
public function provideUserAdditions() {
|
||||
|
@ -157,7 +156,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
/** @dataProvider provideUserAuthentication */
|
||||
public function testAuthenticateAUser(string $cmd, int $exitStatus, string $output) {
|
||||
// Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
// FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
Arsse::$user = $this->createMock(User::class);
|
||||
Arsse::$user->method("auth")->will($this->returnCallback(function($user, $pass) {
|
||||
return (
|
||||
|
@ -165,7 +164,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
($user == "jane.doe@example.com" && $pass == "superman")
|
||||
);
|
||||
}));
|
||||
$this->assertConsole(new CLI, $cmd, $exitStatus, $output);
|
||||
$this->assertConsole($this->cli, $cmd, $exitStatus, $output);
|
||||
}
|
||||
|
||||
public function provideUserAuthentication() {
|
||||
|
@ -180,7 +179,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
/** @dataProvider provideUserRemovals */
|
||||
public function testRemoveAUser(string $cmd, int $exitStatus, string $output) {
|
||||
// Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
// FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
Arsse::$user = $this->createMock(User::class);
|
||||
Arsse::$user->method("remove")->will($this->returnCallback(function($user) {
|
||||
if ($user == "john.doe@example.com") {
|
||||
|
@ -188,7 +187,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
}
|
||||
throw new \JKingWeb\Arsse\User\Exception("doesNotExist");
|
||||
}));
|
||||
$this->assertConsole(new CLI, $cmd, $exitStatus, $output);
|
||||
$this->assertConsole($this->cli, $cmd, $exitStatus, $output);
|
||||
}
|
||||
|
||||
public function provideUserRemovals() {
|
||||
|
@ -200,7 +199,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
|
||||
/** @dataProvider provideUserPasswordChanges */
|
||||
public function testChangeAUserPassword(string $cmd, int $exitStatus, string $output) {
|
||||
// Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
// FIXME: Phake is somehow unable to mock the User class correctly, so we use PHPUnit's mocks instead
|
||||
Arsse::$user = $this->createMock(User::class);
|
||||
Arsse::$user->method("passwordSet")->will($this->returnCallback(function($user, $pass = null) {
|
||||
switch ($user) {
|
||||
|
@ -210,7 +209,7 @@ class TestCLI extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
return is_null($pass) ? "random password" : $pass;
|
||||
}
|
||||
}));
|
||||
$this->assertConsole(new CLI, $cmd, $exitStatus, $output);
|
||||
$this->assertConsole($this->cli, $cmd, $exitStatus, $output);
|
||||
}
|
||||
|
||||
public function provideUserPasswordChanges() {
|
||||
|
|
|
@ -15,7 +15,7 @@ class TestConf extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
public static $path;
|
||||
|
||||
public function setUp() {
|
||||
$this->clearData();
|
||||
self::clearData();
|
||||
self::$vfs = vfsStream::setup("root", null, [
|
||||
'confGood' => '<?php return Array("lang" => "xx");',
|
||||
'confNotArray' => '<?php return 0;',
|
||||
|
@ -35,7 +35,7 @@ class TestConf extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
public function tearDown() {
|
||||
self::$path = null;
|
||||
self::$vfs = null;
|
||||
$this->clearData();
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public function testLoadDefaultValues() {
|
||||
|
|
|
@ -4,37 +4,85 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\User\Driver as UserDriver;
|
||||
use JKingWeb\Arsse\Test\Database;
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\User;
|
||||
use JKingWeb\Arsse\Misc\ValueInfo;
|
||||
use JKingWeb\Arsse\Test\Database;
|
||||
use JKingWeb\Arsse\Db\Result;
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
use Phake;
|
||||
|
||||
trait Setup {
|
||||
protected $drv;
|
||||
abstract class Base extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use SeriesMiscellany;
|
||||
use SeriesMeta;
|
||||
use SeriesUser;
|
||||
use SeriesSession;
|
||||
use SeriesFolder;
|
||||
use SeriesFeed;
|
||||
use SeriesSubscription;
|
||||
use SeriesArticle;
|
||||
use SeriesLabel;
|
||||
use SeriesCleanup;
|
||||
|
||||
/** @var \JKingWeb\Arsse\Test\DatabaseInformation */
|
||||
protected static $dbInfo;
|
||||
/** @var \JKingWeb\Arsse\Db\Driver */
|
||||
protected static $drv;
|
||||
protected static $failureReason = "";
|
||||
protected $primed = false;
|
||||
|
||||
public function setUp() {
|
||||
abstract protected function nextID(string $table): int;
|
||||
|
||||
protected function findTraitOfTest(string $test): string {
|
||||
$class = new \ReflectionClass(self::class);
|
||||
foreach ($class->getTraits() as $trait) {
|
||||
if ($trait->hasMethod($test)) {
|
||||
return $trait->getShortName();
|
||||
}
|
||||
}
|
||||
return $class->getShortName();
|
||||
}
|
||||
|
||||
public static function setUpBeforeClass() {
|
||||
// establish a clean baseline
|
||||
$this->clearData();
|
||||
$this->setConf();
|
||||
// configure and create the relevant database driver
|
||||
$this->setUpDriver();
|
||||
// create the database interface with the suitable driver
|
||||
Arsse::$db = new Database($this->drv);
|
||||
static::clearData();
|
||||
// perform an initial connection to the database to reset its version to zero
|
||||
// in the case of SQLite this will always be the case (we use a memory database),
|
||||
// but other engines should clean up from potentially interrupted prior tests
|
||||
static::$dbInfo = new DatabaseInformation(static::$implementation);
|
||||
static::setConf();
|
||||
try {
|
||||
static::$drv = new static::$dbInfo->driverClass;
|
||||
} catch (\JKingWeb\Arsse\Db\Exception $e) {
|
||||
static::$failureReason = $e->getMessage();
|
||||
return;
|
||||
}
|
||||
// wipe the database absolutely clean
|
||||
(static::$dbInfo->razeFunction)(static::$drv);
|
||||
// create the database interface with the suitable driver and apply the latest schema
|
||||
Arsse::$db = new Database(static::$drv);
|
||||
Arsse::$db->driverSchemaUpdate();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
// get the name of the test's test series
|
||||
$this->series = $this->findTraitofTest($this->getName());
|
||||
static::clearData();
|
||||
static::setConf();
|
||||
if (strlen(static::$failureReason)) {
|
||||
$this->markTestSkipped(static::$failureReason);
|
||||
}
|
||||
Arsse::$db = new Database(static::$drv);
|
||||
Arsse::$db->driverSchemaUpdate();
|
||||
// create a mock user manager
|
||||
Arsse::$user = Phake::mock(User::class);
|
||||
Phake::when(Arsse::$user)->authorize->thenReturn(true);
|
||||
// call the additional setup method if it exists
|
||||
if (method_exists($this, "setUpSeries")) {
|
||||
$this->setUpSeries();
|
||||
}
|
||||
// call the series-specific setup method
|
||||
$setUp = "setUp".$this->series;
|
||||
$this->$setUp();
|
||||
// prime the database with series data if it hasn't already been done
|
||||
if (!$this->primed && isset($this->data)) {
|
||||
$this->primeDatabase($this->data);
|
||||
|
@ -42,21 +90,36 @@ trait Setup {
|
|||
}
|
||||
|
||||
public function tearDown() {
|
||||
// call the additional teardiwn method if it exists
|
||||
if (method_exists($this, "tearDownSeries")) {
|
||||
$this->tearDownSeries();
|
||||
}
|
||||
// call the series-specific teardown method
|
||||
$this->series = $this->findTraitofTest($this->getName());
|
||||
$tearDown = "tearDown".$this->series;
|
||||
$this->$tearDown();
|
||||
// clean up
|
||||
$this->primed = false;
|
||||
$this->drv = null;
|
||||
$this->clearData();
|
||||
// call the database-specific table cleanup function
|
||||
(static::$dbInfo->truncateFunction)(static::$drv);
|
||||
// clear state
|
||||
static::clearData();
|
||||
}
|
||||
|
||||
public function primeDatabase(array $data, \JKingWeb\Arsse\Db\Driver $drv = null): bool {
|
||||
$drv = $drv ?? $this->drv;
|
||||
public static function tearDownAfterClass() {
|
||||
// wipe the database absolutely clean
|
||||
(static::$dbInfo->razeFunction)(static::$drv);
|
||||
// clean up
|
||||
static::$drv = null;
|
||||
static::$dbInfo = null;
|
||||
static::$failureReason = "";
|
||||
static::clearData();
|
||||
}
|
||||
|
||||
public function primeDatabase(array $data): bool {
|
||||
$drv = static::$drv;
|
||||
$tr = $drv->begin();
|
||||
foreach ($data as $table => $info) {
|
||||
$cols = implode(",", array_keys($info['columns']));
|
||||
$cols = array_map(function($v) {
|
||||
return '"'.str_replace('"', '""', $v).'"';
|
||||
}, array_keys($info['columns']));
|
||||
$cols = implode(",", $cols);
|
||||
$bindings = array_values($info['columns']);
|
||||
$params = implode(",", array_fill(0, sizeof($info['columns']), "?"));
|
||||
$s = $drv->prepareArray("INSERT INTO $table($cols) values($params)", $bindings);
|
||||
|
@ -69,21 +132,14 @@ trait Setup {
|
|||
return true;
|
||||
}
|
||||
|
||||
public function primeFile(string $file, array $data = null): bool {
|
||||
$data = $data ?? $this->data;
|
||||
$primed = $this->primed;
|
||||
$drv = new \JKingWeb\Arsse\Db\SQLite3\Driver($file);
|
||||
$drv->schemaUpdate(\JKingWeb\Arsse\Database::SCHEMA_VERSION);
|
||||
$this->primeDatabase($data, $drv);
|
||||
$this->primed = $primed;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function compareExpectations(array $expected): bool {
|
||||
foreach ($expected as $table => $info) {
|
||||
$cols = implode(",", array_keys($info['columns']));
|
||||
$cols = array_map(function($v) {
|
||||
return '"'.str_replace('"', '""', $v).'"';
|
||||
}, array_keys($info['columns']));
|
||||
$cols = implode(",", $cols);
|
||||
$types = $info['columns'];
|
||||
$data = $this->drv->prepare("SELECT $cols from $table")->run()->getAll();
|
||||
$data = static::$drv->prepare("SELECT $cols from $table")->run()->getAll();
|
||||
$cols = array_keys($info['columns']);
|
||||
foreach ($info['rows'] as $index => $row) {
|
||||
$this->assertCount(sizeof($cols), $row, "The number of values for array index $index does not match the number of fields");
|
||||
|
@ -169,7 +225,7 @@ trait Setup {
|
|||
$found = array_search($row, $expected);
|
||||
unset($expected[$found]);
|
||||
}
|
||||
$this->assertArraySubset($expected, [], "Expectations not in result set.");
|
||||
$this->assertArraySubset($expected, [], false, "Expectations not in result set.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
|
@ -13,463 +13,487 @@ use JKingWeb\Arsse\Misc\Date;
|
|||
use Phake;
|
||||
|
||||
trait SeriesArticle {
|
||||
protected $data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
protected function setUpSeriesArticle() {
|
||||
$this->data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
["john.doe@example.org", "", "John Doe"],
|
||||
["john.doe@example.net", "", "John Doe"],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
["john.doe@example.org", "", "John Doe"],
|
||||
["john.doe@example.net", "", "John Doe"],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
[7, "john.doe@example.net", null, "Technology"],
|
||||
[8, "john.doe@example.net", 7, "Software"],
|
||||
[9, "john.doe@example.net", null, "Politics"],
|
||||
]
|
||||
],
|
||||
],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
'arsse_feeds' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1,"http://example.com/1", "Feed 1"],
|
||||
[2,"http://example.com/2", "Feed 2"],
|
||||
[3,"http://example.com/3", "Feed 3"],
|
||||
[4,"http://example.com/4", "Feed 4"],
|
||||
[5,"http://example.com/5", "Feed 5"],
|
||||
[6,"http://example.com/6", "Feed 6"],
|
||||
[7,"http://example.com/7", "Feed 7"],
|
||||
[8,"http://example.com/8", "Feed 8"],
|
||||
[9,"http://example.com/9", "Feed 9"],
|
||||
[10,"http://example.com/10", "Feed 10"],
|
||||
[11,"http://example.com/11", "Feed 11"],
|
||||
[12,"http://example.com/12", "Feed 12"],
|
||||
[13,"http://example.com/13", "Feed 13"],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
[7, "john.doe@example.net", null, "Technology"],
|
||||
[8, "john.doe@example.net", 7, "Software"],
|
||||
[9, "john.doe@example.net", null, "Politics"],
|
||||
]
|
||||
],
|
||||
'arsse_feeds' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'arsse_subscriptions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'feed' => "int",
|
||||
'folder' => "int",
|
||||
'title' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com",1, null,"Subscription 1"],
|
||||
[2, "john.doe@example.com",2, null,null],
|
||||
[3, "john.doe@example.com",3, 1,"Subscription 3"],
|
||||
[4, "john.doe@example.com",4, 6,null],
|
||||
[5, "john.doe@example.com",10, 5,"Subscription 5"],
|
||||
[6, "jane.doe@example.com",1, null,null],
|
||||
[7, "jane.doe@example.com",10,null,"Subscription 7"],
|
||||
[8, "john.doe@example.org",11,null,null],
|
||||
[9, "john.doe@example.org",12,null,"Subscription 9"],
|
||||
[10,"john.doe@example.org",13,null,null],
|
||||
[11,"john.doe@example.net",10,null,"Subscription 11"],
|
||||
[12,"john.doe@example.net",2, 9,null],
|
||||
[13,"john.doe@example.net",3, 8,"Subscription 13"],
|
||||
[14,"john.doe@example.net",4, 7,null],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1,"http://example.com/1", "Feed 1"],
|
||||
[2,"http://example.com/2", "Feed 2"],
|
||||
[3,"http://example.com/3", "Feed 3"],
|
||||
[4,"http://example.com/4", "Feed 4"],
|
||||
[5,"http://example.com/5", "Feed 5"],
|
||||
[6,"http://example.com/6", "Feed 6"],
|
||||
[7,"http://example.com/7", "Feed 7"],
|
||||
[8,"http://example.com/8", "Feed 8"],
|
||||
[9,"http://example.com/9", "Feed 9"],
|
||||
[10,"http://example.com/10", "Feed 10"],
|
||||
[11,"http://example.com/11", "Feed 11"],
|
||||
[12,"http://example.com/12", "Feed 12"],
|
||||
[13,"http://example.com/13", "Feed 13"],
|
||||
]
|
||||
],
|
||||
'arsse_subscriptions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'feed' => "int",
|
||||
'folder' => "int",
|
||||
'title' => "str",
|
||||
'arsse_articles' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'feed' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'author' => "str",
|
||||
'published' => "datetime",
|
||||
'edited' => "datetime",
|
||||
'content' => "str",
|
||||
'guid' => "str",
|
||||
'url_title_hash' => "str",
|
||||
'url_content_hash' => "str",
|
||||
'title_content_hash' => "str",
|
||||
'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"],
|
||||
[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"],
|
||||
[7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','<p>Article content 1</p>','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'],
|
||||
[102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','<p>Article content 2</p>','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'],
|
||||
[103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','<p>Article content 3</p>','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'],
|
||||
[104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','<p>Article content 4</p>','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'],
|
||||
[105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','<p>Article content 5</p>','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com",1, null,"Subscription 1"],
|
||||
[2, "john.doe@example.com",2, null,null],
|
||||
[3, "john.doe@example.com",3, 1,"Subscription 3"],
|
||||
[4, "john.doe@example.com",4, 6,null],
|
||||
[5, "john.doe@example.com",10, 5,"Subscription 5"],
|
||||
[6, "jane.doe@example.com",1, null,null],
|
||||
[7, "jane.doe@example.com",10,null,"Subscription 7"],
|
||||
[8, "john.doe@example.org",11,null,null],
|
||||
[9, "john.doe@example.org",12,null,"Subscription 9"],
|
||||
[10,"john.doe@example.org",13,null,null],
|
||||
[11,"john.doe@example.net",10,null,"Subscription 11"],
|
||||
[12,"john.doe@example.net",2, 9,null],
|
||||
[13,"john.doe@example.net",3, 8,"Subscription 13"],
|
||||
[14,"john.doe@example.net",4, 7,null],
|
||||
]
|
||||
],
|
||||
'arsse_articles' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'feed' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'author' => "str",
|
||||
'published' => "datetime",
|
||||
'edited' => "datetime",
|
||||
'content' => "str",
|
||||
'guid' => "str",
|
||||
'url_title_hash' => "str",
|
||||
'url_content_hash' => "str",
|
||||
'title_content_hash' => "str",
|
||||
'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"],
|
||||
[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"],
|
||||
[7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','<p>Article content 1</p>','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'],
|
||||
[102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','<p>Article content 2</p>','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'],
|
||||
[103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','<p>Article content 3</p>','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'],
|
||||
[104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','<p>Article content 4</p>','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'],
|
||||
[105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','<p>Article content 5</p>','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'],
|
||||
]
|
||||
],
|
||||
'arsse_enclosures' => [
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'url' => "str",
|
||||
'type' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[102,"http://example.com/text","text/plain"],
|
||||
[103,"http://example.com/video","video/webm"],
|
||||
[104,"http://example.com/image","image/svg+xml"],
|
||||
[105,"http://example.com/audio","audio/ogg"],
|
||||
'arsse_enclosures' => [
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'url' => "str",
|
||||
'type' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[102,"http://example.com/text","text/plain"],
|
||||
[103,"http://example.com/video","video/webm"],
|
||||
[104,"http://example.com/image","image/svg+xml"],
|
||||
[105,"http://example.com/audio","audio/ogg"],
|
||||
|
||||
]
|
||||
],
|
||||
'arsse_editions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'article' => "int",
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[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],
|
||||
]
|
||||
],
|
||||
'arsse_marks' => [
|
||||
'columns' => [
|
||||
'subscription' => "int",
|
||||
'article' => "int",
|
||||
'read' => "bool",
|
||||
'starred' => "bool",
|
||||
'modified' => "datetime",
|
||||
'note' => "str",
|
||||
'arsse_editions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'article' => "int",
|
||||
],
|
||||
'rows' => [
|
||||
[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],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1,'2000-01-01 00:00:00',''],
|
||||
[5, 19,1,0,'2016-01-01 00:00:00',''],
|
||||
[5, 20,0,1,'2005-01-01 00:00:00',''],
|
||||
[7, 20,1,0,'2010-01-01 00:00:00',''],
|
||||
[8, 102,1,0,'2000-01-02 02:00:00','Note 2'],
|
||||
[9, 103,0,1,'2000-01-03 03:00:00','Note 3'],
|
||||
[9, 104,1,1,'2000-01-04 04:00:00','Note 4'],
|
||||
[10,105,0,0,'2000-01-05 05:00:00',''],
|
||||
[11, 19,0,0,'2017-01-01 00:00:00','ook'],
|
||||
[11, 20,1,0,'2017-01-01 00:00:00','eek'],
|
||||
[12, 3,0,1,'2017-01-01 00:00:00','ack'],
|
||||
[12, 4,1,1,'2017-01-01 00:00:00','ach'],
|
||||
[1, 2,0,0,'2010-01-01 00:00:00','Some Note'],
|
||||
]
|
||||
],
|
||||
'arsse_categories' => [ // author-supplied categories
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'name' => "str",
|
||||
'arsse_marks' => [
|
||||
'columns' => [
|
||||
'subscription' => "int",
|
||||
'article' => "int",
|
||||
'read' => "bool",
|
||||
'starred' => "bool",
|
||||
'modified' => "datetime",
|
||||
'note' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1,'2000-01-01 00:00:00',''],
|
||||
[5, 19,1,0,'2016-01-01 00:00:00',''],
|
||||
[5, 20,0,1,'2005-01-01 00:00:00',''],
|
||||
[7, 20,1,0,'2010-01-01 00:00:00',''],
|
||||
[8, 102,1,0,'2000-01-02 02:00:00','Note 2'],
|
||||
[9, 103,0,1,'2000-01-03 03:00:00','Note 3'],
|
||||
[9, 104,1,1,'2000-01-04 04:00:00','Note 4'],
|
||||
[10,105,0,0,'2000-01-05 05:00:00',''],
|
||||
[11, 19,0,0,'2017-01-01 00:00:00','ook'],
|
||||
[11, 20,1,0,'2017-01-01 00:00:00','eek'],
|
||||
[12, 3,0,1,'2017-01-01 00:00:00','ack'],
|
||||
[12, 4,1,1,'2017-01-01 00:00:00','ach'],
|
||||
[1, 2,0,0,'2010-01-01 00:00:00','Some Note'],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[19,"Fascinating"],
|
||||
[19,"Logical"],
|
||||
[20,"Interesting"],
|
||||
[20,"Logical"],
|
||||
'arsse_categories' => [ // author-supplied categories
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'name' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[19,"Fascinating"],
|
||||
[19,"Logical"],
|
||||
[20,"Interesting"],
|
||||
[20,"Logical"],
|
||||
],
|
||||
],
|
||||
],
|
||||
'arsse_labels' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'name' => "str",
|
||||
'arsse_labels' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'name' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com","Interesting"],
|
||||
[2,"john.doe@example.com","Fascinating"],
|
||||
[3,"jane.doe@example.com","Boring"],
|
||||
[4,"john.doe@example.com","Lonely"],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com","Interesting"],
|
||||
[2,"john.doe@example.com","Fascinating"],
|
||||
[3,"jane.doe@example.com","Boring"],
|
||||
[4,"john.doe@example.com","Lonely"],
|
||||
'arsse_label_members' => [
|
||||
'columns' => [
|
||||
'label' => "int",
|
||||
'article' => "int",
|
||||
'subscription' => "int",
|
||||
'assigned' => "bool",
|
||||
'modified' => "datetime",
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1,'2000-01-01 00:00:00'],
|
||||
[2, 1,1,1,'2000-01-01 00:00:00'],
|
||||
[1,19,5,1,'2000-01-01 00:00:00'],
|
||||
[2,20,5,1,'2000-01-01 00:00:00'],
|
||||
[1, 5,3,0,'2000-01-01 00:00:00'],
|
||||
[2, 5,3,1,'2000-01-01 00:00:00'],
|
||||
[4, 7,4,0,'2000-01-01 00:00:00'],
|
||||
[4, 8,4,1,'2015-01-01 00:00:00'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'arsse_label_members' => [
|
||||
'columns' => [
|
||||
'label' => "int",
|
||||
'article' => "int",
|
||||
'subscription' => "int",
|
||||
'assigned' => "bool",
|
||||
'modified' => "datetime",
|
||||
];
|
||||
$this->matches = [
|
||||
[
|
||||
'id' => 101,
|
||||
'url' => 'http://example.com/1',
|
||||
'title' => 'Article title 1',
|
||||
'subscription_title' => "Feed 11",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 1</p>',
|
||||
'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda',
|
||||
'published_date' => '2000-01-01 00:00:00',
|
||||
'edited_date' => '2000-01-01 00:00:01',
|
||||
'modified_date' => '2000-01-01 01:00:00',
|
||||
'unread' => 1,
|
||||
'starred' => 0,
|
||||
'edition' => 101,
|
||||
'subscription' => 8,
|
||||
'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',
|
||||
'media_url' => null,
|
||||
'media_type' => null,
|
||||
'note' => "",
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1,'2000-01-01 00:00:00'],
|
||||
[2, 1,1,1,'2000-01-01 00:00:00'],
|
||||
[1,19,5,1,'2000-01-01 00:00:00'],
|
||||
[2,20,5,1,'2000-01-01 00:00:00'],
|
||||
[1, 5,3,0,'2000-01-01 00:00:00'],
|
||||
[2, 5,3,1,'2000-01-01 00:00:00'],
|
||||
[4, 7,4,0,'2000-01-01 00:00:00'],
|
||||
[4, 8,4,1,'2015-01-01 00:00:00'],
|
||||
[
|
||||
'id' => 102,
|
||||
'url' => 'http://example.com/2',
|
||||
'title' => 'Article title 2',
|
||||
'subscription_title' => "Feed 11",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 2</p>',
|
||||
'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7',
|
||||
'published_date' => '2000-01-02 00:00:00',
|
||||
'edited_date' => '2000-01-02 00:00:02',
|
||||
'modified_date' => '2000-01-02 02:00:00',
|
||||
'unread' => 0,
|
||||
'starred' => 0,
|
||||
'edition' => 202,
|
||||
'subscription' => 8,
|
||||
'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',
|
||||
'media_url' => "http://example.com/text",
|
||||
'media_type' => "text/plain",
|
||||
'note' => "Note 2",
|
||||
],
|
||||
],
|
||||
];
|
||||
protected $matches = [
|
||||
[
|
||||
'id' => 101,
|
||||
'url' => 'http://example.com/1',
|
||||
'title' => 'Article title 1',
|
||||
'subscription_title' => "Feed 11",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 1</p>',
|
||||
'guid' => 'e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda',
|
||||
'published_date' => '2000-01-01 00:00:00',
|
||||
'edited_date' => '2000-01-01 00:00:01',
|
||||
'modified_date' => '2000-01-01 01:00:00',
|
||||
'unread' => 1,
|
||||
'starred' => 0,
|
||||
'edition' => 101,
|
||||
'subscription' => 8,
|
||||
'fingerprint' => 'f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6:fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4:18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207',
|
||||
'media_url' => null,
|
||||
'media_type' => null,
|
||||
'note' => "",
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'url' => 'http://example.com/2',
|
||||
'title' => 'Article title 2',
|
||||
'subscription_title' => "Feed 11",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 2</p>',
|
||||
'guid' => '5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7',
|
||||
'published_date' => '2000-01-02 00:00:00',
|
||||
'edited_date' => '2000-01-02 00:00:02',
|
||||
'modified_date' => '2000-01-02 02:00:00',
|
||||
'unread' => 0,
|
||||
'starred' => 0,
|
||||
'edition' => 202,
|
||||
'subscription' => 8,
|
||||
'fingerprint' => '0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153:13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9:2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e',
|
||||
'media_url' => "http://example.com/text",
|
||||
'media_type' => "text/plain",
|
||||
'note' => "Note 2",
|
||||
],
|
||||
[
|
||||
'id' => 103,
|
||||
'url' => 'http://example.com/3',
|
||||
'title' => 'Article title 3',
|
||||
'subscription_title' => "Subscription 9",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 3</p>',
|
||||
'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92',
|
||||
'published_date' => '2000-01-03 00:00:00',
|
||||
'edited_date' => '2000-01-03 00:00:03',
|
||||
'modified_date' => '2000-01-03 03:00:00',
|
||||
'unread' => 1,
|
||||
'starred' => 1,
|
||||
'edition' => 203,
|
||||
'subscription' => 9,
|
||||
'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',
|
||||
'media_url' => "http://example.com/video",
|
||||
'media_type' => "video/webm",
|
||||
'note' => "Note 3",
|
||||
],
|
||||
[
|
||||
'id' => 104,
|
||||
'url' => 'http://example.com/4',
|
||||
'title' => 'Article title 4',
|
||||
'subscription_title' => "Subscription 9",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 4</p>',
|
||||
'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180',
|
||||
'published_date' => '2000-01-04 00:00:00',
|
||||
'edited_date' => '2000-01-04 00:00:04',
|
||||
'modified_date' => '2000-01-04 04:00:00',
|
||||
'unread' => 0,
|
||||
'starred' => 1,
|
||||
'edition' => 204,
|
||||
'subscription' => 9,
|
||||
'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',
|
||||
'media_url' => "http://example.com/image",
|
||||
'media_type' => "image/svg+xml",
|
||||
'note' => "Note 4",
|
||||
],
|
||||
[
|
||||
'id' => 105,
|
||||
'url' => 'http://example.com/5',
|
||||
'title' => 'Article title 5',
|
||||
'subscription_title' => "Feed 13",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 5</p>',
|
||||
'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41',
|
||||
'published_date' => '2000-01-05 00:00:00',
|
||||
'edited_date' => '2000-01-05 00:00:05',
|
||||
'modified_date' => '2000-01-05 05:00:00',
|
||||
'unread' => 1,
|
||||
'starred' => 0,
|
||||
'edition' => 305,
|
||||
'subscription' => 10,
|
||||
'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',
|
||||
'media_url' => "http://example.com/audio",
|
||||
'media_type' => "audio/ogg",
|
||||
'note' => "",
|
||||
],
|
||||
];
|
||||
protected $fields = [
|
||||
Database::LIST_MINIMAL => [
|
||||
"id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date",
|
||||
],
|
||||
Database::LIST_CONSERVATIVE => [
|
||||
"id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date",
|
||||
"url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint",
|
||||
],
|
||||
Database::LIST_TYPICAL => [
|
||||
"id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date",
|
||||
"url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint",
|
||||
"content", "media_url", "media_type",
|
||||
],
|
||||
Database::LIST_FULL => [
|
||||
[
|
||||
'id' => 103,
|
||||
'url' => 'http://example.com/3',
|
||||
'title' => 'Article title 3',
|
||||
'subscription_title' => "Subscription 9",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 3</p>',
|
||||
'guid' => '31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92',
|
||||
'published_date' => '2000-01-03 00:00:00',
|
||||
'edited_date' => '2000-01-03 00:00:03',
|
||||
'modified_date' => '2000-01-03 03:00:00',
|
||||
'unread' => 1,
|
||||
'starred' => 1,
|
||||
'edition' => 203,
|
||||
'subscription' => 9,
|
||||
'fingerprint' => 'f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b:b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406:ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b',
|
||||
'media_url' => "http://example.com/video",
|
||||
'media_type' => "video/webm",
|
||||
'note' => "Note 3",
|
||||
],
|
||||
[
|
||||
'id' => 104,
|
||||
'url' => 'http://example.com/4',
|
||||
'title' => 'Article title 4',
|
||||
'subscription_title' => "Subscription 9",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 4</p>',
|
||||
'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180',
|
||||
'published_date' => '2000-01-04 00:00:00',
|
||||
'edited_date' => '2000-01-04 00:00:04',
|
||||
'modified_date' => '2000-01-04 04:00:00',
|
||||
'unread' => 0,
|
||||
'starred' => 1,
|
||||
'edition' => 204,
|
||||
'subscription' => 9,
|
||||
'fingerprint' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8:f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3:ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',
|
||||
'media_url' => "http://example.com/image",
|
||||
'media_type' => "image/svg+xml",
|
||||
'note' => "Note 4",
|
||||
],
|
||||
[
|
||||
'id' => 105,
|
||||
'url' => 'http://example.com/5',
|
||||
'title' => 'Article title 5',
|
||||
'subscription_title' => "Feed 13",
|
||||
'author' => '',
|
||||
'content' => '<p>Article content 5</p>',
|
||||
'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41',
|
||||
'published_date' => '2000-01-05 00:00:00',
|
||||
'edited_date' => '2000-01-05 00:00:05',
|
||||
'modified_date' => '2000-01-05 05:00:00',
|
||||
'unread' => 1,
|
||||
'starred' => 0,
|
||||
'edition' => 305,
|
||||
'subscription' => 10,
|
||||
'fingerprint' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022:834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900:43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',
|
||||
'media_url' => "http://example.com/audio",
|
||||
'media_type' => "audio/ogg",
|
||||
'note' => "",
|
||||
],
|
||||
];
|
||||
$this->fields = [
|
||||
"id", "subscription", "feed", "modified_date", "marked_date", "unread", "starred", "edition", "edited_date",
|
||||
"url", "title", "subscription_title", "author", "guid", "published_date", "fingerprint",
|
||||
"content", "media_url", "media_type",
|
||||
"note",
|
||||
],
|
||||
];
|
||||
|
||||
public function setUpSeries() {
|
||||
];
|
||||
$this->checkTables = ['arsse_marks' => ["subscription","article","read","starred","modified","note"],];
|
||||
$this->user = "john.doe@example.net";
|
||||
}
|
||||
|
||||
protected function compareIds(array $exp, Context $c) {
|
||||
$ids = array_column($ids = Arsse::$db->articleList($this->user, $c)->getAll(), "id");
|
||||
sort($ids);
|
||||
sort($exp);
|
||||
$this->assertEquals($exp, $ids);
|
||||
protected function tearDownSeriesArticle() {
|
||||
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)));
|
||||
}
|
||||
|
||||
public function testListArticlesCheckingContext() {
|
||||
$this->user = "john.doe@example.com";
|
||||
$compareIds = function(array $exp, Context $c) {
|
||||
$ids = array_column($ids = Arsse::$db->articleList("john.doe@example.com", $c)->getAll(), "id");
|
||||
sort($ids);
|
||||
sort($exp);
|
||||
$this->assertEquals($exp, $ids);
|
||||
};
|
||||
// get all items for user
|
||||
$exp = [1,2,3,4,5,6,7,8,19,20];
|
||||
$this->compareIds($exp, new Context);
|
||||
$this->compareIds($exp, (new Context)->articles(range(1, Database::LIMIT_ARTICLES * 3)));
|
||||
$compareIds($exp, new Context);
|
||||
$compareIds($exp, (new Context)->articles(range(1, Database::LIMIT_ARTICLES * 3)));
|
||||
// get items from a folder tree
|
||||
$this->compareIds([5,6,7,8], (new Context)->folder(1));
|
||||
$compareIds([5,6,7,8], (new Context)->folder(1));
|
||||
// get items from a leaf folder
|
||||
$this->compareIds([7,8], (new Context)->folder(6));
|
||||
$compareIds([7,8], (new Context)->folder(6));
|
||||
// get items from a non-leaf folder without descending
|
||||
$this->compareIds([1,2,3,4], (new Context)->folderShallow(0));
|
||||
$this->compareIds([5,6], (new Context)->folderShallow(1));
|
||||
$compareIds([1,2,3,4], (new Context)->folderShallow(0));
|
||||
$compareIds([5,6], (new Context)->folderShallow(1));
|
||||
// get items from a single subscription
|
||||
$exp = [19,20];
|
||||
$this->compareIds($exp, (new Context)->subscription(5));
|
||||
$compareIds($exp, (new Context)->subscription(5));
|
||||
// get un/read items from a single subscription
|
||||
$this->compareIds([20], (new Context)->subscription(5)->unread(true));
|
||||
$this->compareIds([19], (new Context)->subscription(5)->unread(false));
|
||||
$compareIds([20], (new Context)->subscription(5)->unread(true));
|
||||
$compareIds([19], (new Context)->subscription(5)->unread(false));
|
||||
// get starred articles
|
||||
$this->compareIds([1,20], (new Context)->starred(true));
|
||||
$this->compareIds([2,3,4,5,6,7,8,19], (new Context)->starred(false));
|
||||
$this->compareIds([1], (new Context)->starred(true)->unread(false));
|
||||
$this->compareIds([], (new Context)->starred(true)->unread(false)->subscription(5));
|
||||
$compareIds([1,20], (new Context)->starred(true));
|
||||
$compareIds([2,3,4,5,6,7,8,19], (new Context)->starred(false));
|
||||
$compareIds([1], (new Context)->starred(true)->unread(false));
|
||||
$compareIds([], (new Context)->starred(true)->unread(false)->subscription(5));
|
||||
// get items relative to edition
|
||||
$this->compareIds([19], (new Context)->subscription(5)->latestEdition(999));
|
||||
$this->compareIds([19], (new Context)->subscription(5)->latestEdition(19));
|
||||
$this->compareIds([20], (new Context)->subscription(5)->oldestEdition(999));
|
||||
$this->compareIds([20], (new Context)->subscription(5)->oldestEdition(1001));
|
||||
$compareIds([19], (new Context)->subscription(5)->latestEdition(999));
|
||||
$compareIds([19], (new Context)->subscription(5)->latestEdition(19));
|
||||
$compareIds([20], (new Context)->subscription(5)->oldestEdition(999));
|
||||
$compareIds([20], (new Context)->subscription(5)->oldestEdition(1001));
|
||||
// get items relative to article ID
|
||||
$this->compareIds([1,2,3], (new Context)->latestArticle(3));
|
||||
$this->compareIds([19,20], (new Context)->oldestArticle(19));
|
||||
$compareIds([1,2,3], (new Context)->latestArticle(3));
|
||||
$compareIds([19,20], (new Context)->oldestArticle(19));
|
||||
// get items relative to (feed) modification date
|
||||
$exp = [2,4,6,8,20];
|
||||
$this->compareIds($exp, (new Context)->modifiedSince("2005-01-01T00:00:00Z"));
|
||||
$this->compareIds($exp, (new Context)->modifiedSince("2010-01-01T00:00:00Z"));
|
||||
$compareIds($exp, (new Context)->modifiedSince("2005-01-01T00:00:00Z"));
|
||||
$compareIds($exp, (new Context)->modifiedSince("2010-01-01T00:00:00Z"));
|
||||
$exp = [1,3,5,7,19];
|
||||
$this->compareIds($exp, (new Context)->notModifiedSince("2005-01-01T00:00:00Z"));
|
||||
$this->compareIds($exp, (new Context)->notModifiedSince("2000-01-01T00:00:00Z"));
|
||||
$compareIds($exp, (new Context)->notModifiedSince("2005-01-01T00:00:00Z"));
|
||||
$compareIds($exp, (new Context)->notModifiedSince("2000-01-01T00:00:00Z"));
|
||||
// get items relative to (user) modification date (both marks and labels apply)
|
||||
$this->compareIds([8,19], (new Context)->markedSince("2014-01-01T00:00:00Z"));
|
||||
$this->compareIds([2,4,6,8,19,20], (new Context)->markedSince("2010-01-01T00:00:00Z"));
|
||||
$this->compareIds([1,2,3,4,5,6,7,20], (new Context)->notMarkedSince("2014-01-01T00:00:00Z"));
|
||||
$this->compareIds([1,3,5,7], (new Context)->notMarkedSince("2005-01-01T00:00:00Z"));
|
||||
$compareIds([8,19], (new Context)->markedSince("2014-01-01T00:00:00Z"));
|
||||
$compareIds([2,4,6,8,19,20], (new Context)->markedSince("2010-01-01T00:00:00Z"));
|
||||
$compareIds([1,2,3,4,5,6,7,20], (new Context)->notMarkedSince("2014-01-01T00:00:00Z"));
|
||||
$compareIds([1,3,5,7], (new Context)->notMarkedSince("2005-01-01T00:00:00Z"));
|
||||
// paged results
|
||||
$this->compareIds([1], (new Context)->limit(1));
|
||||
$this->compareIds([2], (new Context)->limit(1)->oldestEdition(1+1));
|
||||
$this->compareIds([3], (new Context)->limit(1)->oldestEdition(2+1));
|
||||
$this->compareIds([4,5], (new Context)->limit(2)->oldestEdition(3+1));
|
||||
$compareIds([1], (new Context)->limit(1));
|
||||
$compareIds([2], (new Context)->limit(1)->oldestEdition(1+1));
|
||||
$compareIds([3], (new Context)->limit(1)->oldestEdition(2+1));
|
||||
$compareIds([4,5], (new Context)->limit(2)->oldestEdition(3+1));
|
||||
// reversed results
|
||||
$this->compareIds([20], (new Context)->reverse(true)->limit(1));
|
||||
$this->compareIds([19], (new Context)->reverse(true)->limit(1)->latestEdition(1001-1));
|
||||
$this->compareIds([8], (new Context)->reverse(true)->limit(1)->latestEdition(19-1));
|
||||
$this->compareIds([7,6], (new Context)->reverse(true)->limit(2)->latestEdition(8-1));
|
||||
$compareIds([20], (new Context)->reverse(true)->limit(1));
|
||||
$compareIds([19], (new Context)->reverse(true)->limit(1)->latestEdition(1001-1));
|
||||
$compareIds([8], (new Context)->reverse(true)->limit(1)->latestEdition(19-1));
|
||||
$compareIds([7,6], (new Context)->reverse(true)->limit(2)->latestEdition(8-1));
|
||||
// get articles by label ID
|
||||
$this->compareIds([1,19], (new Context)->label(1));
|
||||
$this->compareIds([1,5,20], (new Context)->label(2));
|
||||
$compareIds([1,19], (new Context)->label(1));
|
||||
$compareIds([1,5,20], (new Context)->label(2));
|
||||
// get articles by label name
|
||||
$this->compareIds([1,19], (new Context)->labelName("Interesting"));
|
||||
$this->compareIds([1,5,20], (new Context)->labelName("Fascinating"));
|
||||
$compareIds([1,19], (new Context)->labelName("Interesting"));
|
||||
$compareIds([1,5,20], (new Context)->labelName("Fascinating"));
|
||||
// get articles with any or no label
|
||||
$this->compareIds([1,5,8,19,20], (new Context)->labelled(true));
|
||||
$this->compareIds([2,3,4,6,7], (new Context)->labelled(false));
|
||||
$compareIds([1,5,8,19,20], (new Context)->labelled(true));
|
||||
$compareIds([2,3,4,6,7], (new Context)->labelled(false));
|
||||
// get a specific article or edition
|
||||
$this->compareIds([20], (new Context)->article(20));
|
||||
$this->compareIds([20], (new Context)->edition(1001));
|
||||
$compareIds([20], (new Context)->article(20));
|
||||
$compareIds([20], (new Context)->edition(1001));
|
||||
// get multiple specific articles or editions
|
||||
$this->compareIds([1,20], (new Context)->articles([1,20,50]));
|
||||
$this->compareIds([1,20], (new Context)->editions([1,1001,50]));
|
||||
$compareIds([1,20], (new Context)->articles([1,20,50]));
|
||||
$compareIds([1,20], (new Context)->editions([1,1001,50]));
|
||||
// get articles base on whether or not they have notes
|
||||
$this->compareIds([1,3,4,5,6,7,8,19,20], (new Context)->annotated(false));
|
||||
$this->compareIds([2], (new Context)->annotated(true));
|
||||
$compareIds([1,3,4,5,6,7,8,19,20], (new Context)->annotated(false));
|
||||
$compareIds([2], (new Context)->annotated(true));
|
||||
// get specific starred articles
|
||||
$this->compareIds([1], (new Context)->articles([1,2,3])->starred(true));
|
||||
$this->compareIds([2,3], (new Context)->articles([1,2,3])->starred(false));
|
||||
$compareIds([1], (new Context)->articles([1,2,3])->starred(true));
|
||||
$compareIds([2,3], (new Context)->articles([1,2,3])->starred(false));
|
||||
}
|
||||
|
||||
public function testListArticlesOfAMissingFolder() {
|
||||
|
@ -484,17 +508,15 @@ trait SeriesArticle {
|
|||
|
||||
public function testListArticlesCheckingProperties() {
|
||||
$this->user = "john.doe@example.org";
|
||||
$this->assertResult($this->matches, Arsse::$db->articleList($this->user));
|
||||
// check that the different fieldset groups return the expected columns
|
||||
foreach ($this->fields as $constant => $columns) {
|
||||
$test = array_keys(Arsse::$db->articleList($this->user, (new Context)->article(101), $constant)->getRow());
|
||||
sort($columns);
|
||||
sort($test);
|
||||
$this->assertEquals($columns, $test, "Fields do not match expectation for verbosity $constant");
|
||||
foreach ($this->fields as $column) {
|
||||
$test = array_keys(Arsse::$db->articleList($this->user, (new Context)->article(101), [$column])->getRow());
|
||||
$this->assertEquals([$column], $test);
|
||||
}
|
||||
// check that an unknown fieldset produces an exception
|
||||
$this->assertException("constantUnknown");
|
||||
Arsse::$db->articleList($this->user, (new Context)->article(101), \PHP_INT_MAX);
|
||||
// check that an unknown field is silently ignored
|
||||
$columns = array_merge($this->fields, ["unknown_column", "bogus_column"]);
|
||||
$test = array_keys(Arsse::$db->articleList($this->user, (new Context)->article(101), $columns)->getRow());
|
||||
$this->assertEquals($this->fields, $test);
|
||||
}
|
||||
|
||||
public function testListArticlesWithoutAuthority() {
|
||||
|
@ -503,6 +525,10 @@ trait SeriesArticle {
|
|||
Arsse::$db->articleList($this->user);
|
||||
}
|
||||
|
||||
public function testMarkNothing() {
|
||||
$this->assertSame(0, Arsse::$db->articleMark($this->user, []));
|
||||
}
|
||||
|
||||
public function testMarkAllArticlesUnread() {
|
||||
Arsse::$db->articleMark($this->user, ['read'=>false]);
|
||||
$now = Date::transform(time(), "sql");
|
||||
|
@ -746,6 +772,12 @@ trait SeriesArticle {
|
|||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
public function testMarkMultipleMissingEditions() {
|
||||
$this->assertSame(0, Arsse::$db->articleMark($this->user, ['starred'=>true], (new Context)->editions([500,501])));
|
||||
$state = $this->primeExpectations($this->data, $this->checkTables);
|
||||
$this->compareExpectations($state);
|
||||
}
|
||||
|
||||
public function testMarkMultipleEditionsUnread() {
|
||||
Arsse::$db->articleMark($this->user, ['read'=>false], (new Context)->editions([2,4,7,1001]));
|
||||
$now = Date::transform(time(), "sql");
|
||||
|
@ -915,7 +947,7 @@ trait SeriesArticle {
|
|||
}
|
||||
|
||||
public function testListTheLabelsOfAnArticle() {
|
||||
$this->assertEquals([2,1], Arsse::$db->articleLabelsGet("john.doe@example.com", 1));
|
||||
$this->assertEquals([1,2], Arsse::$db->articleLabelsGet("john.doe@example.com", 1));
|
||||
$this->assertEquals([2], Arsse::$db->articleLabelsGet("john.doe@example.com", 5));
|
||||
$this->assertEquals([], Arsse::$db->articleLabelsGet("john.doe@example.com", 2));
|
||||
$this->assertEquals(["Fascinating","Interesting"], Arsse::$db->articleLabelsGet("john.doe@example.com", 1, true));
|
|
@ -4,13 +4,13 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use Phake;
|
||||
|
||||
trait SeriesCleanup {
|
||||
public function setUpSeries() {
|
||||
protected function setUpSeriesCleanup() {
|
||||
// set up the configuration
|
||||
Arsse::$conf->import([
|
||||
'userSessionTimeout' => "PT1H",
|
||||
|
@ -135,6 +135,10 @@ trait SeriesCleanup {
|
|||
];
|
||||
}
|
||||
|
||||
protected function tearDownSeriesCleanup() {
|
||||
unset($this->data);
|
||||
}
|
||||
|
||||
public function testCleanUpOrphanedFeeds() {
|
||||
Arsse::$db->feedCleanup();
|
||||
$now = gmdate("Y-m-d H:i:s");
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Feed;
|
||||
|
@ -12,26 +12,7 @@ use JKingWeb\Arsse\Feed\Exception as FeedException;
|
|||
use Phake;
|
||||
|
||||
trait SeriesFeed {
|
||||
protected $matches = [
|
||||
[
|
||||
'id' => 4,
|
||||
'edited' => '2000-01-04 00:00:00',
|
||||
'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180',
|
||||
'url_title_hash' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8',
|
||||
'url_content_hash' => 'f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3',
|
||||
'title_content_hash' => 'ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'edited' => '2000-01-05 00:00:00',
|
||||
'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41',
|
||||
'url_title_hash' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022',
|
||||
'url_content_hash' => '834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900',
|
||||
'title_content_hash' => '43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',
|
||||
],
|
||||
];
|
||||
|
||||
public function setUpSeries() {
|
||||
protected function setUpSeriesFeed() {
|
||||
// set up the test data
|
||||
$past = gmdate("Y-m-d H:i:s", strtotime("now - 1 minute"));
|
||||
$future = gmdate("Y-m-d H:i:s", strtotime("now + 1 minute"));
|
||||
|
@ -163,6 +144,28 @@ trait SeriesFeed {
|
|||
]
|
||||
],
|
||||
];
|
||||
$this->matches = [
|
||||
[
|
||||
'id' => 4,
|
||||
'edited' => '2000-01-04 00:00:00',
|
||||
'guid' => '804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180',
|
||||
'url_title_hash' => 'f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8',
|
||||
'url_content_hash' => 'f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3',
|
||||
'title_content_hash' => 'ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9',
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'edited' => '2000-01-05 00:00:00',
|
||||
'guid' => 'db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41',
|
||||
'url_title_hash' => 'd40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022',
|
||||
'url_content_hash' => '834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900',
|
||||
'title_content_hash' => '43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function tearDownSeriesFeed() {
|
||||
unset($this->data, $this->matches);
|
||||
}
|
||||
|
||||
public function testListLatestItems() {
|
|
@ -4,51 +4,57 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use Phake;
|
||||
|
||||
trait SeriesFolder {
|
||||
protected $data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
protected function setUpSeriesFolder() {
|
||||
$this->data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
],
|
||||
/* Layout translates to:
|
||||
Jane
|
||||
Politics
|
||||
John
|
||||
Technology
|
||||
Software
|
||||
Politics
|
||||
Rocketry
|
||||
Politics
|
||||
*/
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
]
|
||||
],
|
||||
],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
],
|
||||
/* Layout translates to:
|
||||
Jane
|
||||
Politics
|
||||
John
|
||||
Technology
|
||||
Software
|
||||
Politics
|
||||
Rocketry
|
||||
Politics
|
||||
*/
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
]
|
||||
],
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
protected function tearDownSeriesFolder() {
|
||||
unset($this->data);
|
||||
}
|
||||
|
||||
public function testAddARootFolder() {
|
||||
$user = "john.doe@example.com";
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Misc\Context;
|
||||
|
@ -12,241 +12,244 @@ use JKingWeb\Arsse\Misc\Date;
|
|||
use Phake;
|
||||
|
||||
trait SeriesLabel {
|
||||
protected $data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
protected function setUpSeriesLabel() {
|
||||
$this->data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
["john.doe@example.org", "", "John Doe"],
|
||||
["john.doe@example.net", "", "John Doe"],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
["john.doe@example.org", "", "John Doe"],
|
||||
["john.doe@example.net", "", "John Doe"],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
[7, "john.doe@example.net", null, "Technology"],
|
||||
[8, "john.doe@example.net", 7, "Software"],
|
||||
[9, "john.doe@example.net", null, "Politics"],
|
||||
]
|
||||
],
|
||||
],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
'arsse_feeds' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1,"http://example.com/1"],
|
||||
[2,"http://example.com/2"],
|
||||
[3,"http://example.com/3"],
|
||||
[4,"http://example.com/4"],
|
||||
[5,"http://example.com/5"],
|
||||
[6,"http://example.com/6"],
|
||||
[7,"http://example.com/7"],
|
||||
[8,"http://example.com/8"],
|
||||
[9,"http://example.com/9"],
|
||||
[10,"http://example.com/10"],
|
||||
[11,"http://example.com/11"],
|
||||
[12,"http://example.com/12"],
|
||||
[13,"http://example.com/13"],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
[7, "john.doe@example.net", null, "Technology"],
|
||||
[8, "john.doe@example.net", 7, "Software"],
|
||||
[9, "john.doe@example.net", null, "Politics"],
|
||||
]
|
||||
],
|
||||
'arsse_feeds' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
'arsse_subscriptions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'feed' => "int",
|
||||
'folder' => "int",
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com",1,null],
|
||||
[2,"john.doe@example.com",2,null],
|
||||
[3,"john.doe@example.com",3,1],
|
||||
[4,"john.doe@example.com",4,6],
|
||||
[5,"john.doe@example.com",10,5],
|
||||
[6,"jane.doe@example.com",1,null],
|
||||
[7,"jane.doe@example.com",10,null],
|
||||
[8,"john.doe@example.org",11,null],
|
||||
[9,"john.doe@example.org",12,null],
|
||||
[10,"john.doe@example.org",13,null],
|
||||
[11,"john.doe@example.net",10,null],
|
||||
[12,"john.doe@example.net",2,9],
|
||||
[13,"john.doe@example.net",3,8],
|
||||
[14,"john.doe@example.net",4,7],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1,"http://example.com/1"],
|
||||
[2,"http://example.com/2"],
|
||||
[3,"http://example.com/3"],
|
||||
[4,"http://example.com/4"],
|
||||
[5,"http://example.com/5"],
|
||||
[6,"http://example.com/6"],
|
||||
[7,"http://example.com/7"],
|
||||
[8,"http://example.com/8"],
|
||||
[9,"http://example.com/9"],
|
||||
[10,"http://example.com/10"],
|
||||
[11,"http://example.com/11"],
|
||||
[12,"http://example.com/12"],
|
||||
[13,"http://example.com/13"],
|
||||
]
|
||||
],
|
||||
'arsse_subscriptions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'feed' => "int",
|
||||
'folder' => "int",
|
||||
'arsse_articles' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'feed' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'author' => "str",
|
||||
'published' => "datetime",
|
||||
'edited' => "datetime",
|
||||
'content' => "str",
|
||||
'guid' => "str",
|
||||
'url_title_hash' => "str",
|
||||
'url_content_hash' => "str",
|
||||
'title_content_hash' => "str",
|
||||
'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"],
|
||||
[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"],
|
||||
[7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','<p>Article content 1</p>','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'],
|
||||
[102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','<p>Article content 2</p>','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'],
|
||||
[103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','<p>Article content 3</p>','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'],
|
||||
[104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','<p>Article content 4</p>','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'],
|
||||
[105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','<p>Article content 5</p>','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com",1,null],
|
||||
[2,"john.doe@example.com",2,null],
|
||||
[3,"john.doe@example.com",3,1],
|
||||
[4,"john.doe@example.com",4,6],
|
||||
[5,"john.doe@example.com",10,5],
|
||||
[6,"jane.doe@example.com",1,null],
|
||||
[7,"jane.doe@example.com",10,null],
|
||||
[8,"john.doe@example.org",11,null],
|
||||
[9,"john.doe@example.org",12,null],
|
||||
[10,"john.doe@example.org",13,null],
|
||||
[11,"john.doe@example.net",10,null],
|
||||
[12,"john.doe@example.net",2,9],
|
||||
[13,"john.doe@example.net",3,8],
|
||||
[14,"john.doe@example.net",4,7],
|
||||
]
|
||||
],
|
||||
'arsse_articles' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'feed' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'author' => "str",
|
||||
'published' => "datetime",
|
||||
'edited' => "datetime",
|
||||
'content' => "str",
|
||||
'guid' => "str",
|
||||
'url_title_hash' => "str",
|
||||
'url_content_hash' => "str",
|
||||
'title_content_hash' => "str",
|
||||
'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"],
|
||||
[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"],
|
||||
[7,4,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[8,4,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[9,5,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[10,5,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[11,6,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[12,6,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[13,7,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[14,7,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[15,8,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[16,8,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[17,9,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[18,9,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[19,10,null,null,null,null,null,null,null,"","","","2000-01-01T00:00:00Z"],
|
||||
[20,10,null,null,null,null,null,null,null,"","","","2010-01-01T00:00:00Z"],
|
||||
[101,11,'http://example.com/1','Article title 1','','2000-01-01 00:00:00','2000-01-01 00:00:01','<p>Article content 1</p>','e433653cef2e572eee4215fa299a4a5af9137b2cefd6283c85bd69a32915beda','f5cb8bfc1c7396dc9816af212a3e2ac5221585c2a00bf7ccb6aabd95dcfcd6a6','fb0bc8f8cb08913dc5a497db700e327f1d34e4987402687d494a5891f24714d4','18fdd4fa93d693128c43b004399e5c9cea6c261ddfa002518d3669f55d8c2207','2000-01-01 01:00:00'],
|
||||
[102,11,'http://example.com/2','Article title 2','','2000-01-02 00:00:00','2000-01-02 00:00:02','<p>Article content 2</p>','5be8a5a46ecd52ed132191c8d27fb1af6b3d4edc00234c5d9f8f0e10562ed3b7','0e86d2de822a174fe3c44a466953e63ca1f1a58a19cbf475fce0855d4e3d5153','13075894189c47ffcfafd1dfe7fbb539f7c74a69d35a399b3abf8518952714f9','2abd0a8cba83b8214a66c8f0293ba63e467d720540e29ff8ddcdab069d4f1c9e','2000-01-02 02:00:00'],
|
||||
[103,12,'http://example.com/3','Article title 3','','2000-01-03 00:00:00','2000-01-03 00:00:03','<p>Article content 3</p>','31a6594500a48b59fcc8a075ce82b946c9c3c782460d088bd7b8ef3ede97ad92','f74b06b240bd08abf4d3fdfc20dba6a6f6eb8b4f1a00e9a617efd63a87180a4b','b278380e984cefe63f0e412b88ffc9cb0befdfa06fdc00bace1da99a8daff406','ad622b31e739cd3a3f3c788991082cf4d2f7a8773773008e75f0572e58cd373b','2000-01-03 03:00:00'],
|
||||
[104,12,'http://example.com/4','Article title 4','','2000-01-04 00:00:00','2000-01-04 00:00:04','<p>Article content 4</p>','804e517d623390e71497982c77cf6823180342ebcd2e7d5e32da1e55b09dd180','f3615c7f16336d3ea242d35cf3fc17dbc4ee3afb78376bf49da2dd7a5a25dec8','f11c2b4046f207579aeb9c69a8c20ca5461cef49756ccfa5ba5e2344266da3b3','ab2da63276acce431250b18d3d49b988b226a99c7faadf275c90b751aee05be9','2000-01-04 04:00:00'],
|
||||
[105,13,'http://example.com/5','Article title 5','','2000-01-05 00:00:00','2000-01-05 00:00:05','<p>Article content 5</p>','db3e736c2c492f5def5c5da33ddcbea1824040e9ced2142069276b0a6e291a41','d40da96e39eea6c55948ccbe9b3d275b5f931298288dbe953990c5f496097022','834240f84501b5341d375414718204ec421561f3825d34c22bf9182203e42900','43b970ac6ec5f8a9647b2c7e4eed8b1d7f62e154a95eed748b0294c1256764ba','2000-01-05 05:00:00'],
|
||||
]
|
||||
],
|
||||
'arsse_enclosures' => [
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'url' => "str",
|
||||
'type' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[102,"http://example.com/text","text/plain"],
|
||||
[103,"http://example.com/video","video/webm"],
|
||||
[104,"http://example.com/image","image/svg+xml"],
|
||||
[105,"http://example.com/audio","audio/ogg"],
|
||||
'arsse_enclosures' => [
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'url' => "str",
|
||||
'type' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[102,"http://example.com/text","text/plain"],
|
||||
[103,"http://example.com/video","video/webm"],
|
||||
[104,"http://example.com/image","image/svg+xml"],
|
||||
[105,"http://example.com/audio","audio/ogg"],
|
||||
|
||||
]
|
||||
],
|
||||
'arsse_editions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'article' => "int",
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[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],
|
||||
]
|
||||
],
|
||||
'arsse_marks' => [
|
||||
'columns' => [
|
||||
'subscription' => "int",
|
||||
'article' => "int",
|
||||
'read' => "bool",
|
||||
'starred' => "bool",
|
||||
'modified' => "datetime"
|
||||
'arsse_editions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'article' => "int",
|
||||
],
|
||||
'rows' => [
|
||||
[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],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1,'2000-01-01 00:00:00'],
|
||||
[5, 19,1,0,'2000-01-01 00:00:00'],
|
||||
[5, 20,0,1,'2010-01-01 00:00:00'],
|
||||
[7, 20,1,0,'2010-01-01 00:00:00'],
|
||||
[8, 102,1,0,'2000-01-02 02:00:00'],
|
||||
[9, 103,0,1,'2000-01-03 03:00:00'],
|
||||
[9, 104,1,1,'2000-01-04 04:00:00'],
|
||||
[10,105,0,0,'2000-01-05 05:00:00'],
|
||||
[11, 19,0,0,'2017-01-01 00:00:00'],
|
||||
[11, 20,1,0,'2017-01-01 00:00:00'],
|
||||
[12, 3,0,1,'2017-01-01 00:00:00'],
|
||||
[12, 4,1,1,'2017-01-01 00:00:00'],
|
||||
]
|
||||
],
|
||||
'arsse_labels' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'name' => "str",
|
||||
'arsse_marks' => [
|
||||
'columns' => [
|
||||
'subscription' => "int",
|
||||
'article' => "int",
|
||||
'read' => "bool",
|
||||
'starred' => "bool",
|
||||
'modified' => "datetime"
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1,'2000-01-01 00:00:00'],
|
||||
[5, 19,1,0,'2000-01-01 00:00:00'],
|
||||
[5, 20,0,1,'2010-01-01 00:00:00'],
|
||||
[7, 20,1,0,'2010-01-01 00:00:00'],
|
||||
[8, 102,1,0,'2000-01-02 02:00:00'],
|
||||
[9, 103,0,1,'2000-01-03 03:00:00'],
|
||||
[9, 104,1,1,'2000-01-04 04:00:00'],
|
||||
[10,105,0,0,'2000-01-05 05:00:00'],
|
||||
[11, 19,0,0,'2017-01-01 00:00:00'],
|
||||
[11, 20,1,0,'2017-01-01 00:00:00'],
|
||||
[12, 3,0,1,'2017-01-01 00:00:00'],
|
||||
[12, 4,1,1,'2017-01-01 00:00:00'],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com","Interesting"],
|
||||
[2,"john.doe@example.com","Fascinating"],
|
||||
[3,"jane.doe@example.com","Boring"],
|
||||
[4,"john.doe@example.com","Lonely"],
|
||||
'arsse_labels' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'name' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com","Interesting"],
|
||||
[2,"john.doe@example.com","Fascinating"],
|
||||
[3,"jane.doe@example.com","Boring"],
|
||||
[4,"john.doe@example.com","Lonely"],
|
||||
],
|
||||
],
|
||||
],
|
||||
'arsse_label_members' => [
|
||||
'columns' => [
|
||||
'label' => "int",
|
||||
'article' => "int",
|
||||
'subscription' => "int",
|
||||
'assigned' => "bool",
|
||||
'arsse_label_members' => [
|
||||
'columns' => [
|
||||
'label' => "int",
|
||||
'article' => "int",
|
||||
'subscription' => "int",
|
||||
'assigned' => "bool",
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1],
|
||||
[2, 1,1,1],
|
||||
[1,19,5,1],
|
||||
[2,20,5,1],
|
||||
[1, 5,3,0],
|
||||
[2, 5,3,1],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
[1, 1,1,1],
|
||||
[2, 1,1,1],
|
||||
[1,19,5,1],
|
||||
[2,20,5,1],
|
||||
[1, 5,3,0],
|
||||
[2, 5,3,1],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function setUpSeries() {
|
||||
];
|
||||
$this->checkLabels = ['arsse_labels' => ["id","owner","name"]];
|
||||
$this->checkMembers = ['arsse_label_members' => ["label","article","subscription","assigned"]];
|
||||
$this->user = "john.doe@example.com";
|
||||
}
|
||||
|
||||
protected function tearDownSeriesLabel() {
|
||||
unset($this->data, $this->checkLabels, $this->checkMembers, $this->user);
|
||||
}
|
||||
|
||||
public function testAddALabel() {
|
||||
$user = "john.doe@example.com";
|
||||
$labelID = $this->nextID("arsse_labels");
|
|
@ -4,32 +4,35 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Test\Database;
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
|
||||
trait SeriesMeta {
|
||||
protected $dataBare = [
|
||||
'arsse_meta' => [
|
||||
'columns' => [
|
||||
'key' => 'str',
|
||||
'value' => 'str',
|
||||
protected function setUpSeriesMeta() {
|
||||
$dataBare = [
|
||||
'arsse_meta' => [
|
||||
'columns' => [
|
||||
'key' => 'str',
|
||||
'value' => 'str',
|
||||
],
|
||||
'rows' => [
|
||||
//['schema_version', "".\JKingWeb\Arsse\Database::SCHEMA_VERSION],
|
||||
['album',"A Farewell to Kings"],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
//['schema_version', "".\JKingWeb\Arsse\Database::SCHEMA_VERSION],
|
||||
['album',"A Farewell to Kings"],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
public function setUpSeries() {
|
||||
];
|
||||
// the schema_version key is a special case, and to avoid jumping through hoops for every test we deal with it now
|
||||
$this->data = $this->dataBare;
|
||||
$this->data = $dataBare;
|
||||
// as far as tests are concerned the schema version is part of the expectations primed into the database
|
||||
array_unshift($this->data['arsse_meta']['rows'], ['schema_version', "".Database::SCHEMA_VERSION]);
|
||||
// but it's already been inserted by the driver, so we prime without it
|
||||
$this->primeDatabase($this->dataBare);
|
||||
$this->primeDatabase($dataBare);
|
||||
}
|
||||
|
||||
protected function tearDownSeriesMeta() {
|
||||
unset($this->data);
|
||||
}
|
||||
|
||||
public function testAddANewValue() {
|
|
@ -4,12 +4,21 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Database;
|
||||
|
||||
trait SeriesMiscellany {
|
||||
protected function setUpSeriesMiscellany() {
|
||||
static::setConf([
|
||||
'dbDriver' => static::$dbInfo->driverClass,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDownSeriesMiscellany() {
|
||||
}
|
||||
|
||||
public function testListDrivers() {
|
||||
$exp = [
|
||||
'JKingWeb\\Arsse\\Db\\SQLite3\\Driver' => Arsse::$lang->msg("Driver.Db.SQLite3.Name"),
|
||||
|
@ -18,11 +27,13 @@ trait SeriesMiscellany {
|
|||
}
|
||||
|
||||
public function testInitializeDatabase() {
|
||||
$d = new Database();
|
||||
(static::$dbInfo->razeFunction)(static::$drv);
|
||||
$d = new Database(true);
|
||||
$this->assertSame(Database::SCHEMA_VERSION, $d->driverSchemaVersion());
|
||||
}
|
||||
|
||||
public function testManuallyInitializeDatabase() {
|
||||
(static::$dbInfo->razeFunction)(static::$drv);
|
||||
$d = new Database(false);
|
||||
$this->assertSame(0, $d->driverSchemaVersion());
|
||||
$this->assertTrue($d->driverSchemaUpdate());
|
||||
|
@ -31,7 +42,6 @@ trait SeriesMiscellany {
|
|||
}
|
||||
|
||||
public function testCheckCharacterSetAcceptability() {
|
||||
$d = new Database();
|
||||
$this->assertInternalType("bool", $d->driverCharsetAcceptable());
|
||||
$this->assertInternalType("bool", Arsse::$db->driverCharsetAcceptable());
|
||||
}
|
||||
}
|
|
@ -4,16 +4,16 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Misc\Date;
|
||||
use Phake;
|
||||
|
||||
trait SeriesSession {
|
||||
public function setUpSeries() {
|
||||
protected function setUpSeriesSession() {
|
||||
// set up the configuration
|
||||
Arsse::$conf->import([
|
||||
static::setConf([
|
||||
'userSessionTimeout' => "PT1H",
|
||||
'userSessionLifetime' => "PT24H",
|
||||
]);
|
||||
|
@ -51,6 +51,10 @@ trait SeriesSession {
|
|||
];
|
||||
}
|
||||
|
||||
protected function tearDownSeriesSession() {
|
||||
unset($this->data);
|
||||
}
|
||||
|
||||
public function testResumeAValidSession() {
|
||||
$exp1 = [
|
||||
'id' => "80fa94c1a11f11e78667001e673b2560",
|
|
@ -4,7 +4,7 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Test\Database;
|
||||
|
@ -12,112 +12,115 @@ use JKingWeb\Arsse\Feed\Exception as FeedException;
|
|||
use Phake;
|
||||
|
||||
trait SeriesSubscription {
|
||||
protected $data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
public function setUpSeriesSubscription() {
|
||||
$this->data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
["jane.doe@example.com", "", "Jane Doe"],
|
||||
["john.doe@example.com", "", "John Doe"],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
]
|
||||
],
|
||||
],
|
||||
'arsse_folders' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'parent' => "int",
|
||||
'name' => "str",
|
||||
'arsse_feeds' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'username' => "str",
|
||||
'password' => "str",
|
||||
'next_fetch' => "datetime",
|
||||
'favicon' => "str",
|
||||
],
|
||||
'rows' => [] // filled in the series setup
|
||||
],
|
||||
'rows' => [
|
||||
[1, "john.doe@example.com", null, "Technology"],
|
||||
[2, "john.doe@example.com", 1, "Software"],
|
||||
[3, "john.doe@example.com", 1, "Rocketry"],
|
||||
[4, "jane.doe@example.com", null, "Politics"],
|
||||
[5, "john.doe@example.com", null, "Politics"],
|
||||
[6, "john.doe@example.com", 2, "Politics"],
|
||||
]
|
||||
],
|
||||
'arsse_feeds' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'url' => "str",
|
||||
'title' => "str",
|
||||
'username' => "str",
|
||||
'password' => "str",
|
||||
'next_fetch' => "datetime",
|
||||
'favicon' => "str",
|
||||
'arsse_subscriptions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'feed' => "int",
|
||||
'title' => "str",
|
||||
'folder' => "int",
|
||||
'pinned' => "bool",
|
||||
'order_type' => "int",
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com",2,null,null,1,2],
|
||||
[2,"jane.doe@example.com",2,null,null,0,0],
|
||||
[3,"john.doe@example.com",3,"Ook",2,0,1],
|
||||
]
|
||||
],
|
||||
'rows' => [] // filled in the series setup
|
||||
],
|
||||
'arsse_subscriptions' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'owner' => "str",
|
||||
'feed' => "int",
|
||||
'title' => "str",
|
||||
'folder' => "int",
|
||||
'pinned' => "bool",
|
||||
'order_type' => "int",
|
||||
'arsse_articles' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'feed' => "int",
|
||||
'url_title_hash' => "str",
|
||||
'url_content_hash' => "str",
|
||||
'title_content_hash' => "str",
|
||||
],
|
||||
'rows' => [
|
||||
[1,2,"","",""],
|
||||
[2,2,"","",""],
|
||||
[3,2,"","",""],
|
||||
[4,2,"","",""],
|
||||
[5,2,"","",""],
|
||||
[6,3,"","",""],
|
||||
[7,3,"","",""],
|
||||
[8,3,"","",""],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1,"john.doe@example.com",2,null,null,1,2],
|
||||
[2,"jane.doe@example.com",2,null,null,0,0],
|
||||
[3,"john.doe@example.com",3,"Ook",2,0,1],
|
||||
]
|
||||
],
|
||||
'arsse_articles' => [
|
||||
'columns' => [
|
||||
'id' => "int",
|
||||
'feed' => "int",
|
||||
'url_title_hash' => "str",
|
||||
'url_content_hash' => "str",
|
||||
'title_content_hash' => "str",
|
||||
'arsse_marks' => [
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'subscription' => "int",
|
||||
'read' => "bool",
|
||||
'starred' => "bool",
|
||||
],
|
||||
'rows' => [
|
||||
[1,2,1,0],
|
||||
[2,2,1,0],
|
||||
[3,2,1,0],
|
||||
[4,2,1,0],
|
||||
[5,2,1,0],
|
||||
[1,1,1,0],
|
||||
[7,3,1,0],
|
||||
[8,3,0,0],
|
||||
]
|
||||
],
|
||||
'rows' => [
|
||||
[1,2,"","",""],
|
||||
[2,2,"","",""],
|
||||
[3,2,"","",""],
|
||||
[4,2,"","",""],
|
||||
[5,2,"","",""],
|
||||
[6,3,"","",""],
|
||||
[7,3,"","",""],
|
||||
[8,3,"","",""],
|
||||
]
|
||||
],
|
||||
'arsse_marks' => [
|
||||
'columns' => [
|
||||
'article' => "int",
|
||||
'subscription' => "int",
|
||||
'read' => "bool",
|
||||
'starred' => "bool",
|
||||
],
|
||||
'rows' => [
|
||||
[1,2,1,0],
|
||||
[2,2,1,0],
|
||||
[3,2,1,0],
|
||||
[4,2,1,0],
|
||||
[5,2,1,0],
|
||||
[1,1,1,0],
|
||||
[7,3,1,0],
|
||||
[8,3,0,0],
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
public function setUpSeries() {
|
||||
];
|
||||
$this->data['arsse_feeds']['rows'] = [
|
||||
[1,"http://example.com/feed1", "Ook", "", "",strtotime("now"),''],
|
||||
[2,"http://example.com/feed2", "eek", "", "",strtotime("now - 1 hour"),'http://example.com/favicon.ico'],
|
||||
[3,"http://example.com/feed3", "Ack", "", "",strtotime("now + 1 hour"),''],
|
||||
];
|
||||
// initialize a partial mock of the Database object to later manipulate the feedUpdate method
|
||||
Arsse::$db = Phake::partialMock(Database::class, $this->drv);
|
||||
Arsse::$db = Phake::partialMock(Database::class, static::$drv);
|
||||
$this->user = "john.doe@example.com";
|
||||
}
|
||||
|
||||
protected function tearDownSeriesSubscription() {
|
||||
unset($this->data, $this->user);
|
||||
}
|
||||
|
||||
public function testAddASubscriptionToAnExistingFeed() {
|
||||
$url = "http://example.com/feed1";
|
||||
$subID = $this->nextID("arsse_subscriptions");
|
|
@ -4,28 +4,34 @@
|
|||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Test\Database;
|
||||
namespace JKingWeb\Arsse\TestCase\Database;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\User\Driver as UserDriver;
|
||||
use Phake;
|
||||
|
||||
trait SeriesUser {
|
||||
protected $data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
'rights' => 'int',
|
||||
protected function setUpSeriesUser() {
|
||||
$this->data = [
|
||||
'arsse_users' => [
|
||||
'columns' => [
|
||||
'id' => 'str',
|
||||
'password' => 'str',
|
||||
'name' => 'str',
|
||||
'rights' => 'int',
|
||||
],
|
||||
'rows' => [
|
||||
["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', "Hard Lip Herbert", 100], // password is hash of "secret"
|
||||
["jane.doe@example.com", "", "Jane Doe", 0],
|
||||
["john.doe@example.com", "", "John Doe", 0],
|
||||
],
|
||||
],
|
||||
'rows' => [
|
||||
["admin@example.net", '$2y$10$PbcG2ZR3Z8TuPzM7aHTF8.v61dtCjzjK78gdZJcp4UePE8T9jEgBW', "Hard Lip Herbert", 100], // password is hash of "secret"
|
||||
["jane.doe@example.com", "", "Jane Doe", 0],
|
||||
["john.doe@example.com", "", "John Doe", 0],
|
||||
],
|
||||
],
|
||||
];
|
||||
];
|
||||
}
|
||||
|
||||
protected function tearDownSeriesUser() {
|
||||
unset($this->data);
|
||||
}
|
||||
|
||||
public function testCheckThatAUserExists() {
|
||||
$this->assertTrue(Arsse::$db->userExists("jane.doe@example.com"));
|
398
tests/cases/Db/BaseDriver.php
Normal file
398
tests/cases/Db/BaseDriver.php
Normal file
|
@ -0,0 +1,398 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db;
|
||||
|
||||
use JKingWeb\Arsse\Db\Statement;
|
||||
use JKingWeb\Arsse\Db\Result;
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected static $dbInfo;
|
||||
protected static $interface;
|
||||
protected $drv;
|
||||
protected $create;
|
||||
protected $lock;
|
||||
protected $setVersion;
|
||||
protected static $conf = [
|
||||
'dbTimeoutExec' => 0.5,
|
||||
'dbSQLite3Timeout' => 0,
|
||||
//'dbSQLite3File' => "(temporary file)",
|
||||
];
|
||||
|
||||
public static function setUpBeforeClass() {
|
||||
// establish a clean baseline
|
||||
static::clearData();
|
||||
static::$dbInfo = new DatabaseInformation(static::$implementation);
|
||||
static::setConf(static::$conf);
|
||||
static::$interface = (static::$dbInfo->interfaceConstructor)();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
self::clearData();
|
||||
self::setConf(static::$conf);
|
||||
if (!static::$interface) {
|
||||
$this->markTestSkipped(static::$implementation." database driver not available");
|
||||
}
|
||||
// completely clear the database and ensure the schema version can easily be altered
|
||||
(static::$dbInfo->razeFunction)(static::$interface, [
|
||||
"CREATE TABLE arsse_meta(key varchar(255) primary key not null, value text)",
|
||||
"INSERT INTO arsse_meta(key,value) values('schema_version','0')",
|
||||
]);
|
||||
// construct a fresh driver for each test
|
||||
$this->drv = new static::$dbInfo->driverClass;
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
// deconstruct the driver
|
||||
unset($this->drv);
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
if (static::$interface) {
|
||||
// completely clear the database
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
}
|
||||
static::$interface = null;
|
||||
static::$dbInfo = null;
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
protected function exec($q): bool {
|
||||
// PDO implementation
|
||||
$q = (!is_array($q)) ? [$q] : $q;
|
||||
foreach ($q as $query) {
|
||||
static::$interface->exec((string) $query);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function query(string $q) {
|
||||
// PDO implementation
|
||||
return static::$interface->query($q)->fetchColumn();
|
||||
}
|
||||
|
||||
# TESTS
|
||||
|
||||
public function testFetchDriverName() {
|
||||
$class = get_class($this->drv);
|
||||
$this->assertTrue(strlen($class::driverName()) > 0);
|
||||
}
|
||||
|
||||
public function testFetchSchemaId() {
|
||||
$class = get_class($this->drv);
|
||||
$this->assertTrue(strlen($class::schemaID()) > 0);
|
||||
}
|
||||
|
||||
public function testCheckCharacterSetAcceptability() {
|
||||
$this->assertTrue($this->drv->charsetAcceptable());
|
||||
}
|
||||
|
||||
public function testTranslateAToken() {
|
||||
$this->assertRegExp("/^[a-z][a-z0-9]*$/i", $this->drv->sqlToken("greatest"));
|
||||
$this->assertSame("distinct", $this->drv->sqlToken("distinct"));
|
||||
}
|
||||
|
||||
public function testExecAValidStatement() {
|
||||
$this->assertTrue($this->drv->exec($this->create));
|
||||
}
|
||||
|
||||
public function testExecAnInvalidStatement() {
|
||||
$this->assertException("engineErrorGeneral", "Db");
|
||||
$this->drv->exec("And the meek shall inherit the earth...");
|
||||
}
|
||||
|
||||
public function testExecMultipleStatements() {
|
||||
$this->assertTrue($this->drv->exec("$this->create; INSERT INTO arsse_test(id) values(2112)"));
|
||||
$this->assertEquals(2112, $this->query("SELECT id from arsse_test"));
|
||||
}
|
||||
|
||||
public function testExecTimeout() {
|
||||
$this->exec($this->create);
|
||||
$this->exec($this->lock);
|
||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||
$lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock;
|
||||
$this->drv->exec($lock);
|
||||
}
|
||||
|
||||
public function testExecConstraintViolation() {
|
||||
$this->drv->exec("CREATE TABLE arsse_test(id varchar(255) not null)");
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$this->drv->exec("INSERT INTO arsse_test default values");
|
||||
}
|
||||
|
||||
public function testExecTypeViolation() {
|
||||
$this->drv->exec($this->create);
|
||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||
$this->drv->exec("INSERT INTO arsse_test(id) values('ook')");
|
||||
}
|
||||
|
||||
public function testMakeAValidQuery() {
|
||||
$this->assertInstanceOf(Result::class, $this->drv->query("SELECT 1"));
|
||||
}
|
||||
|
||||
public function testMakeAnInvalidQuery() {
|
||||
$this->assertException("engineErrorGeneral", "Db");
|
||||
$this->drv->query("Apollo was astonished; Dionysus thought me mad");
|
||||
}
|
||||
|
||||
public function testQueryTimeout() {
|
||||
$this->exec($this->create);
|
||||
$this->exec($this->lock);
|
||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||
$lock = is_array($this->lock) ? implode("; ", $this->lock) : $this->lock;
|
||||
$this->drv->exec($lock);
|
||||
}
|
||||
|
||||
public function testQueryConstraintViolation() {
|
||||
$this->drv->exec("CREATE TABLE arsse_test(id integer not null)");
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$this->drv->query("INSERT INTO arsse_test default values");
|
||||
}
|
||||
|
||||
public function testQueryTypeViolation() {
|
||||
$this->drv->exec($this->create);
|
||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||
$this->drv->query("INSERT INTO arsse_test(id) values('ook')");
|
||||
}
|
||||
|
||||
public function testPrepareAValidQuery() {
|
||||
$s = $this->drv->prepare("SELECT ?, ?", "int", "int");
|
||||
$this->assertInstanceOf(Statement::class, $s);
|
||||
}
|
||||
|
||||
public function testPrepareAnInvalidQuery() {
|
||||
$this->assertException("engineErrorGeneral", "Db");
|
||||
$s = $this->drv->prepare("This is an invalid query", "int", "int")->run();
|
||||
}
|
||||
|
||||
public function testCreateASavepoint() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
}
|
||||
|
||||
public function testReleaseASavepoint() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(true, $this->drv->savepointRelease());
|
||||
$this->assertException("savepointInvalid", "Db");
|
||||
$this->drv->savepointRelease();
|
||||
}
|
||||
|
||||
public function testUndoASavepoint() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(true, $this->drv->savepointUndo());
|
||||
$this->assertException("savepointInvalid", "Db");
|
||||
$this->drv->savepointUndo();
|
||||
}
|
||||
|
||||
public function testManipulateSavepoints() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
$this->assertEquals(4, $this->drv->savepointCreate());
|
||||
$this->assertEquals(5, $this->drv->savepointCreate());
|
||||
$this->assertTrue($this->drv->savepointUndo(3));
|
||||
$this->assertFalse($this->drv->savepointRelease(4));
|
||||
$this->assertEquals(6, $this->drv->savepointCreate());
|
||||
$this->assertFalse($this->drv->savepointRelease(5));
|
||||
$this->assertTrue($this->drv->savepointRelease(6));
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
$this->assertTrue($this->drv->savepointRelease(2));
|
||||
$this->assertException("savepointStale", "Db");
|
||||
$this->drv->savepointRelease(2);
|
||||
}
|
||||
|
||||
public function testManipulateSavepointsSomeMore() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
$this->assertEquals(4, $this->drv->savepointCreate());
|
||||
$this->assertTrue($this->drv->savepointRelease(2));
|
||||
$this->assertFalse($this->drv->savepointUndo(3));
|
||||
$this->assertException("savepointStale", "Db");
|
||||
$this->drv->savepointUndo(2);
|
||||
}
|
||||
|
||||
public function testBeginATransaction() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
}
|
||||
|
||||
public function testCommitATransaction() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr->commit();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(1, $this->query($select));
|
||||
}
|
||||
|
||||
public function testRollbackATransaction() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
}
|
||||
|
||||
public function testBeginChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
}
|
||||
|
||||
public function testCommitChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2->commit();
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr1->commit();
|
||||
$this->assertEquals(2, $this->query($select));
|
||||
}
|
||||
|
||||
public function testCommitChainedTransactionsOutOfOrder() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr1->commit();
|
||||
$this->assertEquals(2, $this->query($select));
|
||||
$tr2->commit();
|
||||
}
|
||||
|
||||
public function testRollbackChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2->rollback();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr1->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
}
|
||||
|
||||
public function testRollbackChainedTransactionsOutOfOrder() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr1->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
}
|
||||
|
||||
public function testPartiallyRollbackChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM arsse_test";
|
||||
$insert = "INSERT INTO arsse_test default values";
|
||||
$this->drv->exec($this->create);
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr2->rollback();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->query($select));
|
||||
$tr1->commit();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(1, $this->query($select));
|
||||
}
|
||||
|
||||
public function testFetchSchemaVersion() {
|
||||
$this->assertSame(0, $this->drv->schemaVersion());
|
||||
$this->drv->exec(str_replace("#", "1", $this->setVersion));
|
||||
$this->assertSame(1, $this->drv->schemaVersion());
|
||||
$this->drv->exec(str_replace("#", "2", $this->setVersion));
|
||||
$this->assertSame(2, $this->drv->schemaVersion());
|
||||
// SQLite is unaffected by the removal of the metadata table; other backends are
|
||||
// in neither case should a query for the schema version produce an error, however
|
||||
$this->exec("DROP TABLE IF EXISTS arsse_meta");
|
||||
$exp = (static::$dbInfo->backend == "SQLite 3") ? 2 : 0;
|
||||
$this->assertSame($exp, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testLockTheDatabase() {
|
||||
// PostgreSQL doesn't actually lock the whole database, only the metadata table
|
||||
// normally the application will first query this table to ensure the schema version is correct,
|
||||
// so the effect is usually the same
|
||||
$this->drv->savepointCreate(true);
|
||||
$this->assertException();
|
||||
$this->exec($this->lock);
|
||||
}
|
||||
|
||||
public function testUnlockTheDatabase() {
|
||||
$this->drv->savepointCreate(true);
|
||||
$this->drv->savepointRelease();
|
||||
$this->drv->savepointCreate(true);
|
||||
$this->drv->savepointUndo();
|
||||
$this->assertTrue($this->exec(str_replace("#", "3", $this->setVersion)));
|
||||
}
|
||||
}
|
137
tests/cases/Db/BaseResult.php
Normal file
137
tests/cases/Db/BaseResult.php
Normal file
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db;
|
||||
|
||||
use JKingWeb\Arsse\Db\Result;
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
abstract class BaseResult extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected static $dbInfo;
|
||||
protected static $interface;
|
||||
protected $resultClass;
|
||||
protected $stringOutput;
|
||||
|
||||
abstract protected function makeResult(string $q): array;
|
||||
|
||||
public static function setUpBeforeClass() {
|
||||
// establish a clean baseline
|
||||
static::clearData();
|
||||
static::$dbInfo = new DatabaseInformation(static::$implementation);
|
||||
static::setConf();
|
||||
static::$interface = (static::$dbInfo->interfaceConstructor)();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
self::clearData();
|
||||
self::setConf();
|
||||
if (!static::$interface) {
|
||||
$this->markTestSkipped(static::$implementation." database driver not available");
|
||||
}
|
||||
// completely clear the database
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
$this->resultClass = static::$dbInfo->resultClass;
|
||||
$this->stringOutput = static::$dbInfo->stringOutput;
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
if (static::$interface) {
|
||||
// completely clear the database
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
}
|
||||
static::$interface = null;
|
||||
static::$dbInfo = null;
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public function testConstructResult() {
|
||||
$this->assertInstanceOf(Result::class, new $this->resultClass(...$this->makeResult("SELECT 1")));
|
||||
}
|
||||
|
||||
public function testGetChangeCountAndLastInsertId() {
|
||||
$this->makeResult(static::$createMeta);
|
||||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_meta(key,value) values('test', 1)"));
|
||||
$this->assertSame(1, $r->changes());
|
||||
$this->assertSame(0, $r->lastId());
|
||||
}
|
||||
|
||||
public function testGetChangeCountAndLastInsertIdBis() {
|
||||
$this->makeResult(static::$createTest);
|
||||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_test default values"));
|
||||
$this->assertSame(1, $r->changes());
|
||||
$this->assertSame(1, $r->lastId());
|
||||
$r = new $this->resultClass(...$this->makeResult("INSERT INTO arsse_test default values"));
|
||||
$this->assertSame(1, $r->changes());
|
||||
$this->assertSame(2, $r->lastId());
|
||||
}
|
||||
|
||||
public function testIterateOverResults() {
|
||||
$exp = [0 => 1, 1 => 2, 2 => 3];
|
||||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp;
|
||||
foreach (new $this->resultClass(...$this->makeResult("SELECT 1 as col union select 2 as col union select 3 as col")) as $index => $row) {
|
||||
$rows[$index] = $row['col'];
|
||||
}
|
||||
$this->assertSame($exp, $rows);
|
||||
}
|
||||
|
||||
public function testIterateOverResultsTwice() {
|
||||
$exp = [0 => 1, 1 => 2, 2 => 3];
|
||||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp;
|
||||
$result = new $this->resultClass(...$this->makeResult("SELECT 1 as col union select 2 as col union select 3 as col"));
|
||||
foreach ($result as $index => $row) {
|
||||
$rows[$index] = $row['col'];
|
||||
}
|
||||
$this->assertSame($exp, $rows);
|
||||
$this->assertException("resultReused", "Db");
|
||||
foreach ($result as $row) {
|
||||
$rows[] = $row['col'];
|
||||
}
|
||||
}
|
||||
|
||||
public function testGetSingleValues() {
|
||||
$exp = [1867, 1970, 2112];
|
||||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp;
|
||||
$test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year union all select 1970 as year union all select 2112 as year"));
|
||||
$this->assertSame($exp[0], $test->getValue());
|
||||
$this->assertSame($exp[1], $test->getValue());
|
||||
$this->assertSame($exp[2], $test->getValue());
|
||||
$this->assertSame(null, $test->getValue());
|
||||
}
|
||||
|
||||
public function testGetFirstValuesOnly() {
|
||||
$exp = [1867, 1970, 2112];
|
||||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp;
|
||||
$test = new $this->resultClass(...$this->makeResult("SELECT 1867 as year, 19 as century union all select 1970 as year, 20 as century union all select 2112 as year, 22 as century"));
|
||||
$this->assertSame($exp[0], $test->getValue());
|
||||
$this->assertSame($exp[1], $test->getValue());
|
||||
$this->assertSame($exp[2], $test->getValue());
|
||||
$this->assertSame(null, $test->getValue());
|
||||
}
|
||||
|
||||
public function testGetRows() {
|
||||
$exp = [
|
||||
['album' => '2112', 'track' => '2112'],
|
||||
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'],
|
||||
];
|
||||
$test = new $this->resultClass(...$this->makeResult("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track"));
|
||||
$this->assertSame($exp[0], $test->getRow());
|
||||
$this->assertSame($exp[1], $test->getRow());
|
||||
$this->assertSame(null, $test->getRow());
|
||||
}
|
||||
|
||||
public function testGetAllRows() {
|
||||
$exp = [
|
||||
['album' => '2112', 'track' => '2112'],
|
||||
['album' => 'Clockwork Angels', 'track' => 'The Wreckers'],
|
||||
];
|
||||
$test = new $this->resultClass(...$this->makeResult("SELECT '2112' as album, '2112' as track union select 'Clockwork Angels' as album, 'The Wreckers' as track"));
|
||||
$this->assertEquals($exp, $test->getAll());
|
||||
}
|
||||
}
|
333
tests/cases/Db/BaseStatement.php
Normal file
333
tests/cases/Db/BaseStatement.php
Normal file
|
@ -0,0 +1,333 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db;
|
||||
|
||||
use JKingWeb\Arsse\Db\Statement;
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
abstract class BaseStatement extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected static $dbInfo;
|
||||
protected static $interface;
|
||||
protected $statementClass;
|
||||
protected $stringOutput;
|
||||
|
||||
abstract protected function makeStatement(string $q, array $types = []): array;
|
||||
abstract protected function decorateTypeSyntax(string $value, string $type): string;
|
||||
|
||||
public static function setUpBeforeClass() {
|
||||
// establish a clean baseline
|
||||
static::clearData();
|
||||
static::$dbInfo = new DatabaseInformation(static::$implementation);
|
||||
static::setConf();
|
||||
static::$interface = (static::$dbInfo->interfaceConstructor)();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
self::clearData();
|
||||
self::setConf();
|
||||
if (!static::$interface) {
|
||||
$this->markTestSkipped(static::$implementation." database driver not available");
|
||||
}
|
||||
// completely clear the database
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
$this->statementClass = static::$dbInfo->statementClass;
|
||||
$this->stringOutput = static::$dbInfo->stringOutput;
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
if (static::$interface) {
|
||||
// completely clear the database
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
}
|
||||
static::$interface = null;
|
||||
static::$dbInfo = null;
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public function testConstructStatement() {
|
||||
$this->assertInstanceOf(Statement::class, new $this->statementClass(...$this->makeStatement("SELECT ? as value")));
|
||||
}
|
||||
|
||||
/** @dataProvider provideBindings */
|
||||
public function testBindATypedValue($value, string $type, string $exp) {
|
||||
if ($exp=="null") {
|
||||
$query = "SELECT (? is null) as pass";
|
||||
} else {
|
||||
$query = "SELECT ($exp = ?) as pass";
|
||||
}
|
||||
$typeStr = "'".str_replace("'", "''", $type)."'";
|
||||
$s = new $this->statementClass(...$this->makeStatement($query));
|
||||
$s->retype(...[$type]);
|
||||
$act = $s->run(...[$value])->getValue();
|
||||
$this->assertTrue((bool) $act);
|
||||
}
|
||||
|
||||
/** @dataProvider provideBinaryBindings */
|
||||
public function testHandleBinaryData($value, string $type, string $exp) {
|
||||
if (in_array(static::$implementation, ["PostgreSQL", "PDO PostgreSQL"])) {
|
||||
$this->markTestSkipped("Correct handling of binary data with PostgreSQL is currently unknown");
|
||||
}
|
||||
if ($exp=="null") {
|
||||
$query = "SELECT (? is null) as pass";
|
||||
} else {
|
||||
$query = "SELECT ($exp = ?) as pass";
|
||||
}
|
||||
$typeStr = "'".str_replace("'", "''", $type)."'";
|
||||
$s = new $this->statementClass(...$this->makeStatement($query));
|
||||
$s->retype(...[$type]);
|
||||
$act = $s->run(...[$value])->getValue();
|
||||
$this->assertTrue((bool) $act);
|
||||
}
|
||||
|
||||
public function testBindMissingValue() {
|
||||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", ["int"]));
|
||||
$val = $s->runArray()->getRow()['value'];
|
||||
$this->assertSame(null, $val);
|
||||
}
|
||||
|
||||
public function testBindMultipleValues() {
|
||||
$exp = [
|
||||
'one' => 1,
|
||||
'two' => 2,
|
||||
];
|
||||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp;
|
||||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two", ["int", "int"]));
|
||||
$val = $s->runArray([1,2])->getRow();
|
||||
$this->assertSame($exp, $val);
|
||||
}
|
||||
|
||||
public function testBindRecursively() {
|
||||
$exp = [
|
||||
'one' => 1,
|
||||
'two' => 2,
|
||||
'three' => 3,
|
||||
'four' => 4,
|
||||
];
|
||||
$exp = $this->stringOutput ? $this->stringify($exp) : $exp;
|
||||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as one, ? as two, ? as three, ? as four", ["int", ["int", "int"], "int"]));
|
||||
$val = $s->runArray([1, [2, 3], 4])->getRow();
|
||||
$this->assertSame($exp, $val);
|
||||
}
|
||||
|
||||
public function testBindWithoutType() {
|
||||
$this->assertException("paramTypeMissing", "Db");
|
||||
$s = new $this->statementClass(...$this->makeStatement("SELECT ? as value", []));
|
||||
$s->runArray([1]);
|
||||
}
|
||||
|
||||
public function testViolateConstraint() {
|
||||
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_meta(key varchar(255) primary key not null, value text)")))->run();
|
||||
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_meta(key) values(?)", ["str"]));
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$s->runArray([null]);
|
||||
}
|
||||
|
||||
public function testMismatchTypes() {
|
||||
(new $this->statementClass(...$this->makeStatement("CREATE TABLE if not exists arsse_feeds(id integer primary key not null, url text not null)")))->run();
|
||||
$s = new $this->statementClass(...$this->makeStatement("INSERT INTO arsse_feeds(id,url) values(?,?)", ["str", "str"]));
|
||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||
$s->runArray(['ook', 'eek']);
|
||||
}
|
||||
|
||||
public function provideBindings() {
|
||||
$dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto"));
|
||||
$dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto"));
|
||||
$dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC"));
|
||||
$tests = [
|
||||
'Null as integer' => [null, "integer", "null"],
|
||||
'Null as float' => [null, "float", "null"],
|
||||
'Null as string' => [null, "string", "null"],
|
||||
'Null as datetime' => [null, "datetime", "null"],
|
||||
'Null as boolean' => [null, "boolean", "null"],
|
||||
'Null as strict integer' => [null, "strict integer", "0"],
|
||||
'Null as strict float' => [null, "strict float", "0.0"],
|
||||
'Null as strict string' => [null, "strict string", "''"],
|
||||
'Null as strict datetime' => [null, "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'Null as strict boolean' => [null, "strict boolean", "0"],
|
||||
'True as integer' => [true, "integer", "1"],
|
||||
'True as float' => [true, "float", "1.0"],
|
||||
'True as string' => [true, "string", "'1'"],
|
||||
'True as datetime' => [true, "datetime", "null"],
|
||||
'True as boolean' => [true, "boolean", "1"],
|
||||
'True as strict integer' => [true, "strict integer", "1"],
|
||||
'True as strict float' => [true, "strict float", "1.0"],
|
||||
'True as strict string' => [true, "strict string", "'1'"],
|
||||
'True as strict datetime' => [true, "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'True as strict boolean' => [true, "strict boolean", "1"],
|
||||
'False as integer' => [false, "integer", "0"],
|
||||
'False as float' => [false, "float", "0.0"],
|
||||
'False as string' => [false, "string", "''"],
|
||||
'False as datetime' => [false, "datetime", "null"],
|
||||
'False as boolean' => [false, "boolean", "0"],
|
||||
'False as strict integer' => [false, "strict integer", "0"],
|
||||
'False as strict float' => [false, "strict float", "0.0"],
|
||||
'False as strict string' => [false, "strict string", "''"],
|
||||
'False as strict datetime' => [false, "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'False as strict boolean' => [false, "strict boolean", "0"],
|
||||
'Integer as integer' => [2112, "integer", "2112"],
|
||||
'Integer as float' => [2112, "float", "2112.0"],
|
||||
'Integer as string' => [2112, "string", "'2112'"],
|
||||
'Integer as datetime' => [2112, "datetime", "'1970-01-01 00:35:12'"],
|
||||
'Integer as boolean' => [2112, "boolean", "1"],
|
||||
'Integer as strict integer' => [2112, "strict integer", "2112"],
|
||||
'Integer as strict float' => [2112, "strict float", "2112.0"],
|
||||
'Integer as strict string' => [2112, "strict string", "'2112'"],
|
||||
'Integer as strict datetime' => [2112, "strict datetime", "'1970-01-01 00:35:12'"],
|
||||
'Integer as strict boolean' => [2112, "strict boolean", "1"],
|
||||
'Integer zero as integer' => [0, "integer", "0"],
|
||||
'Integer zero as float' => [0, "float", "0.0"],
|
||||
'Integer zero as string' => [0, "string", "'0'"],
|
||||
'Integer zero as datetime' => [0, "datetime", "'1970-01-01 00:00:00'"],
|
||||
'Integer zero as boolean' => [0, "boolean", "0"],
|
||||
'Integer zero as strict integer' => [0, "strict integer", "0"],
|
||||
'Integer zero as strict float' => [0, "strict float", "0.0"],
|
||||
'Integer zero as strict string' => [0, "strict string", "'0'"],
|
||||
'Integer zero as strict datetime' => [0, "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'Integer zero as strict boolean' => [0, "strict boolean", "0"],
|
||||
'Float as integer' => [2112.5, "integer", "2112"],
|
||||
'Float as float' => [2112.5, "float", "2112.5"],
|
||||
'Float as string' => [2112.5, "string", "'2112.5'"],
|
||||
'Float as datetime' => [2112.5, "datetime", "'1970-01-01 00:35:12'"],
|
||||
'Float as boolean' => [2112.5, "boolean", "1"],
|
||||
'Float as strict integer' => [2112.5, "strict integer", "2112"],
|
||||
'Float as strict float' => [2112.5, "strict float", "2112.5"],
|
||||
'Float as strict string' => [2112.5, "strict string", "'2112.5'"],
|
||||
'Float as strict datetime' => [2112.5, "strict datetime", "'1970-01-01 00:35:12'"],
|
||||
'Float as strict boolean' => [2112.5, "strict boolean", "1"],
|
||||
'Float zero as integer' => [0.0, "integer", "0"],
|
||||
'Float zero as float' => [0.0, "float", "0.0"],
|
||||
'Float zero as string' => [0.0, "string", "'0'"],
|
||||
'Float zero as datetime' => [0.0, "datetime", "'1970-01-01 00:00:00'"],
|
||||
'Float zero as boolean' => [0.0, "boolean", "0"],
|
||||
'Float zero as strict integer' => [0.0, "strict integer", "0"],
|
||||
'Float zero as strict float' => [0.0, "strict float", "0.0"],
|
||||
'Float zero as strict string' => [0.0, "strict string", "'0'"],
|
||||
'Float zero as strict datetime' => [0.0, "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'Float zero as strict boolean' => [0.0, "strict boolean", "0"],
|
||||
'ASCII string as integer' => ["Random string", "integer", "0"],
|
||||
'ASCII string as float' => ["Random string", "float", "0.0"],
|
||||
'ASCII string as string' => ["Random string", "string", "'Random string'"],
|
||||
'ASCII string as datetime' => ["Random string", "datetime", "null"],
|
||||
'ASCII string as boolean' => ["Random string", "boolean", "1"],
|
||||
'ASCII string as strict integer' => ["Random string", "strict integer", "0"],
|
||||
'ASCII string as strict float' => ["Random string", "strict float", "0.0"],
|
||||
'ASCII string as strict string' => ["Random string", "strict string", "'Random string'"],
|
||||
'ASCII string as strict datetime' => ["Random string", "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'ASCII string as strict boolean' => ["Random string", "strict boolean", "1"],
|
||||
'UTF-8 string as integer' => ["\u{e9}", "integer", "0"],
|
||||
'UTF-8 string as float' => ["\u{e9}", "float", "0.0"],
|
||||
'UTF-8 string as string' => ["\u{e9}", "string", "char(233)"],
|
||||
'UTF-8 string as datetime' => ["\u{e9}", "datetime", "null"],
|
||||
'UTF-8 string as boolean' => ["\u{e9}", "boolean", "1"],
|
||||
'UTF-8 string as strict integer' => ["\u{e9}", "strict integer", "0"],
|
||||
'UTF-8 string as strict float' => ["\u{e9}", "strict float", "0.0"],
|
||||
'UTF-8 string as strict string' => ["\u{e9}", "strict string", "char(233)"],
|
||||
'UTF-8 string as strict datetime' => ["\u{e9}", "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'UTF-8 string as strict boolean' => ["\u{e9}", "strict boolean", "1"],
|
||||
'ISO 8601 string as integer' => ["2017-01-09T13:11:17", "integer", "0"],
|
||||
'ISO 8601 string as float' => ["2017-01-09T13:11:17", "float", "0.0"],
|
||||
'ISO 8601 string as string' => ["2017-01-09T13:11:17", "string", "'2017-01-09T13:11:17'"],
|
||||
'ISO 8601 string as datetime' => ["2017-01-09T13:11:17", "datetime", "'2017-01-09 13:11:17'"],
|
||||
'ISO 8601 string as boolean' => ["2017-01-09T13:11:17", "boolean", "1"],
|
||||
'ISO 8601 string as strict integer' => ["2017-01-09T13:11:17", "strict integer", "0"],
|
||||
'ISO 8601 string as strict float' => ["2017-01-09T13:11:17", "strict float", "0.0"],
|
||||
'ISO 8601 string as strict string' => ["2017-01-09T13:11:17", "strict string", "'2017-01-09T13:11:17'"],
|
||||
'ISO 8601 string as strict datetime' => ["2017-01-09T13:11:17", "strict datetime", "'2017-01-09 13:11:17'"],
|
||||
'ISO 8601 string as strict boolean' => ["2017-01-09T13:11:17", "strict boolean", "1"],
|
||||
'Arbitrary date string as integer' => ["Today", "integer", "0"],
|
||||
'Arbitrary date string as float' => ["Today", "float", "0.0"],
|
||||
'Arbitrary date string as string' => ["Today", "string", "'Today'"],
|
||||
'Arbitrary date string as datetime' => ["Today", "datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"],
|
||||
'Arbitrary date string as boolean' => ["Today", "boolean", "1"],
|
||||
'Arbitrary date string as strict integer' => ["Today", "strict integer", "0"],
|
||||
'Arbitrary date string as strict float' => ["Today", "strict float", "0.0"],
|
||||
'Arbitrary date string as strict string' => ["Today", "strict string", "'Today'"],
|
||||
'Arbitrary date string as strict datetime' => ["Today", "strict datetime", "'".date_create("Today", new \DateTimezone("UTC"))->format("Y-m-d H:i:s")."'"],
|
||||
'Arbitrary date string as strict boolean' => ["Today", "strict boolean", "1"],
|
||||
'DateTime as integer' => [$dateMutable, "integer", (string) $dateUTC->getTimestamp()],
|
||||
'DateTime as float' => [$dateMutable, "float", $dateUTC->getTimestamp().".0"],
|
||||
'DateTime as string' => [$dateMutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTime as datetime' => [$dateMutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTime as boolean' => [$dateMutable, "boolean", "1"],
|
||||
'DateTime as strict integer' => [$dateMutable, "strict integer", (string) $dateUTC->getTimestamp()],
|
||||
'DateTime as strict float' => [$dateMutable, "strict float", $dateUTC->getTimestamp().".0"],
|
||||
'DateTime as strict string' => [$dateMutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTime as strict datetime' => [$dateMutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTime as strict boolean' => [$dateMutable, "strict boolean", "1"],
|
||||
'DateTimeImmutable as integer' => [$dateImmutable, "integer", (string) $dateUTC->getTimestamp()],
|
||||
'DateTimeImmutable as float' => [$dateImmutable, "float", $dateUTC->getTimestamp().".0"],
|
||||
'DateTimeImmutable as string' => [$dateImmutable, "string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTimeImmutable as datetime' => [$dateImmutable, "datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTimeImmutable as boolean' => [$dateImmutable, "boolean", "1"],
|
||||
'DateTimeImmutable as strict integer' => [$dateImmutable, "strict integer", (string) $dateUTC->getTimestamp()],
|
||||
'DateTimeImmutable as strict float' => [$dateImmutable, "strict float", $dateUTC->getTimestamp().".0"],
|
||||
'DateTimeImmutable as strict string' => [$dateImmutable, "strict string", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTimeImmutable as strict datetime' => [$dateImmutable, "strict datetime", "'".$dateUTC->format("Y-m-d H:i:s")."'"],
|
||||
'DateTimeImmutable as strict boolean' => [$dateImmutable, "strict boolean", "1"],
|
||||
];
|
||||
foreach ($tests as $index => list($value, $type, $exp)) {
|
||||
$t = preg_replace("<^strict >", "", $type);
|
||||
$exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t);
|
||||
yield $index => [$value, $type, $exp];
|
||||
}
|
||||
}
|
||||
|
||||
public function provideBinaryBindings() {
|
||||
$dateMutable = new \DateTime("Noon Today", new \DateTimezone("America/Toronto"));
|
||||
$dateImmutable = new \DateTimeImmutable("Noon Today", new \DateTimezone("America/Toronto"));
|
||||
$dateUTC = new \DateTime("@".$dateMutable->getTimestamp(), new \DateTimezone("UTC"));
|
||||
$tests = [
|
||||
'Null as binary' => [null, "binary", "null"],
|
||||
'Null as strict binary' => [null, "strict binary", "x''"],
|
||||
'True as binary' => [true, "binary", "x'31'"],
|
||||
'True as strict binary' => [true, "strict binary", "x'31'"],
|
||||
'False as binary' => [false, "binary", "x''"],
|
||||
'False as strict binary' => [false, "strict binary", "x''"],
|
||||
'Integer as binary' => [2112, "binary", "x'32313132'"],
|
||||
'Integer as strict binary' => [2112, "strict binary", "x'32313132'"],
|
||||
'Integer zero as binary' => [0, "binary", "x'30'"],
|
||||
'Integer zero as strict binary' => [0, "strict binary", "x'30'"],
|
||||
'Float as binary' => [2112.5, "binary", "x'323131322e35'"],
|
||||
'Float as strict binary' => [2112.5, "strict binary", "x'323131322e35'"],
|
||||
'Float zero as binary' => [0.0, "binary", "x'30'"],
|
||||
'Float zero as strict binary' => [0.0, "strict binary", "x'30'"],
|
||||
'ASCII string as binary' => ["Random string", "binary", "x'52616e646f6d20737472696e67'"],
|
||||
'ASCII string as strict binary' => ["Random string", "strict binary", "x'52616e646f6d20737472696e67'"],
|
||||
'UTF-8 string as binary' => ["\u{e9}", "binary", "x'c3a9'"],
|
||||
'UTF-8 string as strict binary' => ["\u{e9}", "strict binary", "x'c3a9'"],
|
||||
'Binary string as integer' => [chr(233).chr(233), "integer", "0"],
|
||||
'Binary string as float' => [chr(233).chr(233), "float", "0.0"],
|
||||
'Binary string as string' => [chr(233).chr(233), "string", "'".chr(233).chr(233)."'"],
|
||||
'Binary string as binary' => [chr(233).chr(233), "binary", "x'e9e9'"],
|
||||
'Binary string as datetime' => [chr(233).chr(233), "datetime", "null"],
|
||||
'Binary string as boolean' => [chr(233).chr(233), "boolean", "1"],
|
||||
'Binary string as strict integer' => [chr(233).chr(233), "strict integer", "0"],
|
||||
'Binary string as strict float' => [chr(233).chr(233), "strict float", "0.0"],
|
||||
'Binary string as strict string' => [chr(233).chr(233), "strict string", "'".chr(233).chr(233)."'"],
|
||||
'Binary string as strict binary' => [chr(233).chr(233), "strict binary", "x'e9e9'"],
|
||||
'Binary string as strict datetime' => [chr(233).chr(233), "strict datetime", "'1970-01-01 00:00:00'"],
|
||||
'Binary string as strict boolean' => [chr(233).chr(233), "strict boolean", "1"],
|
||||
'ISO 8601 string as binary' => ["2017-01-09T13:11:17", "binary", "x'323031372d30312d30395431333a31313a3137'"],
|
||||
'ISO 8601 string as strict binary' => ["2017-01-09T13:11:17", "strict binary", "x'323031372d30312d30395431333a31313a3137'"],
|
||||
'Arbitrary date string as binary' => ["Today", "binary", "x'546f646179'"],
|
||||
'Arbitrary date string as strict binary' => ["Today", "strict binary", "x'546f646179'"],
|
||||
'DateTime as binary' => [$dateMutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
|
||||
'DateTime as strict binary' => [$dateMutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
|
||||
'DateTimeImmutable as binary' => [$dateImmutable, "binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
|
||||
'DateTimeImmutable as strict binary' => [$dateImmutable, "strict binary", "x'".bin2hex($dateUTC->format("Y-m-d H:i:s"))."'"],
|
||||
];
|
||||
foreach ($tests as $index => list($value, $type, $exp)) {
|
||||
$t = preg_replace("<^strict >", "", $type);
|
||||
$exp = ($exp=="null") ? $exp : $this->decorateTypeSyntax($exp, $t);
|
||||
yield $index => [$value, $type, $exp];
|
||||
}
|
||||
}
|
||||
}
|
136
tests/cases/Db/BaseUpdate.php
Normal file
136
tests/cases/Db/BaseUpdate.php
Normal file
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
use org\bovigo\vfs\vfsStream;
|
||||
|
||||
class BaseUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected static $dbInfo;
|
||||
protected static $interface;
|
||||
protected $drv;
|
||||
protected $vfs;
|
||||
protected $base;
|
||||
protected $path;
|
||||
|
||||
public static function setUpBeforeClass() {
|
||||
// establish a clean baseline
|
||||
static::clearData();
|
||||
static::$dbInfo = new DatabaseInformation(static::$implementation);
|
||||
static::setConf();
|
||||
static::$interface = (static::$dbInfo->interfaceConstructor)();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
if (!static::$interface) {
|
||||
$this->markTestSkipped(static::$implementation." database driver not available");
|
||||
}
|
||||
self::clearData();
|
||||
self::setConf();
|
||||
// construct a fresh driver for each test
|
||||
$this->drv = new static::$dbInfo->driverClass;
|
||||
$schemaId = (get_class($this->drv))::schemaID();
|
||||
// set up a virtual filesystem for schema files
|
||||
$this->vfs = vfsStream::setup("schemata", null, [$schemaId => []]);
|
||||
$this->base = $this->vfs->url();
|
||||
$this->path = $this->base."/$schemaId/";
|
||||
// completely clear the database
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
// deconstruct the driver
|
||||
unset($this->drv);
|
||||
unset($this->path, $this->base, $this->vfs);
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
if (static::$interface) {
|
||||
// completely clear the database
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
}
|
||||
static::$interface = null;
|
||||
static::$dbInfo = null;
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public function testLoadMissingFile() {
|
||||
$this->assertException("updateFileMissing", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadUnreadableFile() {
|
||||
touch($this->path."0.sql");
|
||||
chmod($this->path."0.sql", 0000);
|
||||
$this->assertException("updateFileUnreadable", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadCorruptFile() {
|
||||
file_put_contents($this->path."0.sql", "This is a corrupt file");
|
||||
$this->assertException("updateFileError", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadIncompleteFile() {
|
||||
file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadEmptyFile() {
|
||||
file_put_contents($this->path."0.sql", "");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadCorrectFile() {
|
||||
file_put_contents($this->path."0.sql", static::$minimal1);
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
$this->assertEquals(1, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testPerformPartialUpdate() {
|
||||
file_put_contents($this->path."0.sql", static::$minimal1);
|
||||
file_put_contents($this->path."1.sql", "UPDATE arsse_meta set value = '1' where key = 'schema_version'");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
try {
|
||||
$this->drv->schemaUpdate(2, $this->base);
|
||||
} catch (Exception $e) {
|
||||
$this->assertEquals(1, $this->drv->schemaVersion());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function testPerformSequentialUpdate() {
|
||||
file_put_contents($this->path."0.sql", static::$minimal1);
|
||||
file_put_contents($this->path."1.sql", static::$minimal2);
|
||||
$this->drv->schemaUpdate(2, $this->base);
|
||||
$this->assertEquals(2, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testPerformActualUpdate() {
|
||||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
||||
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testDeclineManualUpdate() {
|
||||
// turn auto-updating off
|
||||
Arsse::$conf->dbAutoUpdate = false;
|
||||
$this->assertException("updateManual", "Db");
|
||||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
public function testDeclineDowngrade() {
|
||||
$this->assertException("updateTooNew", "Db");
|
||||
$this->drv->schemaUpdate(-1, $this->base);
|
||||
}
|
||||
}
|
73
tests/cases/Db/PostgreSQL/TestCreation.php
Normal file
73
tests/cases/Db/PostgreSQL/TestCreation.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Db\PostgreSQL\Driver;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
|
||||
class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
public function setUp() {
|
||||
if (!Driver::requirementsMet()) {
|
||||
$this->markTestSkipped("PostgreSQL extension not loaded");
|
||||
}
|
||||
}
|
||||
|
||||
/** @dataProvider provideConnectionStrings */
|
||||
public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) {
|
||||
self::setConf();
|
||||
$timeout = (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0);
|
||||
$postfix = "application_name='arsse' client_encoding='UTF8' connect_timeout='$timeout'";
|
||||
$act = Driver::makeConnectionString($pdo, $user, $pass, $db, $host, $port, $service);
|
||||
if ($act==$postfix) {
|
||||
$this->assertSame($exp, "");
|
||||
} else {
|
||||
$test = substr($act, 0, strlen($act) - (strlen($postfix) + 1));
|
||||
$check = substr($act, strlen($test) + 1);
|
||||
$this->assertSame($postfix, $check);
|
||||
$this->assertSame($exp, $test);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideConnectionStrings() {
|
||||
return [
|
||||
[false, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse' password='secret' user='arsse'"],
|
||||
[false, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse' password='p word' user='arsse'"],
|
||||
[false, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse' password='p\\'word' user='arsse'"],
|
||||
[false, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db' password='secret' user='arsse user'"],
|
||||
[false, "arsse", "secret", "", "", 5432, "", "password='secret' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost' password='secret' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' password='secret' port='9999' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' password='secret' port='9999' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket' password='secret' user='arsse'"],
|
||||
[false, "T'Pau of Vulcan", "", "", "", 5432, "", "user='T\\'Pau of Vulcan'"],
|
||||
[false, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"],
|
||||
[true, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse'"],
|
||||
[true, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse'"],
|
||||
[true, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse'"],
|
||||
[true, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db'"],
|
||||
[true, "arsse", "secret", "", "", 5432, "", ""],
|
||||
[true, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost'"],
|
||||
[true, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' port='9999'"],
|
||||
[true, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' port='9999'"],
|
||||
[true, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket'"],
|
||||
[true, "T'Pau of Vulcan", "", "", "", 5432, "", ""],
|
||||
[true, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"],
|
||||
];
|
||||
}
|
||||
|
||||
public function testFailToConnect() {
|
||||
// we cannnot distinguish between different connection failure modes
|
||||
self::setConf([
|
||||
'dbPostgreSQLPass' => (string) rand(),
|
||||
]);
|
||||
$this->assertException("connectionFailure", "Db");
|
||||
new Driver;
|
||||
}
|
||||
}
|
43
tests/cases/Db/PostgreSQL/TestDatabase.php
Normal file
43
tests/cases/Db/PostgreSQL/TestDatabase.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @group coverageOptional
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query<extended>
|
||||
*/
|
||||
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
|
||||
protected static $implementation = "PostgreSQL";
|
||||
|
||||
protected function nextID(string $table): int {
|
||||
return (int) static::$drv->query("SELECT coalesce(last_value, (select max(id) from $table)) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$seqList =
|
||||
"select
|
||||
replace(substring(column_default, 10), right(column_default, 12), '') as seq,
|
||||
table_name as table,
|
||||
column_name as col
|
||||
from information_schema.columns
|
||||
where table_schema = current_schema()
|
||||
and table_name like 'arsse_%'
|
||||
and column_default like 'nextval(%'
|
||||
";
|
||||
foreach (static::$drv->query($seqList) as $r) {
|
||||
$num = (int) static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue();
|
||||
if (!$num) {
|
||||
continue;
|
||||
}
|
||||
$num++;
|
||||
static::$drv->exec("ALTER SEQUENCE {$r['seq']} RESTART WITH $num");
|
||||
}
|
||||
}
|
||||
}
|
58
tests/cases/Db/PostgreSQL/TestDriver.php
Normal file
58
tests/cases/Db/PostgreSQL/TestDriver.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<extended> */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
protected static $implementation = "PostgreSQL";
|
||||
protected $create = "CREATE TABLE arsse_test(id bigserial primary key)";
|
||||
protected $lock = ["BEGIN", "LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"];
|
||||
protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'";
|
||||
|
||||
public function tearDown() {
|
||||
try {
|
||||
$this->drv->exec("ROLLBACK");
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
if (static::$interface) {
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
@pg_close(static::$interface);
|
||||
static::$interface = null;
|
||||
}
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
protected function exec($q): bool {
|
||||
$q = (!is_array($q)) ? [$q] : $q;
|
||||
foreach ($q as $query) {
|
||||
set_error_handler(function($code, $msg) {
|
||||
throw new \Exception($msg);
|
||||
});
|
||||
try {
|
||||
pg_query(static::$interface, $query);
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function query(string $q) {
|
||||
if ($r = pg_query_params(static::$interface, $q, [])) {
|
||||
return pg_fetch_result($r, 0, 0);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
33
tests/cases/Db/PostgreSQL/TestResult.php
Normal file
33
tests/cases/Db/PostgreSQL/TestResult.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
||||
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Result<extended>
|
||||
*/
|
||||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
|
||||
protected static $implementation = "PostgreSQL";
|
||||
protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)";
|
||||
protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)";
|
||||
|
||||
protected function makeResult(string $q): array {
|
||||
$set = pg_query(static::$interface, $q);
|
||||
return [static::$interface, $set];
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
if (static::$interface) {
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
@pg_close(static::$interface);
|
||||
static::$interface = null;
|
||||
}
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
}
|
42
tests/cases/Db/PostgreSQL/TestStatement.php
Normal file
42
tests/cases/Db/PostgreSQL/TestStatement.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Statement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Dispatch<extended> */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
protected static $implementation = "PostgreSQL";
|
||||
|
||||
protected function makeStatement(string $q, array $types = []): array {
|
||||
return [static::$interface, $q, $types];
|
||||
}
|
||||
|
||||
protected function decorateTypeSyntax(string $value, string $type): string {
|
||||
switch ($type) {
|
||||
case "float":
|
||||
return (substr($value, -2)==".0") ? "'".substr($value, 0, strlen($value) - 2)."'" : "'$value'";
|
||||
case "string":
|
||||
if (preg_match("<^char\((\d+)\)$>", $value, $match)) {
|
||||
return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'";
|
||||
}
|
||||
return $value;
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
if (static::$interface) {
|
||||
(static::$dbInfo->razeFunction)(static::$interface);
|
||||
@pg_close(static::$interface);
|
||||
static::$interface = null;
|
||||
}
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
}
|
16
tests/cases/Db/PostgreSQL/TestUpdate.php
Normal file
16
tests/cases/Db/PostgreSQL/TestUpdate.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQL;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\Driver<extended> */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
protected static $implementation = "PostgreSQL";
|
||||
protected static $minimal1 = "CREATE TABLE arsse_meta(key text primary key, value text); INSERT INTO arsse_meta(key,value) values('schema_version','1');";
|
||||
protected static $minimal2 = "UPDATE arsse_meta set value = '2' where key = 'schema_version';";
|
||||
}
|
73
tests/cases/Db/PostgreSQLPDO/TestCreation.php
Normal file
73
tests/cases/Db/PostgreSQLPDO/TestCreation.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Db\PostgreSQL\PDODriver as Driver;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended> */
|
||||
class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
public function setUp() {
|
||||
if (!Driver::requirementsMet()) {
|
||||
$this->markTestSkipped("PDO-PostgreSQL extension not loaded");
|
||||
}
|
||||
}
|
||||
|
||||
/** @dataProvider provideConnectionStrings */
|
||||
public function testGenerateConnectionString(bool $pdo, string $user, string $pass, string $db, string $host, int $port, string $service, string $exp) {
|
||||
self::setConf();
|
||||
$timeout = (string) ceil(Arsse::$conf->dbTimeoutConnect ?? 0);
|
||||
$postfix = "application_name='arsse' client_encoding='UTF8' connect_timeout='$timeout'";
|
||||
$act = Driver::makeConnectionString($pdo, $user, $pass, $db, $host, $port, $service);
|
||||
if ($act==$postfix) {
|
||||
$this->assertSame($exp, "");
|
||||
} else {
|
||||
$test = substr($act, 0, strlen($act) - (strlen($postfix) + 1));
|
||||
$check = substr($act, strlen($test) + 1);
|
||||
$this->assertSame($postfix, $check);
|
||||
$this->assertSame($exp, $test);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideConnectionStrings() {
|
||||
return [
|
||||
[false, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse' password='secret' user='arsse'"],
|
||||
[false, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse' password='p word' user='arsse'"],
|
||||
[false, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse' password='p\\'word' user='arsse'"],
|
||||
[false, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db' password='secret' user='arsse user'"],
|
||||
[false, "arsse", "secret", "", "", 5432, "", "password='secret' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost' password='secret' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' password='secret' port='9999' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' password='secret' port='9999' user='arsse'"],
|
||||
[false, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket' password='secret' user='arsse'"],
|
||||
[false, "T'Pau of Vulcan", "", "", "", 5432, "", "user='T\\'Pau of Vulcan'"],
|
||||
[false, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"],
|
||||
[true, "arsse", "secret", "arsse", "", 5432, "", "dbname='arsse'"],
|
||||
[true, "arsse", "p word", "arsse", "", 5432, "", "dbname='arsse'"],
|
||||
[true, "arsse", "p'word", "arsse", "", 5432, "", "dbname='arsse'"],
|
||||
[true, "arsse user", "secret", "arsse db", "", 5432, "", "dbname='arsse db'"],
|
||||
[true, "arsse", "secret", "", "", 5432, "", ""],
|
||||
[true, "arsse", "secret", "arsse", "localhost", 5432, "", "dbname='arsse' host='localhost'"],
|
||||
[true, "arsse", "secret", "arsse", "", 9999, "", "dbname='arsse' port='9999'"],
|
||||
[true, "arsse", "secret", "arsse", "localhost", 9999, "", "dbname='arsse' host='localhost' port='9999'"],
|
||||
[true, "arsse", "secret", "arsse", "/socket", 9999, "", "dbname='arsse' host='/socket'"],
|
||||
[true, "T'Pau of Vulcan", "", "", "", 5432, "", ""],
|
||||
[true, "T'Pau of Vulcan", "superman", "datumbase", "somehost", 2112, "arsse", "service='arsse'"],
|
||||
];
|
||||
}
|
||||
|
||||
public function testFailToConnect() {
|
||||
// PDO dies not distinguish between different connection failure modes
|
||||
self::setConf([
|
||||
'dbPostgreSQLPass' => (string) rand(),
|
||||
]);
|
||||
$this->assertException("connectionFailure", "Db");
|
||||
new Driver;
|
||||
}
|
||||
}
|
44
tests/cases/Db/PostgreSQLPDO/TestDatabase.php
Normal file
44
tests/cases/Db/PostgreSQLPDO/TestDatabase.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @group optional
|
||||
* @group coverageOptional
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query<extended>
|
||||
*/
|
||||
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
|
||||
protected static $implementation = "PDO PostgreSQL";
|
||||
|
||||
protected function nextID(string $table): int {
|
||||
return (int) static::$drv->query("SELECT coalesce(last_value, (select max(id) from $table)) + 1 from pg_sequences where sequencename = '{$table}_id_seq'")->getValue();
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$seqList =
|
||||
"select
|
||||
replace(substring(column_default, 10), right(column_default, 12), '') as seq,
|
||||
table_name as table,
|
||||
column_name as col
|
||||
from information_schema.columns
|
||||
where table_schema = current_schema()
|
||||
and table_name like 'arsse_%'
|
||||
and column_default like 'nextval(%'
|
||||
";
|
||||
foreach (static::$drv->query($seqList) as $r) {
|
||||
$num = (int) static::$drv->query("SELECT max({$r['col']}) from {$r['table']}")->getValue();
|
||||
if (!$num) {
|
||||
continue;
|
||||
}
|
||||
$num++;
|
||||
static::$drv->exec("ALTER SEQUENCE {$r['seq']} RESTART WITH $num");
|
||||
}
|
||||
}
|
||||
}
|
27
tests/cases/Db/PostgreSQLPDO/TestDriver.php
Normal file
27
tests/cases/Db/PostgreSQLPDO/TestDriver.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDODriver
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
protected static $implementation = "PDO PostgreSQL";
|
||||
protected $create = "CREATE TABLE arsse_test(id bigserial primary key)";
|
||||
protected $lock = ["BEGIN", "LOCK TABLE arsse_meta IN EXCLUSIVE MODE NOWAIT"];
|
||||
protected $setVersion = "UPDATE arsse_meta set value = '#' where key = 'schema_version'";
|
||||
|
||||
public function tearDown() {
|
||||
try {
|
||||
$this->drv->exec("ROLLBACK");
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
24
tests/cases/Db/PostgreSQLPDO/TestResult.php
Normal file
24
tests/cases/Db/PostgreSQLPDO/TestResult.php
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
||||
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PDOResult<extended>
|
||||
*/
|
||||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
|
||||
protected static $implementation = "PDO PostgreSQL";
|
||||
protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text)";
|
||||
protected static $createTest = "CREATE TABLE arsse_test(id bigserial primary key)";
|
||||
|
||||
protected function makeResult(string $q): array {
|
||||
$set = static::$interface->query($q);
|
||||
return [static::$interface, $set];
|
||||
}
|
||||
}
|
33
tests/cases/Db/PostgreSQLPDO/TestStatement.php
Normal file
33
tests/cases/Db/PostgreSQLPDO/TestStatement.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDOStatement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
protected static $implementation = "PDO PostgreSQL";
|
||||
|
||||
protected function makeStatement(string $q, array $types = []): array {
|
||||
return [static::$interface, $q, $types];
|
||||
}
|
||||
|
||||
protected function decorateTypeSyntax(string $value, string $type): string {
|
||||
switch ($type) {
|
||||
case "float":
|
||||
return (substr($value, -2)==".0") ? "'".substr($value, 0, strlen($value) - 2)."'" : "'$value'";
|
||||
case "string":
|
||||
if (preg_match("<^char\((\d+)\)$>", $value, $match)) {
|
||||
return "U&'\\+".str_pad(dechex((int) $match[1]), 6, "0", \STR_PAD_LEFT)."'";
|
||||
}
|
||||
return $value;
|
||||
default:
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
17
tests/cases/Db/PostgreSQLPDO/TestUpdate.php
Normal file
17
tests/cases/Db/PostgreSQLPDO/TestUpdate.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\PostgreSQLPDO;
|
||||
|
||||
/**
|
||||
* @group slow
|
||||
* @covers \JKingWeb\Arsse\Db\PostgreSQL\PDODriver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\PDOError */
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
protected static $implementation = "PDO PostgreSQL";
|
||||
protected static $minimal1 = "CREATE TABLE arsse_meta(key text primary key, value text); INSERT INTO arsse_meta(key,value) values('schema_version','1');";
|
||||
protected static $minimal2 = "UPDATE arsse_meta set value = '2' where key = 'schema_version';";
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestArticle extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesArticle;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestCleanup extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesCleanup;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesFeed;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestFolder extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesFolder;
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestLabel extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesLabel;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestMeta extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesMeta;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestMiscellany extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesMiscellany;
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestSession extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesSession;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestSubscription extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesSubscription;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestUser extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesUser;
|
||||
}
|
|
@ -24,7 +24,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
if (!Driver::requirementsMet()) {
|
||||
$this->markTestSkipped("SQLite extension not loaded");
|
||||
}
|
||||
$this->clearData();
|
||||
self::clearData();
|
||||
// test files
|
||||
$this->files = [
|
||||
// cannot create files
|
||||
|
@ -107,11 +107,11 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
chmod($path."Awal/arsse.db-wal", 0111);
|
||||
chmod($path."Ashm/arsse.db-shm", 0111);
|
||||
// set up configuration
|
||||
$this->setConf(['dbSQLite3File' => ":memory:"]);
|
||||
self::setConf();
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
$this->clearData();
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public function testFailToCreateDatabase() {
|
||||
|
|
20
tests/cases/Db/SQLite3/TestDatabase.php
Normal file
20
tests/cases/Db/SQLite3/TestDatabase.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
|
||||
|
||||
/**
|
||||
* @group optional
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query<extended>
|
||||
*/
|
||||
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
|
||||
protected static $implementation = "SQLite 3";
|
||||
|
||||
protected function nextID(string $table): int {
|
||||
return static::$drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue();
|
||||
}
|
||||
}
|
|
@ -6,338 +6,42 @@
|
|||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Db\SQLite3\Driver;
|
||||
use JKingWeb\Arsse\Db\Result;
|
||||
use JKingWeb\Arsse\Db\Statement;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\Driver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
||||
class TestDriver extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected $data;
|
||||
protected $drv;
|
||||
protected $ch;
|
||||
class TestDriver extends \JKingWeb\Arsse\TestCase\Db\BaseDriver {
|
||||
protected static $implementation = "SQLite 3";
|
||||
protected $create = "CREATE TABLE arsse_test(id integer primary key)";
|
||||
protected $lock = "BEGIN EXCLUSIVE TRANSACTION";
|
||||
protected $setVersion = "PRAGMA user_version=#";
|
||||
protected static $file;
|
||||
|
||||
public function setUp() {
|
||||
if (!Driver::requirementsMet()) {
|
||||
$this->markTestSkipped("SQLite extension not loaded");
|
||||
public static function setUpBeforeClass() {
|
||||
// create a temporary database file rather than using a memory database
|
||||
// some tests require one connection to block another, so a memory database is not suitable
|
||||
static::$file = tempnam(sys_get_temp_dir(), 'ook');
|
||||
static::$conf['dbSQLite3File'] = static::$file;
|
||||
parent::setUpBeforeclass();
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
static::$interface->close();
|
||||
static::$interface = null;
|
||||
parent::tearDownAfterClass();
|
||||
@unlink(static::$file);
|
||||
static::$file = null;
|
||||
}
|
||||
|
||||
protected function exec($q): bool {
|
||||
// SQLite's implementation coincidentally matches PDO's, but we reproduce it here for correctness' sake
|
||||
$q = (!is_array($q)) ? [$q] : $q;
|
||||
foreach ($q as $query) {
|
||||
static::$interface->exec((string) $query);
|
||||
}
|
||||
$this->clearData();
|
||||
$this->setConf([
|
||||
'dbDriver' => Driver::class,
|
||||
'dbSQLite3Timeout' => 0,
|
||||
'dbSQLite3File' => tempnam(sys_get_temp_dir(), 'ook'),
|
||||
]);
|
||||
$this->drv = new Driver();
|
||||
$this->ch = new \SQLite3(Arsse::$conf->dbSQLite3File);
|
||||
$this->ch->enableExceptions(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
unset($this->drv);
|
||||
unset($this->ch);
|
||||
if (isset(Arsse::$conf)) {
|
||||
unlink(Arsse::$conf->dbSQLite3File);
|
||||
}
|
||||
$this->clearData();
|
||||
}
|
||||
|
||||
public function testFetchDriverName() {
|
||||
$class = Arsse::$conf->dbDriver;
|
||||
$this->assertTrue(strlen($class::driverName()) > 0);
|
||||
}
|
||||
|
||||
public function testCheckCharacterSetAcceptability() {
|
||||
$this->assertTrue($this->drv->charsetAcceptable());
|
||||
}
|
||||
|
||||
public function testExecAValidStatement() {
|
||||
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key)"));
|
||||
}
|
||||
|
||||
public function testExecAnInvalidStatement() {
|
||||
$this->assertException("engineErrorGeneral", "Db");
|
||||
$this->drv->exec("And the meek shall inherit the earth...");
|
||||
}
|
||||
|
||||
public function testExecMultipleStatements() {
|
||||
$this->assertTrue($this->drv->exec("CREATE TABLE test(id integer primary key); INSERT INTO test(id) values(2112)"));
|
||||
$this->assertEquals(2112, $this->ch->querySingle("SELECT id from test"));
|
||||
}
|
||||
|
||||
public function testExecTimeout() {
|
||||
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
|
||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
}
|
||||
|
||||
public function testExecConstraintViolation() {
|
||||
$this->drv->exec("CREATE TABLE test(id integer not null)");
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$this->drv->exec("INSERT INTO test(id) values(null)");
|
||||
}
|
||||
|
||||
public function testExecTypeViolation() {
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||
$this->drv->exec("INSERT INTO test(id) values('ook')");
|
||||
}
|
||||
|
||||
public function testMakeAValidQuery() {
|
||||
$this->assertInstanceOf(Result::class, $this->drv->query("SELECT 1"));
|
||||
}
|
||||
|
||||
public function testMakeAnInvalidQuery() {
|
||||
$this->assertException("engineErrorGeneral", "Db");
|
||||
$this->drv->query("Apollo was astonished; Dionysus thought me mad");
|
||||
}
|
||||
|
||||
public function testQueryTimeout() {
|
||||
$this->ch->exec("BEGIN EXCLUSIVE TRANSACTION");
|
||||
$this->assertException("general", "Db", "ExceptionTimeout");
|
||||
$this->drv->query("CREATE TABLE test(id integer primary key)");
|
||||
}
|
||||
|
||||
public function testQueryConstraintViolation() {
|
||||
$this->drv->exec("CREATE TABLE test(id integer not null)");
|
||||
$this->assertException("constraintViolation", "Db", "ExceptionInput");
|
||||
$this->drv->query("INSERT INTO test(id) values(null)");
|
||||
}
|
||||
|
||||
public function testQueryTypeViolation() {
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$this->assertException("typeViolation", "Db", "ExceptionInput");
|
||||
$this->drv->query("INSERT INTO test(id) values('ook')");
|
||||
}
|
||||
|
||||
public function testPrepareAValidQuery() {
|
||||
$s = $this->drv->prepare("SELECT ?, ?", "int", "int");
|
||||
$this->assertInstanceOf(Statement::class, $s);
|
||||
}
|
||||
|
||||
public function testPrepareAnInvalidQuery() {
|
||||
$this->assertException("engineErrorGeneral", "Db");
|
||||
$s = $this->drv->prepare("This is an invalid query", "int", "int");
|
||||
}
|
||||
|
||||
public function testCreateASavepoint() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
}
|
||||
|
||||
public function testReleaseASavepoint() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(true, $this->drv->savepointRelease());
|
||||
$this->assertException("savepointInvalid", "Db");
|
||||
$this->drv->savepointRelease();
|
||||
}
|
||||
|
||||
public function testUndoASavepoint() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(true, $this->drv->savepointUndo());
|
||||
$this->assertException("savepointInvalid", "Db");
|
||||
$this->drv->savepointUndo();
|
||||
}
|
||||
|
||||
public function testManipulateSavepoints() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
$this->assertEquals(4, $this->drv->savepointCreate());
|
||||
$this->assertEquals(5, $this->drv->savepointCreate());
|
||||
$this->assertTrue($this->drv->savepointUndo(3));
|
||||
$this->assertFalse($this->drv->savepointRelease(4));
|
||||
$this->assertEquals(6, $this->drv->savepointCreate());
|
||||
$this->assertFalse($this->drv->savepointRelease(5));
|
||||
$this->assertTrue($this->drv->savepointRelease(6));
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
$this->assertTrue($this->drv->savepointRelease(2));
|
||||
$this->assertException("savepointStale", "Db");
|
||||
$this->drv->savepointRelease(2);
|
||||
}
|
||||
|
||||
public function testManipulateSavepointsSomeMore() {
|
||||
$this->assertEquals(1, $this->drv->savepointCreate());
|
||||
$this->assertEquals(2, $this->drv->savepointCreate());
|
||||
$this->assertEquals(3, $this->drv->savepointCreate());
|
||||
$this->assertEquals(4, $this->drv->savepointCreate());
|
||||
$this->assertTrue($this->drv->savepointRelease(2));
|
||||
$this->assertFalse($this->drv->savepointUndo(3));
|
||||
$this->assertException("savepointStale", "Db");
|
||||
$this->drv->savepointUndo(2);
|
||||
}
|
||||
|
||||
public function testBeginATransaction() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testCommitATransaction() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr->commit();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(1, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testRollbackATransaction() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testBeginChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testCommitChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2->commit();
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr1->commit();
|
||||
$this->assertEquals(2, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testCommitChainedTransactionsOutOfOrder() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr1->commit();
|
||||
$this->assertEquals(2, $this->ch->querySingle($select));
|
||||
$tr2->commit();
|
||||
}
|
||||
|
||||
public function testRollbackChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2->rollback();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr1->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testRollbackChainedTransactionsOutOfOrder() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr1->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2->rollback();
|
||||
$this->assertEquals(0, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testPartiallyRollbackChainedTransactions() {
|
||||
$select = "SELECT count(*) FROM test";
|
||||
$insert = "INSERT INTO test(id) values(null)";
|
||||
$this->drv->exec("CREATE TABLE test(id integer primary key)");
|
||||
$tr1 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2 = $this->drv->begin();
|
||||
$this->drv->query($insert);
|
||||
$this->assertEquals(2, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr2->rollback();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(0, $this->ch->querySingle($select));
|
||||
$tr1->commit();
|
||||
$this->assertEquals(1, $this->drv->query($select)->getValue());
|
||||
$this->assertEquals(1, $this->ch->querySingle($select));
|
||||
}
|
||||
|
||||
public function testFetchSchemaVersion() {
|
||||
$this->assertSame(0, $this->drv->schemaVersion());
|
||||
$this->drv->exec("PRAGMA user_version=1");
|
||||
$this->assertSame(1, $this->drv->schemaVersion());
|
||||
$this->drv->exec("PRAGMA user_version=2");
|
||||
$this->assertSame(2, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testLockTheDatabase() {
|
||||
$this->drv->savepointCreate(true);
|
||||
$this->assertException();
|
||||
$this->ch->exec("CREATE TABLE test(id integer primary key)");
|
||||
}
|
||||
|
||||
public function testUnlockTheDatabase() {
|
||||
$this->drv->savepointCreate(true);
|
||||
$this->drv->savepointRelease();
|
||||
$this->drv->savepointCreate(true);
|
||||
$this->drv->savepointUndo();
|
||||
$this->assertSame(true, $this->ch->exec("CREATE TABLE test(id integer primary key)"));
|
||||
protected function query(string $q) {
|
||||
return static::$interface->querySingle($q);
|
||||
}
|
||||
}
|
||||
|
|
31
tests/cases/Db/SQLite3/TestResult.php
Normal file
31
tests/cases/Db/SQLite3/TestResult.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
|
||||
|
||||
use JKingWeb\Arsse\Test\DatabaseInformation;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\Result<extended>
|
||||
*/
|
||||
class TestResult extends \JKingWeb\Arsse\TestCase\Db\BaseResult {
|
||||
protected static $implementation = "SQLite 3";
|
||||
protected static $createMeta = "CREATE TABLE arsse_meta(key text primary key not null, value text) without rowid";
|
||||
protected static $createTest = "CREATE TABLE arsse_test(id integer primary key)";
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
static::$interface->close();
|
||||
static::$interface = null;
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
protected function makeResult(string $q): array {
|
||||
$set = static::$interface->query($q);
|
||||
$rows = static::$interface->changes();
|
||||
$id = static::$interface->lastInsertRowID();
|
||||
return [$set, [$rows, $id]];
|
||||
}
|
||||
}
|
28
tests/cases/Db/SQLite3/TestStatement.php
Normal file
28
tests/cases/Db/SQLite3/TestStatement.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\Statement<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
||||
class TestStatement extends \JKingWeb\Arsse\TestCase\Db\BaseStatement {
|
||||
protected static $implementation = "SQLite 3";
|
||||
|
||||
public static function tearDownAfterClass() {
|
||||
static::$interface->close();
|
||||
static::$interface = null;
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
|
||||
protected function makeStatement(string $q, array $types = []): array {
|
||||
return [static::$interface, static::$interface->prepare($q), $types];
|
||||
}
|
||||
|
||||
protected function decorateTypeSyntax(string $value, string $type): string {
|
||||
return $value;
|
||||
}
|
||||
}
|
|
@ -6,118 +6,17 @@
|
|||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3;
|
||||
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
use JKingWeb\Arsse\Conf;
|
||||
use JKingWeb\Arsse\Database;
|
||||
use JKingWeb\Arsse\Db\Exception;
|
||||
use JKingWeb\Arsse\Db\SQLite3\Driver;
|
||||
use org\bovigo\vfs\vfsStream;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\Driver<extended>
|
||||
* @covers \JKingWeb\Arsse\Db\SQLite3\ExceptionBuilder */
|
||||
class TestUpdate extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
protected $data;
|
||||
protected $drv;
|
||||
protected $vfs;
|
||||
protected $base;
|
||||
class TestUpdate extends \JKingWeb\Arsse\TestCase\Db\BaseUpdate {
|
||||
protected static $implementation = "SQLite 3";
|
||||
protected static $minimal1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1";
|
||||
protected static $minimal2 = "pragma user_version=2";
|
||||
|
||||
const MINIMAL1 = "create table arsse_meta(key text primary key not null, value text); pragma user_version=1";
|
||||
const MINIMAL2 = "pragma user_version=2";
|
||||
|
||||
public function setUp(Conf $conf = null) {
|
||||
if (!Driver::requirementsMet()) {
|
||||
$this->markTestSkipped("SQLite extension not loaded");
|
||||
}
|
||||
$this->clearData();
|
||||
$this->vfs = vfsStream::setup("schemata", null, ['SQLite3' => []]);
|
||||
$conf = $conf ?? new Conf;
|
||||
$conf->dbDriver = Driver::class;
|
||||
$conf->dbSQLite3File = ":memory:";
|
||||
Arsse::$conf = $conf;
|
||||
$this->base = $this->vfs->url();
|
||||
$this->path = $this->base."/SQLite3/";
|
||||
$this->drv = new Driver();
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
unset($this->drv);
|
||||
unset($this->data);
|
||||
unset($this->vfs);
|
||||
$this->clearData();
|
||||
}
|
||||
|
||||
public function testLoadMissingFile() {
|
||||
$this->assertException("updateFileMissing", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadUnreadableFile() {
|
||||
touch($this->path."0.sql");
|
||||
chmod($this->path."0.sql", 0000);
|
||||
$this->assertException("updateFileUnreadable", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadCorruptFile() {
|
||||
file_put_contents($this->path."0.sql", "This is a corrupt file");
|
||||
$this->assertException("updateFileError", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadIncompleteFile() {
|
||||
file_put_contents($this->path."0.sql", "create table arsse_meta(key text primary key not null, value text);");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadEmptyFile() {
|
||||
file_put_contents($this->path."0.sql", "");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
}
|
||||
|
||||
public function testLoadCorrectFile() {
|
||||
file_put_contents($this->path."0.sql", self::MINIMAL1);
|
||||
$this->drv->schemaUpdate(1, $this->base);
|
||||
$this->assertEquals(1, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testPerformPartialUpdate() {
|
||||
file_put_contents($this->path."0.sql", self::MINIMAL1);
|
||||
file_put_contents($this->path."1.sql", " ");
|
||||
$this->assertException("updateFileIncomplete", "Db");
|
||||
try {
|
||||
$this->drv->schemaUpdate(2, $this->base);
|
||||
} catch (Exception $e) {
|
||||
$this->assertEquals(1, $this->drv->schemaVersion());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function testPerformSequentialUpdate() {
|
||||
file_put_contents($this->path."0.sql", self::MINIMAL1);
|
||||
file_put_contents($this->path."1.sql", self::MINIMAL2);
|
||||
$this->drv->schemaUpdate(2, $this->base);
|
||||
$this->assertEquals(2, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testPerformActualUpdate() {
|
||||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
||||
$this->assertEquals(Database::SCHEMA_VERSION, $this->drv->schemaVersion());
|
||||
}
|
||||
|
||||
public function testDeclineManualUpdate() {
|
||||
// turn auto-updating off
|
||||
$conf = new Conf;
|
||||
$conf->dbAutoUpdate = false;
|
||||
$this->setUp($conf);
|
||||
$this->assertException("updateManual", "Db");
|
||||
$this->drv->schemaUpdate(Database::SCHEMA_VERSION);
|
||||
}
|
||||
|
||||
public function testDeclineDowngrade() {
|
||||
$this->assertException("updateTooNew", "Db");
|
||||
$this->drv->schemaUpdate(-1, $this->base);
|
||||
public static function tearDownAfterClass() {
|
||||
static::$interface->close();
|
||||
static::$interface = null;
|
||||
parent::tearDownAfterClass();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestArticle extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesArticle;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestCleanup extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesCleanup;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestFeed extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesFeed;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestFolder extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesFolder;
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestLabel extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesLabel;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestMeta extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesMeta;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestMiscellany extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesMiscellany;
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestSession extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesSession;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestSubscription extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesSubscription;
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO\Database;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query
|
||||
*/
|
||||
class TestUser extends \JKingWeb\Arsse\Test\AbstractTest {
|
||||
use \JKingWeb\Arsse\Test\Database\Setup;
|
||||
use \JKingWeb\Arsse\Test\Database\DriverSQLite3PDO;
|
||||
use \JKingWeb\Arsse\Test\Database\SeriesUser;
|
||||
}
|
|
@ -25,7 +25,7 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
if (!Driver::requirementsMet()) {
|
||||
$this->markTestSkipped("PDO-SQLite extension not loaded");
|
||||
}
|
||||
$this->clearData();
|
||||
self::clearData();
|
||||
// test files
|
||||
$this->files = [
|
||||
// cannot create files
|
||||
|
@ -108,11 +108,11 @@ class TestCreation extends \JKingWeb\Arsse\Test\AbstractTest {
|
|||
chmod($path."Awal/arsse.db-wal", 0111);
|
||||
chmod($path."Ashm/arsse.db-shm", 0111);
|
||||
// set up configuration
|
||||
$this->setConf(['dbSQLite3File' => ":memory:"]);
|
||||
self::setConf();
|
||||
}
|
||||
|
||||
public function tearDown() {
|
||||
$this->clearData();
|
||||
self::clearData();
|
||||
}
|
||||
|
||||
public function testFailToCreateDatabase() {
|
||||
|
|
19
tests/cases/Db/SQLite3PDO/TestDatabase.php
Normal file
19
tests/cases/Db/SQLite3PDO/TestDatabase.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
/** @license MIT
|
||||
* Copyright 2017 J. King, Dustin Wilson et al.
|
||||
* See LICENSE and AUTHORS files for details */
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\TestCase\Db\SQLite3PDO;
|
||||
|
||||
/**
|
||||
* @covers \JKingWeb\Arsse\Database<extended>
|
||||
* @covers \JKingWeb\Arsse\Misc\Query<extended>
|
||||
*/
|
||||
class TestDatabase extends \JKingWeb\Arsse\TestCase\Database\Base {
|
||||
protected static $implementation = "PDO SQLite 3";
|
||||
|
||||
protected function nextID(string $table): int {
|
||||
return (int) static::$drv->query("SELECT (case when max(id) then max(id) else 0 end)+1 from $table")->getValue();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue