2020-03-14 17:18:51 +00:00
|
|
|
<?php
|
2020-09-23 22:22:39 +00:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
|
2020-03-14 17:18:51 +00:00
|
|
|
register_module([
|
|
|
|
"name" => "Library: Storage box",
|
2022-02-27 15:35:01 +00:00
|
|
|
"version" => "0.13.1",
|
2020-03-14 17:18:51 +00:00
|
|
|
"author" => "Starbeamrainbowlabs",
|
|
|
|
"description" => "A library module that provides a fast cached key-value store backed by SQLite. Used by the search engine.",
|
|
|
|
"id" => "lib-storage-box",
|
|
|
|
"code" => function() {
|
|
|
|
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
|
|
|
|
/*
|
|
|
|
███████ ████████ ██████ ██████ █████ ██████ ███████ ██████ ██████ ██ ██
|
|
|
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
|
|
███████ ██ ██ ██ ██████ ███████ ██ ███ █████ ██████ ██ ██ ███
|
|
|
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
|
|
|
███████ ██ ██████ ██ ██ ██ ██ ██████ ███████ ██████ ██████ ██ ██
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents a key-value data store.
|
2020-03-15 17:54:27 +00:00
|
|
|
*
|
2020-03-14 17:18:51 +00:00
|
|
|
*/
|
|
|
|
class StorageBox {
|
|
|
|
const MODE_JSON = 0;
|
|
|
|
const MODE_ARR_SIMPLE = 1;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The SQLite database connection.
|
|
|
|
* @var \PDO
|
|
|
|
*/
|
|
|
|
private $db;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A cache of values.
|
|
|
|
* @var object[]
|
|
|
|
*/
|
|
|
|
private $cache = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A cache of prepared SQL statements.
|
|
|
|
* @var \PDOStatement[]
|
|
|
|
*/
|
|
|
|
private $query_cache = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialises a new store connection.
|
|
|
|
* @param string $filename The filename that the store is located in.
|
|
|
|
*/
|
|
|
|
function __construct(string $filename) {
|
|
|
|
$firstrun = !file_exists($filename);
|
2022-02-27 15:56:34 +00:00
|
|
|
if(!file_exists($filename)) touch($filename);
|
|
|
|
$this->db = new \PDO("sqlite:$filename"); // HACK: This might not work on some systems, because it depends on the current working directory
|
2020-03-14 17:18:51 +00:00
|
|
|
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
|
|
if($firstrun) {
|
2022-02-27 15:35:01 +00:00
|
|
|
$this->query("CREATE TABLE IF NOT EXISTS store (key TEXT UNIQUE NOT NULL, value TEXT)");
|
2020-03-14 17:18:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* Makes a query against the database.
|
|
|
|
* @param string $sql The (potentially parametised) query to make.
|
|
|
|
* @param array $variables Optional. The variables to substitute into the SQL query.
|
|
|
|
* @return \PDOStatement The result of the query, as a PDOStatement.
|
|
|
|
*/
|
|
|
|
private function query(string $sql, array $variables = []) {
|
|
|
|
// Add to the query cache if it doesn't exist
|
|
|
|
if(!isset($this->query_cache[$sql]))
|
|
|
|
$this->query_cache[$sql] = $this->db->prepare($sql);
|
|
|
|
$this->query_cache[$sql]->execute($variables);
|
|
|
|
return $this->query_cache[$sql]; // fetchColumn(), fetchAll(), etc. are defined on the statement, not the return value of execute()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if the given key exists in the store or not.
|
|
|
|
* @param string $key The key to test.
|
|
|
|
* @return bool Whether the key exists in the store or not.
|
|
|
|
*/
|
|
|
|
public function has(string $key) : bool {
|
|
|
|
if(isset($this->cache[$key]))
|
|
|
|
return true;
|
|
|
|
return $this->query(
|
|
|
|
"SELECT COUNT(key) FROM store WHERE key = :key;",
|
|
|
|
[ "key" => $key ]
|
|
|
|
)->fetchColumn() > 0;
|
|
|
|
}
|
|
|
|
|
2020-03-15 17:54:27 +00:00
|
|
|
/**
|
|
|
|
* Returns an iterable that returns all the keys that do not contain the given string.
|
|
|
|
* @param string $exclude The string to search for when excluding keys.
|
|
|
|
* @return PDOStatement The iterable. Use a foreach loop on it.
|
|
|
|
*/
|
|
|
|
public function get_keys(string $exclude) : \PDOStatement {
|
|
|
|
return $this->query(
|
2020-03-15 22:04:18 +00:00
|
|
|
"SELECT key FROM store WHERE key NOT LIKE :containing;",
|
2020-03-15 17:54:27 +00:00
|
|
|
[ "containing" => "%$exclude%" ]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-03-14 17:18:51 +00:00
|
|
|
/**
|
|
|
|
* Gets a value from the store.
|
|
|
|
* @param string $key The key value is stored under.
|
|
|
|
* @return mixed The stored value.
|
|
|
|
*/
|
|
|
|
public function get(string $key) {
|
|
|
|
// If it's not in the cache, insert it
|
|
|
|
if(!isset($this->cache[$key])) {
|
|
|
|
$this->cache[$key] = [ "modified" => false, "value" => json_decode($this->query(
|
|
|
|
"SELECT value FROM store WHERE key = :key;",
|
|
|
|
[ "key" => $key ]
|
|
|
|
)->fetchColumn()) ];
|
|
|
|
}
|
|
|
|
return $this->cache[$key]["value"];
|
|
|
|
}
|
|
|
|
public function get_arr_simple(string $key, string $delimiter = "|") {
|
|
|
|
// If it's not in the cache, insert it
|
|
|
|
if(!isset($this->cache[$key])) {
|
|
|
|
$this->cache[$key] = [
|
|
|
|
"modified" => false,
|
|
|
|
"value" => explode($delimiter, $this->query(
|
|
|
|
"SELECT value FROM store WHERE key = :key;",
|
|
|
|
[ "key" => $key ]
|
|
|
|
)->fetchColumn())
|
|
|
|
];
|
|
|
|
}
|
|
|
|
return $this->cache[$key]["value"];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets a value in the data store.
|
|
|
|
* Note that this does NOT save changes to disk until you close the connection!
|
|
|
|
* @param string $key The key to set the value of.
|
|
|
|
* @param mixed $value The value to store.
|
|
|
|
*/
|
|
|
|
public function set(string $key, $value) : void {
|
|
|
|
if(!isset($this->cache[$key])) $this->cache[$key] = [];
|
|
|
|
$this->cache[$key]["value"] = $value;
|
|
|
|
$this->cache[$key]["modified"] = true;
|
|
|
|
$this->cache[$key]["mode"] = self::MODE_JSON;
|
|
|
|
}
|
|
|
|
public function set_arr_simple(string $key, $value, string $delimiter = "|") : void {
|
|
|
|
if(!isset($this->cache[$key])) $this->cache[$key] = [];
|
|
|
|
$this->cache[$key]["value"] = $value;
|
|
|
|
$this->cache[$key]["modified"] = true;
|
|
|
|
$this->cache[$key]["delimiter"] = $delimiter;
|
|
|
|
$this->cache[$key]["mode"] = self::MODE_ARR_SIMPLE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes an item from the data store.
|
|
|
|
* @param string $key The key of the item to delete.
|
|
|
|
* @return bool Whether it was really deleted or not. Note that if it doesn't exist, then it can't be deleted.
|
|
|
|
*/
|
|
|
|
public function delete(string $key) : bool {
|
|
|
|
// Remove it from the cache
|
|
|
|
if(isset($this->cache[$key]))
|
|
|
|
unset($this->cache[$key]);
|
|
|
|
// Remove it from disk
|
|
|
|
// TODO: Queue this action for the transaction later
|
|
|
|
return $this->query(
|
|
|
|
"DELETE FROM store WHERE key = :key;",
|
|
|
|
[ "key" => $key ]
|
|
|
|
)->rowCount() > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Empties the store.
|
|
|
|
*/
|
|
|
|
public function clear() : void {
|
|
|
|
// Empty the cache;
|
|
|
|
$this->cache = [];
|
|
|
|
// Empty the disk
|
|
|
|
$this->query("DELETE FROM store;");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Syncs changes to disk and closes the PDO connection.
|
|
|
|
*/
|
|
|
|
public function close() : void {
|
|
|
|
$this->db->beginTransaction();
|
|
|
|
foreach($this->cache as $key => $value_data) {
|
|
|
|
// If it wasn't modified, there's no point in saving it, is there?
|
|
|
|
if(!$value_data["modified"])
|
|
|
|
continue;
|
|
|
|
|
|
|
|
$this->query(
|
|
|
|
"INSERT OR REPLACE INTO store(key, value) VALUES(:key, :value)",
|
|
|
|
[
|
|
|
|
"key" => $key,
|
|
|
|
"value" => $value_data["mode"] == self::MODE_ARR_SIMPLE ?
|
|
|
|
implode($value_data["delimiter"], $value_data["value"]) :
|
|
|
|
json_encode($value_data["value"])
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
$this->db->commit();
|
|
|
|
$this->db = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
?>
|