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; } }