247 lines
7.7 KiB
PHP
247 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace NoccyLabs\LiteDb;
|
|
|
|
use PDO;
|
|
use PDOException;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
|
|
/**
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
class LiteDb
|
|
{
|
|
|
|
/** @var int|null The current database schema version or null if not initialized */
|
|
private $version = null;
|
|
/** @var string|null The database filename */
|
|
private $dbfile;
|
|
/** @var PDO */
|
|
private $pdo;
|
|
/** @var array Store metadata */
|
|
private $storeMeta = [];
|
|
/** @var ObjectStore[] */
|
|
private $stores = [];
|
|
/** @var array */
|
|
private $metadata = [];
|
|
|
|
/** @var null|LoggerInterface */
|
|
public static $logger;
|
|
|
|
// Metadata constants
|
|
const META_LDB_VERSION = "ldb.version";
|
|
const META_SCHEMA_VERSION = "schema.version";
|
|
const META_STORES = "schema.stores";
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string The path to the database file to use
|
|
* @param int The database version, defaults to 1
|
|
* @param callable|null The upgrade handler
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function __construct(string $dbfile, int $version=1, ?callable $upgradeHandler=null)
|
|
{
|
|
// Assign a null logger if none has been set
|
|
if (self::$logger == null) {
|
|
self::$logger = new NullLogger();
|
|
}
|
|
|
|
try {
|
|
self::$logger->info("Opening Sqlite3 database file {$dbfile}");
|
|
/** @var PDO $pdo */
|
|
$pdo = new PDO("sqlite:" . $dbfile);
|
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$this->initializeDatabaseFile($pdo);
|
|
$this->pdo = $pdo;
|
|
} catch (PDOException $e) {
|
|
// rethrow
|
|
throw new \RuntimeException("Database could not be opened/created", 0, $e);
|
|
}
|
|
|
|
$dbVersion = $this->dbGetMeta(self::META_LDB_VERSION, null);
|
|
if ($dbVersion == null) {
|
|
$this->dbSetMeta(self::META_LDB_VERSION, 1);
|
|
} elseif ($dbVersion > 1) {
|
|
throw new \RuntimeException("Unsupported database version {$dbVersion}");
|
|
}
|
|
|
|
$this->version = $this->dbGetMeta(self::META_SCHEMA_VERSION, null);
|
|
|
|
// Call on the upgrade handler if this version is newer
|
|
if (is_callable($upgradeHandler) && ($version > $this->version)) {
|
|
self::$logger->notice("The database schema version " . ($this->version??'null') . " need to be upgraded to version {$version}");
|
|
call_user_func($upgradeHandler, $this, $this->version);
|
|
// Write the new version number to the database
|
|
$this->dbSetMeta(self::META_SCHEMA_VERSION, $version);
|
|
$this->version = $version;
|
|
}
|
|
|
|
// Refresh the database version, for debug purposes
|
|
$dbVersion = $this->dbGetMeta(self::META_LDB_VERSION, null);
|
|
self::$logger->debug("Database version: {$dbVersion}");
|
|
self::$logger->debug("Current schema version: {$version}");
|
|
self::$logger->debug("Created stores:");
|
|
foreach ($this->storeMeta as $k=>$v)
|
|
self::$logger->debug(sprintf(" %s: %s", $k, json_encode($v)));
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Open and initialize the database file.
|
|
*
|
|
* Creates all the necessary tables and sets the current database version.
|
|
*
|
|
* @param PDO The opened database handle
|
|
*/
|
|
protected function initializeDatabaseFile(PDO $pdo)
|
|
{
|
|
// check to ensure that if there are more than 0 tables, at least one of them is
|
|
// the ldb_metadata table. If not, we are trying to open the wrong file.
|
|
$tables = $pdo->query("SELECT name FROM sqlite_master WHERE type='table';")->fetchAll(PDO::FETCH_ASSOC);
|
|
$tables = array_map(function ($v) { return $v['name']; }, $tables);
|
|
|
|
// Check for the magic table
|
|
if (count($tables) > 0 && !in_array('ldb_metadata', $tables)) {
|
|
throw new \RuntimeException("Invalid database file, no ldb_metadata table found");
|
|
}
|
|
|
|
// Create or update core tables and indexes
|
|
//
|
|
// ldb_metadata key (string, primary)
|
|
// value
|
|
// ldb_store name (string)
|
|
// options (string) [serialized]
|
|
// indexes (string) [serialized]
|
|
// primkey (int)
|
|
//
|
|
$pdo->exec("create table if not exists ldb_metadata (key string primary key, value);");
|
|
$pdo->exec("create unique index if not exists ldb_metadata_key on ldb_metadata(key);");
|
|
|
|
// Preload the metadata
|
|
$meta = $pdo->query("SELECT key,value FROM ldb_metadata;")->fetchAll(PDO::FETCH_ASSOC);
|
|
foreach ($meta as $row) {
|
|
$this->metadata[$row['key']] = unserialize($row['value']);
|
|
}
|
|
|
|
// Preload the defined stores
|
|
$this->storeMeta = $this->dbGetMeta(self::META_STORES, []);
|
|
|
|
}
|
|
|
|
/**
|
|
* Create or update an object store
|
|
*
|
|
* @param string The name of the store to create or update
|
|
* @param null|array An array holding any options for the store
|
|
*/
|
|
public function createStore(string $storeName, ?array $options=null): ObjectStore
|
|
{
|
|
// Make sure our options are an array, and then set a magic flag to indicate that
|
|
// we want to create the store if it doesn't exist.
|
|
$options = (array)$options;
|
|
self::$logger->info("Creating store facade for {$storeName}");
|
|
$store = new ObjectStore($this, $storeName, $options);
|
|
$this->stores[$storeName] = $store;
|
|
return $store;
|
|
}
|
|
|
|
/**
|
|
* Helper magic function to fetch a store
|
|
*
|
|
* @param string The name of the store to access
|
|
* @return ObjectStore The object store facade
|
|
*/
|
|
public function __get(string $storeName)
|
|
{
|
|
return $this->getStore($storeName);
|
|
}
|
|
|
|
/**
|
|
* Fetch a store
|
|
*
|
|
* @param string The name of the store to access
|
|
* @return ObjectStore The object store facade
|
|
*/
|
|
public function getStore(string $storeName)
|
|
{
|
|
if (!array_key_exists($storeName, $this->stores) && array_key_exists($storeName, $this->storeMeta)) {
|
|
// Open the store
|
|
$this->createStore($storeName);
|
|
} elseif (!array_key_exists($storeName, $this->storeMeta)) {
|
|
// Throw exception
|
|
}
|
|
|
|
return $this->stores[$storeName];
|
|
}
|
|
|
|
public function hasStore(string $storeName): bool
|
|
{
|
|
return array_key_exists($storeName, $this->storeMeta);
|
|
}
|
|
|
|
public function getVersion(): int
|
|
{
|
|
return $this->version;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string
|
|
* @param mixed
|
|
*/
|
|
public function dbSetMeta(string $key, $value)
|
|
{
|
|
$qKey = $this->pdo->quote($key);
|
|
$qValue = $this->pdo->quote(serialize($value));
|
|
$this->pdo->exec("REPLACE INTO ldb_metadata (key,value) VALUES ({$qKey},{$qValue});");
|
|
self::$logger->info("Setting metadata {$key} = ".json_encode($value));
|
|
|
|
$this->metadata[$key] = $value;
|
|
}
|
|
|
|
public function dbSetStoreMeta(string $store, array $meta)
|
|
{
|
|
$this->storeMeta[$store] = $meta;
|
|
$this->dbSetMeta(self::META_STORES, $this->storeMeta);
|
|
}
|
|
|
|
public function dbGetStoreMeta(string $store): array
|
|
{
|
|
if (!array_key_exists($store, $this->storeMeta)) {
|
|
return [];
|
|
}
|
|
return $this->storeMeta[$store];
|
|
}
|
|
|
|
public function dbHasStore(string $store): bool
|
|
{
|
|
return array_key_exists($store, $this->storeMeta);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param string
|
|
* @param mixed
|
|
*/
|
|
public function dbGetMeta(string $key, $defaultValue=null)
|
|
{
|
|
if (array_key_exists($key, $this->metadata)) {
|
|
return $this->metadata[$key];
|
|
} else {
|
|
return $defaultValue;
|
|
}
|
|
}
|
|
|
|
public function dbGetHandle(): ?PDO
|
|
{
|
|
return $this->pdo;
|
|
}
|
|
|
|
} |